diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f424ef5d518..8b6627395f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ concurrency: cancel-in-progress: true env: - COMPILER_IMAGE: stashapp/compiler:9 + COMPILER_IMAGE: stashapp/compiler:10 jobs: build: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index e29d56c7999..cbb3b021f62 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -9,7 +9,7 @@ on: pull_request: env: - COMPILER_IMAGE: stashapp/compiler:9 + COMPILER_IMAGE: stashapp/compiler:10 jobs: golangci: diff --git a/docker/ci/x86_64/Dockerfile b/docker/ci/x86_64/Dockerfile index 96610b7d219..f0f1e242b78 100644 --- a/docker/ci/x86_64/Dockerfile +++ b/docker/ci/x86_64/Dockerfile @@ -12,16 +12,18 @@ 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 +LABEL org.opencontainers.image.title="Stash" \ + org.opencontainers.image.description="An organizer for your porn, written in Go." \ + org.opencontainers.image.url="https://stashapp.cc" \ + org.opencontainers.image.documentation="https://docs.stashapp.cc" \ + org.opencontainers.image.source="https://github.com/stashapp/stash" \ + org.opencontainers.image.licenses="AGPL-3.0" + EXPOSE 9999 CMD ["stash"] diff --git a/docker/compiler/Dockerfile b/docker/compiler/Dockerfile index d69cea3e34d..737f0ee10f6 100644 --- a/docker/compiler/Dockerfile +++ b/docker/compiler/Dockerfile @@ -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 && \ diff --git a/docker/compiler/Makefile b/docker/compiler/Makefile index dbd9e16f89e..2411fdabb69 100644 --- a/docker/compiler/Makefile +++ b/docker/compiler/Makefile @@ -1,6 +1,6 @@ user=stashapp repo=compiler -version=9 +version=10 latest: docker build -t ${user}/${repo}:latest . diff --git a/go.mod b/go.mod index 9dabee3ac77..05ed48645d2 100644 --- a/go.mod +++ b/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 @@ -48,27 +48,27 @@ 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/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 @@ -112,12 +112,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 ) diff --git a/go.sum b/go.sum index 2a67217ab52..6c7b6a46de0 100644 --- a/go.sum +++ b/go.sum @@ -51,11 +51,8 @@ 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/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +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= @@ -64,17 +61,15 @@ github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3 github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 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= @@ -165,20 +160,17 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV 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/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw= -github.com/dhui/dktest v0.3.16/go.mod h1:gYaA3LRmM8Z4vJl2MA0THIigJoZrwOansEOsp+kqxp0= +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= @@ -361,7 +353,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= @@ -458,7 +449,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= @@ -482,7 +472,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= @@ -491,7 +480,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= @@ -528,7 +516,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= @@ -618,7 +605,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= @@ -630,12 +616,10 @@ 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= @@ -695,24 +679,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= @@ -759,8 +740,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= @@ -801,10 +782,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= @@ -854,8 +834,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= @@ -885,8 +865,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= @@ -969,7 +949,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= @@ -978,13 +957,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= @@ -997,8 +976,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= @@ -1044,7 +1023,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= @@ -1061,11 +1039,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= diff --git a/gqlgen.yml b/gqlgen.yml index 9f22ccb49e2..dc101f03c3e 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -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: diff --git a/graphql/schema/types/stash-box.graphql b/graphql/schema/types/stash-box.graphql index 71ea757f443..d1da8c74a76 100644 --- a/graphql/schema/types/stash-box.graphql +++ b/graphql/schema/types/stash-box.graphql @@ -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 { diff --git a/graphql/stash-box/query.graphql b/graphql/stash-box/query.graphql index 75dbc9797f0..a8a6b8f9cd7 100644 --- a/graphql/stash-box/query.graphql +++ b/graphql/stash-box/query.graphql @@ -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 diff --git a/internal/api/changeset_translator.go b/internal/api/changeset_translator.go index 1170088aac9..5c81c12cb09 100644 --- a/internal/api/changeset_translator.go +++ b/internal/api/changeset_translator.go @@ -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, } } diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index 7263cc70966..87f0883ed24 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -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 { diff --git a/internal/api/resolver_mutation_scene.go b/internal/api/resolver_mutation_scene.go index ca99dafc150..b0c6ac8b5aa 100644 --- a/internal/api/resolver_mutation_scene.go +++ b/internal/api/resolver_mutation_scene.go @@ -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 { diff --git a/internal/api/resolver_mutation_studio.go b/internal/api/resolver_mutation_studio.go index a33e5d9b676..727951755e9 100644 --- a/internal/api/resolver_mutation_studio.go +++ b/internal/api/resolver_mutation_studio.go @@ -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 diff --git a/internal/api/resolver_query_find_image.go b/internal/api/resolver_query_find_image.go index b3d674613a8..48b926345de 100644 --- a/internal/api/resolver_query_find_image.go +++ b/internal/api/resolver_query_find_image.go @@ -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) diff --git a/internal/api/resolver_query_find_scene.go b/internal/api/resolver_query_find_scene.go index 0ea35a490e5..44b5cfd5ee5 100644 --- a/internal/api/resolver_query_find_scene.go +++ b/internal/api/resolver_query_find_scene.go @@ -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 diff --git a/internal/api/resolver_query_package.go b/internal/api/resolver_query_package.go index 5a42221d476..7e772413263 100644 --- a/internal/api/resolver_query_package.go +++ b/internal/api/resolver_query_package.go @@ -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 diff --git a/internal/api/server.go b/internal/api/server.go index 63a81da7c2e..bce8e6a07d1 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -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) diff --git a/internal/autotag/gallery.go b/internal/autotag/gallery.go index cbf5ebf0919..031079e494b 100644 --- a/internal/autotag/gallery.go +++ b/internal/autotag/gallery.go @@ -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 } diff --git a/internal/autotag/image.go b/internal/autotag/image.go index 63544123ab4..e4acbcd3af6 100644 --- a/internal/autotag/image.go +++ b/internal/autotag/image.go @@ -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 } diff --git a/internal/autotag/performer.go b/internal/autotag/performer.go index 12dac0e9344..7badda39047 100644 --- a/internal/autotag/performer.go +++ b/internal/autotag/performer.go @@ -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 } diff --git a/internal/autotag/scene.go b/internal/autotag/scene.go index 751d0ed62c3..273378b9bc1 100644 --- a/internal/autotag/scene.go +++ b/internal/autotag/scene.go @@ -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 } diff --git a/internal/autotag/tag.go b/internal/autotag/tag.go index 5b1b5c319b2..4ebbf28a31d 100644 --- a/internal/autotag/tag.go +++ b/internal/autotag/tag.go @@ -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 } diff --git a/internal/dlna/cds.go b/internal/dlna/cds.go index a38e0e55bed..034ebbbc17f 100644 --- a/internal/dlna/cds.go +++ b/internal/dlna/cds.go @@ -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 } diff --git a/internal/dlna/whitelist.go b/internal/dlna/whitelist.go index 609ecd38f4c..e423ec58b37 100644 --- a/internal/dlna/whitelist.go +++ b/internal/dlna/whitelist.go @@ -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 diff --git a/internal/identify/identify.go b/internal/identify/identify.go index 5eecd0d9927..70d9322274a 100644 --- a/internal/identify/identify.go +++ b/internal/identify/identify.go @@ -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 } diff --git a/internal/identify/identify_test.go b/internal/identify/identify_test.go index 5dc339eace6..4d8c6e21231 100644 --- a/internal/identify/identify_test.go +++ b/internal/identify/identify_test.go @@ -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 diff --git a/internal/identify/scene.go b/internal/identify/scene.go index 05f1ba90076..847a140c5ae 100644 --- a/internal/identify/scene.go +++ b/internal/identify/scene.go @@ -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 } diff --git a/internal/identify/scene_test.go b/internal/identify/scene_test.go index 272ca43cb1d..7587eee7e27 100644 --- a/internal/identify/scene_test.go +++ b/internal/identify/scene_test.go @@ -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) } diff --git a/internal/manager/config/init.go b/internal/manager/config/init.go index 9c9caafb386..09f1c18bc26 100644 --- a/internal/manager/config/init.go +++ b/internal/manager/config/init.go @@ -39,6 +39,7 @@ var ( "external_host": ExternalHost, "generated": Generated, "metadata": Metadata, + "blobs": BlobsPath, "cache": Cache, "stash": Stash, "ui": UILocation, diff --git a/internal/manager/task_generate_interactive_heatmap_speed.go b/internal/manager/task_generate_interactive_heatmap_speed.go index 61350f09c2b..8a9543d9a68 100644 --- a/internal/manager/task_generate_interactive_heatmap_speed.go +++ b/internal/manager/task_generate_interactive_heatmap_speed.go @@ -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()) } diff --git a/internal/manager/task_scan.go b/internal/manager/task_scan.go index 48a06d09829..6f7f34b3c9d 100644 --- a/internal/manager/task_scan.go +++ b/internal/manager/task_scan.go @@ -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(), } } diff --git a/pkg/ffmpeg/stream_transcode.go b/pkg/ffmpeg/stream_transcode.go index e0a30cdd9e5..bb701664f8e 100644 --- a/pkg/ffmpeg/stream_transcode.go +++ b/pkg/ffmpeg/stream_transcode.go @@ -1,6 +1,7 @@ package ffmpeg import ( + "context" "errors" "io" "net/http" @@ -230,7 +231,10 @@ func (sm *StreamManager) ServeTranscode(w http.ResponseWriter, r *http.Request, handler, err := sm.getTranscodeStream(lockCtx, options) if err != nil { - logger.Errorf("[transcode] error transcoding video file: %v", err) + // don't log context canceled errors + if !errors.Is(err, context.Canceled) { + logger.Errorf("[transcode] error transcoding video file: %v", err) + } w.WriteHeader(http.StatusBadRequest) if _, err := w.Write([]byte(err.Error())); err != nil { logger.Warnf("[transcode] error writing response: %v", err) diff --git a/pkg/gallery/import.go b/pkg/gallery/import.go index c332e0ce043..aaf37bd27e4 100644 --- a/pkg/gallery/import.go +++ b/pkg/gallery/import.go @@ -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 { @@ -212,7 +213,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 { diff --git a/pkg/group/import.go b/pkg/group/import.go index 589e75df30d..3fc7db8f15a 100644 --- a/pkg/group/import.go +++ b/pkg/group/import.go @@ -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 { diff --git a/pkg/group/validate.go b/pkg/group/validate.go index 723b9f6997a..255152a9577 100644 --- a/pkg/group/validate.go +++ b/pkg/group/validate.go @@ -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 } diff --git a/pkg/image/import.go b/pkg/image/import.go index fa8fe21610e..660eb1da18d 100644 --- a/pkg/image/import.go +++ b/pkg/image/import.go @@ -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 { @@ -375,7 +376,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 { diff --git a/pkg/image/scan.go b/pkg/image/scan.go index b388a814518..a6002057f41 100644 --- a/pkg/image/scan.go +++ b/pkg/image/scan.go @@ -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 } diff --git a/pkg/models/model_joins.go b/pkg/models/model_joins.go index 7b7cae3e46a..c6cc8c2b228 100644 --- a/pkg/models/model_joins.go +++ b/pkg/models/model_joins.go @@ -33,7 +33,7 @@ func (u *UpdateGroupIDs) SceneMovieInputs() []SceneMovieInput { return nil } - ret := make([]SceneMovieInput, len(u.Groups)) + ret := make([]SceneMovieInput, 0, len(u.Groups)) for _, id := range u.Groups { ret = append(ret, id.SceneMovieInput()) } diff --git a/pkg/models/model_scene.go b/pkg/models/model_scene.go index 3f26a8cb6d8..cf04993882c 100644 --- a/pkg/models/model_scene.go +++ b/pkg/models/model_scene.go @@ -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 diff --git a/pkg/models/model_scraped_item.go b/pkg/models/model_scraped_item.go index 35f781109cb..43e3e985b3b 100644 --- a/pkg/models/model_scraped_item.go +++ b/pkg/models/model_scraped_item.go @@ -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(), }) } diff --git a/pkg/models/model_scraped_item_test.go b/pkg/models/model_scraped_item_test.go index 87ce2ad57dc..1e8edccb410 100644 --- a/pkg/models/model_scraped_item_test.go +++ b/pkg/models/model_scraped_item_test.go @@ -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) }) diff --git a/pkg/models/performer.go b/pkg/models/performer.go index b14f60044be..47394996d3f 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -226,14 +226,14 @@ 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"` } type PerformerUpdateInput struct { @@ -263,12 +263,12 @@ 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"` } diff --git a/pkg/models/scene.go b/pkg/models/scene.go index 48317240276..c7be343d98c 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -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 { diff --git a/pkg/models/stash_ids.go b/pkg/models/stash_ids.go index fcc2bdec0c2..7751c2ef01c 100644 --- a/pkg/models/stash_ids.go +++ b/pkg/models/stash_ids.go @@ -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 { diff --git a/pkg/models/studio.go b/pkg/models/studio.go index d5575b7ad3b..03ea8a84dcd 100644 --- a/pkg/models/studio.go +++ b/pkg/models/studio.go @@ -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"` } diff --git a/pkg/performer/import.go b/pkg/performer/import.go index d50384fa3d3..49a2ce291ae 100644 --- a/pkg/performer/import.go +++ b/pkg/performer/import.go @@ -3,6 +3,7 @@ package performer import ( "context" "fmt" + "slices" "strconv" "strings" @@ -75,7 +76,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 { diff --git a/pkg/plugin/plugins.go b/pkg/plugin/plugins.go index 38cde68bccc..9671f890195 100644 --- a/pkg/plugin/plugins.go +++ b/pkg/plugin/plugins.go @@ -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 } diff --git a/pkg/scene/import.go b/pkg/scene/import.go index b36e1bd68ab..c1b065bcf8a 100644 --- a/pkg/scene/import.go +++ b/pkg/scene/import.go @@ -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 { @@ -517,7 +518,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 { diff --git a/pkg/scene/merge.go b/pkg/scene/merge.go index e7c9ab2f70d..77b551ab27e 100644 --- a/pkg/scene/merge.go +++ b/pkg/scene/merge.go @@ -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") } diff --git a/pkg/scene/update_test.go b/pkg/scene/update_test.go index 3f0829b5958..c91f57d9f7d 100644 --- a/pkg/scene/update_test.go +++ b/pkg/scene/update_test.go @@ -4,6 +4,7 @@ import ( "errors" "strconv" "testing" + "time" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models/mocks" @@ -241,16 +242,19 @@ func TestUpdateSet_UpdateInput(t *testing.T) { tagIDStrs := intslice.IntSliceToStringSlice(tagIDs) stashID := getUUID("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, }, } diff --git a/pkg/scraper/stashbox/graphql/generated_client.go b/pkg/scraper/stashbox/graphql/generated_client.go index b87d70343fa..b6363dc564c 100644 --- a/pkg/scraper/stashbox/graphql/generated_client.go +++ b/pkg/scraper/stashbox/graphql/generated_client.go @@ -6,240 +6,778 @@ import ( "context" "net/http" - "github.com/Yamashou/gqlgenc/client" + "github.com/Yamashou/gqlgenc/clientv2" ) type StashBoxGraphQLClient interface { - FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) - FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) - FindScenesBySceneFingerprints(ctx context.Context, fingerprints [][]*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesBySceneFingerprints, error) - SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) - SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) - FindPerformerByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindPerformerByID, error) - FindSceneByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByID, error) - FindStudio(ctx context.Context, id *string, name *string, httpRequestOptions ...client.HTTPRequestOption) (*FindStudio, error) - SubmitFingerprint(ctx context.Context, input FingerprintSubmission, httpRequestOptions ...client.HTTPRequestOption) (*SubmitFingerprint, error) - Me(ctx context.Context, httpRequestOptions ...client.HTTPRequestOption) (*Me, error) - SubmitSceneDraft(ctx context.Context, input SceneDraftInput, httpRequestOptions ...client.HTTPRequestOption) (*SubmitSceneDraft, error) - SubmitPerformerDraft(ctx context.Context, input PerformerDraftInput, httpRequestOptions ...client.HTTPRequestOption) (*SubmitPerformerDraft, error) + FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, interceptors ...clientv2.RequestInterceptor) (*FindSceneByFingerprint, error) + FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, interceptors ...clientv2.RequestInterceptor) (*FindScenesByFullFingerprints, error) + FindScenesBySceneFingerprints(ctx context.Context, fingerprints [][]*FingerprintQueryInput, interceptors ...clientv2.RequestInterceptor) (*FindScenesBySceneFingerprints, error) + SearchScene(ctx context.Context, term string, interceptors ...clientv2.RequestInterceptor) (*SearchScene, error) + SearchPerformer(ctx context.Context, term string, interceptors ...clientv2.RequestInterceptor) (*SearchPerformer, error) + FindPerformerByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindPerformerByID, error) + FindSceneByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindSceneByID, error) + FindStudio(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindStudio, error) + SubmitFingerprint(ctx context.Context, input FingerprintSubmission, interceptors ...clientv2.RequestInterceptor) (*SubmitFingerprint, error) + Me(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*Me, error) + SubmitSceneDraft(ctx context.Context, input SceneDraftInput, interceptors ...clientv2.RequestInterceptor) (*SubmitSceneDraft, error) + SubmitPerformerDraft(ctx context.Context, input PerformerDraftInput, interceptors ...clientv2.RequestInterceptor) (*SubmitPerformerDraft, error) } type Client struct { - Client *client.Client -} - -func NewClient(cli *http.Client, baseURL string, options ...client.HTTPRequestOption) StashBoxGraphQLClient { - return &Client{Client: client.NewClient(cli, baseURL, options...)} -} - -type Query struct { - FindPerformer *Performer "json:\"findPerformer\" graphql:\"findPerformer\"" - QueryPerformers QueryPerformersResultType "json:\"queryPerformers\" graphql:\"queryPerformers\"" - FindStudio *Studio "json:\"findStudio\" graphql:\"findStudio\"" - QueryStudios QueryStudiosResultType "json:\"queryStudios\" graphql:\"queryStudios\"" - FindTag *Tag "json:\"findTag\" graphql:\"findTag\"" - QueryTags QueryTagsResultType "json:\"queryTags\" graphql:\"queryTags\"" - FindTagCategory *TagCategory "json:\"findTagCategory\" graphql:\"findTagCategory\"" - QueryTagCategories QueryTagCategoriesResultType "json:\"queryTagCategories\" graphql:\"queryTagCategories\"" - FindScene *Scene "json:\"findScene\" graphql:\"findScene\"" - FindSceneByFingerprint []*Scene "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" - FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" - FindScenesByFullFingerprints []*Scene "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" - FindScenesBySceneFingerprints [][]*Scene "json:\"findScenesBySceneFingerprints\" graphql:\"findScenesBySceneFingerprints\"" - QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\"" - FindSite *Site "json:\"findSite\" graphql:\"findSite\"" - QuerySites QuerySitesResultType "json:\"querySites\" graphql:\"querySites\"" - FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\"" - QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\"" - FindUser *User "json:\"findUser\" graphql:\"findUser\"" - QueryUsers QueryUsersResultType "json:\"queryUsers\" graphql:\"queryUsers\"" - Me *User "json:\"me\" graphql:\"me\"" - SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\"" - SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\"" - SearchTag []*Tag "json:\"searchTag\" graphql:\"searchTag\"" - FindDraft *Draft "json:\"findDraft\" graphql:\"findDraft\"" - FindDrafts []*Draft "json:\"findDrafts\" graphql:\"findDrafts\"" - QueryExistingScene QueryExistingSceneResult "json:\"queryExistingScene\" graphql:\"queryExistingScene\"" - Version Version "json:\"version\" graphql:\"version\"" - GetConfig StashBoxConfig "json:\"getConfig\" graphql:\"getConfig\"" -} -type Mutation struct { - SceneCreate *Scene "json:\"sceneCreate\" graphql:\"sceneCreate\"" - SceneUpdate *Scene "json:\"sceneUpdate\" graphql:\"sceneUpdate\"" - SceneDestroy bool "json:\"sceneDestroy\" graphql:\"sceneDestroy\"" - PerformerCreate *Performer "json:\"performerCreate\" graphql:\"performerCreate\"" - PerformerUpdate *Performer "json:\"performerUpdate\" graphql:\"performerUpdate\"" - PerformerDestroy bool "json:\"performerDestroy\" graphql:\"performerDestroy\"" - StudioCreate *Studio "json:\"studioCreate\" graphql:\"studioCreate\"" - StudioUpdate *Studio "json:\"studioUpdate\" graphql:\"studioUpdate\"" - StudioDestroy bool "json:\"studioDestroy\" graphql:\"studioDestroy\"" - TagCreate *Tag "json:\"tagCreate\" graphql:\"tagCreate\"" - TagUpdate *Tag "json:\"tagUpdate\" graphql:\"tagUpdate\"" - TagDestroy bool "json:\"tagDestroy\" graphql:\"tagDestroy\"" - UserCreate *User "json:\"userCreate\" graphql:\"userCreate\"" - UserUpdate *User "json:\"userUpdate\" graphql:\"userUpdate\"" - UserDestroy bool "json:\"userDestroy\" graphql:\"userDestroy\"" - ImageCreate *Image "json:\"imageCreate\" graphql:\"imageCreate\"" - ImageDestroy bool "json:\"imageDestroy\" graphql:\"imageDestroy\"" - NewUser *string "json:\"newUser\" graphql:\"newUser\"" - ActivateNewUser *User "json:\"activateNewUser\" graphql:\"activateNewUser\"" - GenerateInviteCode *string "json:\"generateInviteCode\" graphql:\"generateInviteCode\"" - RescindInviteCode bool "json:\"rescindInviteCode\" graphql:\"rescindInviteCode\"" - GrantInvite int "json:\"grantInvite\" graphql:\"grantInvite\"" - RevokeInvite int "json:\"revokeInvite\" graphql:\"revokeInvite\"" - TagCategoryCreate *TagCategory "json:\"tagCategoryCreate\" graphql:\"tagCategoryCreate\"" - TagCategoryUpdate *TagCategory "json:\"tagCategoryUpdate\" graphql:\"tagCategoryUpdate\"" - TagCategoryDestroy bool "json:\"tagCategoryDestroy\" graphql:\"tagCategoryDestroy\"" - SiteCreate *Site "json:\"siteCreate\" graphql:\"siteCreate\"" - SiteUpdate *Site "json:\"siteUpdate\" graphql:\"siteUpdate\"" - SiteDestroy bool "json:\"siteDestroy\" graphql:\"siteDestroy\"" - RegenerateAPIKey string "json:\"regenerateAPIKey\" graphql:\"regenerateAPIKey\"" - ResetPassword bool "json:\"resetPassword\" graphql:\"resetPassword\"" - ChangePassword bool "json:\"changePassword\" graphql:\"changePassword\"" - SceneEdit Edit "json:\"sceneEdit\" graphql:\"sceneEdit\"" - PerformerEdit Edit "json:\"performerEdit\" graphql:\"performerEdit\"" - StudioEdit Edit "json:\"studioEdit\" graphql:\"studioEdit\"" - TagEdit Edit "json:\"tagEdit\" graphql:\"tagEdit\"" - SceneEditUpdate Edit "json:\"sceneEditUpdate\" graphql:\"sceneEditUpdate\"" - PerformerEditUpdate Edit "json:\"performerEditUpdate\" graphql:\"performerEditUpdate\"" - StudioEditUpdate Edit "json:\"studioEditUpdate\" graphql:\"studioEditUpdate\"" - TagEditUpdate Edit "json:\"tagEditUpdate\" graphql:\"tagEditUpdate\"" - EditVote Edit "json:\"editVote\" graphql:\"editVote\"" - EditComment Edit "json:\"editComment\" graphql:\"editComment\"" - ApplyEdit Edit "json:\"applyEdit\" graphql:\"applyEdit\"" - CancelEdit Edit "json:\"cancelEdit\" graphql:\"cancelEdit\"" - SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" - SubmitSceneDraft DraftSubmissionStatus "json:\"submitSceneDraft\" graphql:\"submitSceneDraft\"" - SubmitPerformerDraft DraftSubmissionStatus "json:\"submitPerformerDraft\" graphql:\"submitPerformerDraft\"" - DestroyDraft bool "json:\"destroyDraft\" graphql:\"destroyDraft\"" - FavoritePerformer bool "json:\"favoritePerformer\" graphql:\"favoritePerformer\"" - FavoriteStudio bool "json:\"favoriteStudio\" graphql:\"favoriteStudio\"" + Client *clientv2.Client } + +func NewClient(cli *http.Client, baseURL string, options *clientv2.Options, interceptors ...clientv2.RequestInterceptor) StashBoxGraphQLClient { + return &Client{Client: clientv2.NewClient(cli, baseURL, options, interceptors...)} +} + type URLFragment struct { URL string "json:\"url\" graphql:\"url\"" Type string "json:\"type\" graphql:\"type\"" } + +func (t *URLFragment) GetURL() string { + if t == nil { + t = &URLFragment{} + } + return t.URL +} +func (t *URLFragment) GetType() string { + if t == nil { + t = &URLFragment{} + } + return t.Type +} + type ImageFragment struct { ID string "json:\"id\" graphql:\"id\"" URL string "json:\"url\" graphql:\"url\"" Width int "json:\"width\" graphql:\"width\"" Height int "json:\"height\" graphql:\"height\"" } + +func (t *ImageFragment) GetID() string { + if t == nil { + t = &ImageFragment{} + } + return t.ID +} +func (t *ImageFragment) GetURL() string { + if t == nil { + t = &ImageFragment{} + } + return t.URL +} +func (t *ImageFragment) GetWidth() int { + if t == nil { + t = &ImageFragment{} + } + return t.Width +} +func (t *ImageFragment) GetHeight() int { + if t == nil { + t = &ImageFragment{} + } + return t.Height +} + type StudioFragment struct { - Name string "json:\"name\" graphql:\"name\"" - ID string "json:\"id\" graphql:\"id\"" - Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" - Parent *struct { - Name string "json:\"name\" graphql:\"name\"" - ID string "json:\"id\" graphql:\"id\"" - } "json:\"parent\" graphql:\"parent\"" - Images []*ImageFragment "json:\"images\" graphql:\"images\"" + Name string "json:\"name\" graphql:\"name\"" + ID string "json:\"id\" graphql:\"id\"" + Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" + Parent *StudioFragment_Parent "json:\"parent,omitempty\" graphql:\"parent\"" + Images []*ImageFragment "json:\"images\" graphql:\"images\"" +} + +func (t *StudioFragment) GetName() string { + if t == nil { + t = &StudioFragment{} + } + return t.Name +} +func (t *StudioFragment) GetID() string { + if t == nil { + t = &StudioFragment{} + } + return t.ID +} +func (t *StudioFragment) GetUrls() []*URLFragment { + if t == nil { + t = &StudioFragment{} + } + return t.Urls +} +func (t *StudioFragment) GetParent() *StudioFragment_Parent { + if t == nil { + t = &StudioFragment{} + } + return t.Parent +} +func (t *StudioFragment) GetImages() []*ImageFragment { + if t == nil { + t = &StudioFragment{} + } + return t.Images } + type TagFragment struct { Name string "json:\"name\" graphql:\"name\"" ID string "json:\"id\" graphql:\"id\"" } -type FuzzyDateFragment struct { - Date string "json:\"date\" graphql:\"date\"" - Accuracy DateAccuracyEnum "json:\"accuracy\" graphql:\"accuracy\"" + +func (t *TagFragment) GetName() string { + if t == nil { + t = &TagFragment{} + } + return t.Name +} +func (t *TagFragment) GetID() string { + if t == nil { + t = &TagFragment{} + } + return t.ID } + type MeasurementsFragment struct { - BandSize *int "json:\"band_size\" graphql:\"band_size\"" - CupSize *string "json:\"cup_size\" graphql:\"cup_size\"" - Waist *int "json:\"waist\" graphql:\"waist\"" - Hip *int "json:\"hip\" graphql:\"hip\"" + BandSize *int "json:\"band_size,omitempty\" graphql:\"band_size\"" + CupSize *string "json:\"cup_size,omitempty\" graphql:\"cup_size\"" + Waist *int "json:\"waist,omitempty\" graphql:\"waist\"" + Hip *int "json:\"hip,omitempty\" graphql:\"hip\"" +} + +func (t *MeasurementsFragment) GetBandSize() *int { + if t == nil { + t = &MeasurementsFragment{} + } + return t.BandSize +} +func (t *MeasurementsFragment) GetCupSize() *string { + if t == nil { + t = &MeasurementsFragment{} + } + return t.CupSize } +func (t *MeasurementsFragment) GetWaist() *int { + if t == nil { + t = &MeasurementsFragment{} + } + return t.Waist +} +func (t *MeasurementsFragment) GetHip() *int { + if t == nil { + t = &MeasurementsFragment{} + } + return t.Hip +} + type BodyModificationFragment struct { Location string "json:\"location\" graphql:\"location\"" - Description *string "json:\"description\" graphql:\"description\"" + Description *string "json:\"description,omitempty\" graphql:\"description\"" } + +func (t *BodyModificationFragment) GetLocation() string { + if t == nil { + t = &BodyModificationFragment{} + } + return t.Location +} +func (t *BodyModificationFragment) GetDescription() *string { + if t == nil { + t = &BodyModificationFragment{} + } + return t.Description +} + type PerformerFragment struct { ID string "json:\"id\" graphql:\"id\"" Name string "json:\"name\" graphql:\"name\"" - Disambiguation *string "json:\"disambiguation\" graphql:\"disambiguation\"" + Disambiguation *string "json:\"disambiguation,omitempty\" graphql:\"disambiguation\"" Aliases []string "json:\"aliases\" graphql:\"aliases\"" - Gender *GenderEnum "json:\"gender\" graphql:\"gender\"" + Gender *GenderEnum "json:\"gender,omitempty\" graphql:\"gender\"" MergedIds []string "json:\"merged_ids\" graphql:\"merged_ids\"" Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" Images []*ImageFragment "json:\"images\" graphql:\"images\"" - Birthdate *FuzzyDateFragment "json:\"birthdate\" graphql:\"birthdate\"" - Ethnicity *EthnicityEnum "json:\"ethnicity\" graphql:\"ethnicity\"" - Country *string "json:\"country\" graphql:\"country\"" - EyeColor *EyeColorEnum "json:\"eye_color\" graphql:\"eye_color\"" - HairColor *HairColorEnum "json:\"hair_color\" graphql:\"hair_color\"" - Height *int "json:\"height\" graphql:\"height\"" - Measurements MeasurementsFragment "json:\"measurements\" graphql:\"measurements\"" - BreastType *BreastTypeEnum "json:\"breast_type\" graphql:\"breast_type\"" - CareerStartYear *int "json:\"career_start_year\" graphql:\"career_start_year\"" - CareerEndYear *int "json:\"career_end_year\" graphql:\"career_end_year\"" - Tattoos []*BodyModificationFragment "json:\"tattoos\" graphql:\"tattoos\"" - Piercings []*BodyModificationFragment "json:\"piercings\" graphql:\"piercings\"" + BirthDate *string "json:\"birth_date,omitempty\" graphql:\"birth_date\"" + Ethnicity *EthnicityEnum "json:\"ethnicity,omitempty\" graphql:\"ethnicity\"" + Country *string "json:\"country,omitempty\" graphql:\"country\"" + EyeColor *EyeColorEnum "json:\"eye_color,omitempty\" graphql:\"eye_color\"" + HairColor *HairColorEnum "json:\"hair_color,omitempty\" graphql:\"hair_color\"" + Height *int "json:\"height,omitempty\" graphql:\"height\"" + Measurements *MeasurementsFragment "json:\"measurements\" graphql:\"measurements\"" + BreastType *BreastTypeEnum "json:\"breast_type,omitempty\" graphql:\"breast_type\"" + CareerStartYear *int "json:\"career_start_year,omitempty\" graphql:\"career_start_year\"" + CareerEndYear *int "json:\"career_end_year,omitempty\" graphql:\"career_end_year\"" + Tattoos []*BodyModificationFragment "json:\"tattoos,omitempty\" graphql:\"tattoos\"" + Piercings []*BodyModificationFragment "json:\"piercings,omitempty\" graphql:\"piercings\"" +} + +func (t *PerformerFragment) GetID() string { + if t == nil { + t = &PerformerFragment{} + } + return t.ID +} +func (t *PerformerFragment) GetName() string { + if t == nil { + t = &PerformerFragment{} + } + return t.Name +} +func (t *PerformerFragment) GetDisambiguation() *string { + if t == nil { + t = &PerformerFragment{} + } + return t.Disambiguation +} +func (t *PerformerFragment) GetAliases() []string { + if t == nil { + t = &PerformerFragment{} + } + return t.Aliases +} +func (t *PerformerFragment) GetGender() *GenderEnum { + if t == nil { + t = &PerformerFragment{} + } + return t.Gender +} +func (t *PerformerFragment) GetMergedIds() []string { + if t == nil { + t = &PerformerFragment{} + } + return t.MergedIds +} +func (t *PerformerFragment) GetUrls() []*URLFragment { + if t == nil { + t = &PerformerFragment{} + } + return t.Urls +} +func (t *PerformerFragment) GetImages() []*ImageFragment { + if t == nil { + t = &PerformerFragment{} + } + return t.Images +} +func (t *PerformerFragment) GetBirthDate() *string { + if t == nil { + t = &PerformerFragment{} + } + return t.BirthDate +} +func (t *PerformerFragment) GetEthnicity() *EthnicityEnum { + if t == nil { + t = &PerformerFragment{} + } + return t.Ethnicity +} +func (t *PerformerFragment) GetCountry() *string { + if t == nil { + t = &PerformerFragment{} + } + return t.Country +} +func (t *PerformerFragment) GetEyeColor() *EyeColorEnum { + if t == nil { + t = &PerformerFragment{} + } + return t.EyeColor +} +func (t *PerformerFragment) GetHairColor() *HairColorEnum { + if t == nil { + t = &PerformerFragment{} + } + return t.HairColor +} +func (t *PerformerFragment) GetHeight() *int { + if t == nil { + t = &PerformerFragment{} + } + return t.Height } +func (t *PerformerFragment) GetMeasurements() *MeasurementsFragment { + if t == nil { + t = &PerformerFragment{} + } + return t.Measurements +} +func (t *PerformerFragment) GetBreastType() *BreastTypeEnum { + if t == nil { + t = &PerformerFragment{} + } + return t.BreastType +} +func (t *PerformerFragment) GetCareerStartYear() *int { + if t == nil { + t = &PerformerFragment{} + } + return t.CareerStartYear +} +func (t *PerformerFragment) GetCareerEndYear() *int { + if t == nil { + t = &PerformerFragment{} + } + return t.CareerEndYear +} +func (t *PerformerFragment) GetTattoos() []*BodyModificationFragment { + if t == nil { + t = &PerformerFragment{} + } + return t.Tattoos +} +func (t *PerformerFragment) GetPiercings() []*BodyModificationFragment { + if t == nil { + t = &PerformerFragment{} + } + return t.Piercings +} + type PerformerAppearanceFragment struct { - As *string "json:\"as\" graphql:\"as\"" - Performer PerformerFragment "json:\"performer\" graphql:\"performer\"" + As *string "json:\"as,omitempty\" graphql:\"as\"" + Performer *PerformerFragment "json:\"performer\" graphql:\"performer\"" } + +func (t *PerformerAppearanceFragment) GetAs() *string { + if t == nil { + t = &PerformerAppearanceFragment{} + } + return t.As +} +func (t *PerformerAppearanceFragment) GetPerformer() *PerformerFragment { + if t == nil { + t = &PerformerAppearanceFragment{} + } + return t.Performer +} + type FingerprintFragment struct { Algorithm FingerprintAlgorithm "json:\"algorithm\" graphql:\"algorithm\"" Hash string "json:\"hash\" graphql:\"hash\"" Duration int "json:\"duration\" graphql:\"duration\"" } + +func (t *FingerprintFragment) GetAlgorithm() *FingerprintAlgorithm { + if t == nil { + t = &FingerprintFragment{} + } + return &t.Algorithm +} +func (t *FingerprintFragment) GetHash() string { + if t == nil { + t = &FingerprintFragment{} + } + return t.Hash +} +func (t *FingerprintFragment) GetDuration() int { + if t == nil { + t = &FingerprintFragment{} + } + return t.Duration +} + type SceneFragment struct { ID string "json:\"id\" graphql:\"id\"" - Title *string "json:\"title\" graphql:\"title\"" - Code *string "json:\"code\" graphql:\"code\"" - Details *string "json:\"details\" graphql:\"details\"" - Director *string "json:\"director\" graphql:\"director\"" - Duration *int "json:\"duration\" graphql:\"duration\"" - Date *string "json:\"date\" graphql:\"date\"" + Title *string "json:\"title,omitempty\" graphql:\"title\"" + Code *string "json:\"code,omitempty\" graphql:\"code\"" + Details *string "json:\"details,omitempty\" graphql:\"details\"" + Director *string "json:\"director,omitempty\" graphql:\"director\"" + Duration *int "json:\"duration,omitempty\" graphql:\"duration\"" + Date *string "json:\"date,omitempty\" graphql:\"date\"" Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" Images []*ImageFragment "json:\"images\" graphql:\"images\"" - Studio *StudioFragment "json:\"studio\" graphql:\"studio\"" + Studio *StudioFragment "json:\"studio,omitempty\" graphql:\"studio\"" Tags []*TagFragment "json:\"tags\" graphql:\"tags\"" Performers []*PerformerAppearanceFragment "json:\"performers\" graphql:\"performers\"" Fingerprints []*FingerprintFragment "json:\"fingerprints\" graphql:\"fingerprints\"" } + +func (t *SceneFragment) GetID() string { + if t == nil { + t = &SceneFragment{} + } + return t.ID +} +func (t *SceneFragment) GetTitle() *string { + if t == nil { + t = &SceneFragment{} + } + return t.Title +} +func (t *SceneFragment) GetCode() *string { + if t == nil { + t = &SceneFragment{} + } + return t.Code +} +func (t *SceneFragment) GetDetails() *string { + if t == nil { + t = &SceneFragment{} + } + return t.Details +} +func (t *SceneFragment) GetDirector() *string { + if t == nil { + t = &SceneFragment{} + } + return t.Director +} +func (t *SceneFragment) GetDuration() *int { + if t == nil { + t = &SceneFragment{} + } + return t.Duration +} +func (t *SceneFragment) GetDate() *string { + if t == nil { + t = &SceneFragment{} + } + return t.Date +} +func (t *SceneFragment) GetUrls() []*URLFragment { + if t == nil { + t = &SceneFragment{} + } + return t.Urls +} +func (t *SceneFragment) GetImages() []*ImageFragment { + if t == nil { + t = &SceneFragment{} + } + return t.Images +} +func (t *SceneFragment) GetStudio() *StudioFragment { + if t == nil { + t = &SceneFragment{} + } + return t.Studio +} +func (t *SceneFragment) GetTags() []*TagFragment { + if t == nil { + t = &SceneFragment{} + } + return t.Tags +} +func (t *SceneFragment) GetPerformers() []*PerformerAppearanceFragment { + if t == nil { + t = &SceneFragment{} + } + return t.Performers +} +func (t *SceneFragment) GetFingerprints() []*FingerprintFragment { + if t == nil { + t = &SceneFragment{} + } + return t.Fingerprints +} + +type StudioFragment_Parent struct { + Name string "json:\"name\" graphql:\"name\"" + ID string "json:\"id\" graphql:\"id\"" +} + +func (t *StudioFragment_Parent) GetName() string { + if t == nil { + t = &StudioFragment_Parent{} + } + return t.Name +} +func (t *StudioFragment_Parent) GetID() string { + if t == nil { + t = &StudioFragment_Parent{} + } + return t.ID +} + +type SceneFragment_Studio_StudioFragment_Parent struct { + Name string "json:\"name\" graphql:\"name\"" + ID string "json:\"id\" graphql:\"id\"" +} + +func (t *SceneFragment_Studio_StudioFragment_Parent) GetName() string { + if t == nil { + t = &SceneFragment_Studio_StudioFragment_Parent{} + } + return t.Name +} +func (t *SceneFragment_Studio_StudioFragment_Parent) GetID() string { + if t == nil { + t = &SceneFragment_Studio_StudioFragment_Parent{} + } + return t.ID +} + +type FindSceneByFingerprint_FindSceneByFingerprint_SceneFragment_Studio_StudioFragment_Parent struct { + Name string "json:\"name\" graphql:\"name\"" + ID string "json:\"id\" graphql:\"id\"" +} + +func (t *FindSceneByFingerprint_FindSceneByFingerprint_SceneFragment_Studio_StudioFragment_Parent) GetName() string { + if t == nil { + t = &FindSceneByFingerprint_FindSceneByFingerprint_SceneFragment_Studio_StudioFragment_Parent{} + } + return t.Name +} +func (t *FindSceneByFingerprint_FindSceneByFingerprint_SceneFragment_Studio_StudioFragment_Parent) GetID() string { + if t == nil { + t = &FindSceneByFingerprint_FindSceneByFingerprint_SceneFragment_Studio_StudioFragment_Parent{} + } + return t.ID +} + +type FindScenesByFullFingerprints_FindScenesByFullFingerprints_SceneFragment_Studio_StudioFragment_Parent struct { + Name string "json:\"name\" graphql:\"name\"" + ID string "json:\"id\" graphql:\"id\"" +} + +func (t *FindScenesByFullFingerprints_FindScenesByFullFingerprints_SceneFragment_Studio_StudioFragment_Parent) GetName() string { + if t == nil { + t = &FindScenesByFullFingerprints_FindScenesByFullFingerprints_SceneFragment_Studio_StudioFragment_Parent{} + } + return t.Name +} +func (t *FindScenesByFullFingerprints_FindScenesByFullFingerprints_SceneFragment_Studio_StudioFragment_Parent) GetID() string { + if t == nil { + t = &FindScenesByFullFingerprints_FindScenesByFullFingerprints_SceneFragment_Studio_StudioFragment_Parent{} + } + return t.ID +} + +type FindScenesBySceneFingerprints_FindScenesBySceneFingerprints_SceneFragment_Studio_StudioFragment_Parent struct { + Name string "json:\"name\" graphql:\"name\"" + ID string "json:\"id\" graphql:\"id\"" +} + +func (t *FindScenesBySceneFingerprints_FindScenesBySceneFingerprints_SceneFragment_Studio_StudioFragment_Parent) GetName() string { + if t == nil { + t = &FindScenesBySceneFingerprints_FindScenesBySceneFingerprints_SceneFragment_Studio_StudioFragment_Parent{} + } + return t.Name +} +func (t *FindScenesBySceneFingerprints_FindScenesBySceneFingerprints_SceneFragment_Studio_StudioFragment_Parent) GetID() string { + if t == nil { + t = &FindScenesBySceneFingerprints_FindScenesBySceneFingerprints_SceneFragment_Studio_StudioFragment_Parent{} + } + return t.ID +} + +type SearchScene_SearchScene_SceneFragment_Studio_StudioFragment_Parent struct { + Name string "json:\"name\" graphql:\"name\"" + ID string "json:\"id\" graphql:\"id\"" +} + +func (t *SearchScene_SearchScene_SceneFragment_Studio_StudioFragment_Parent) GetName() string { + if t == nil { + t = &SearchScene_SearchScene_SceneFragment_Studio_StudioFragment_Parent{} + } + return t.Name +} +func (t *SearchScene_SearchScene_SceneFragment_Studio_StudioFragment_Parent) GetID() string { + if t == nil { + t = &SearchScene_SearchScene_SceneFragment_Studio_StudioFragment_Parent{} + } + return t.ID +} + +type FindSceneByID_FindScene_SceneFragment_Studio_StudioFragment_Parent struct { + Name string "json:\"name\" graphql:\"name\"" + ID string "json:\"id\" graphql:\"id\"" +} + +func (t *FindSceneByID_FindScene_SceneFragment_Studio_StudioFragment_Parent) GetName() string { + if t == nil { + t = &FindSceneByID_FindScene_SceneFragment_Studio_StudioFragment_Parent{} + } + return t.Name +} +func (t *FindSceneByID_FindScene_SceneFragment_Studio_StudioFragment_Parent) GetID() string { + if t == nil { + t = &FindSceneByID_FindScene_SceneFragment_Studio_StudioFragment_Parent{} + } + return t.ID +} + +type FindStudio_FindStudio_StudioFragment_Parent struct { + Name string "json:\"name\" graphql:\"name\"" + ID string "json:\"id\" graphql:\"id\"" +} + +func (t *FindStudio_FindStudio_StudioFragment_Parent) GetName() string { + if t == nil { + t = &FindStudio_FindStudio_StudioFragment_Parent{} + } + return t.Name +} +func (t *FindStudio_FindStudio_StudioFragment_Parent) GetID() string { + if t == nil { + t = &FindStudio_FindStudio_StudioFragment_Parent{} + } + return t.ID +} + +type Me_Me struct { + Name string "json:\"name\" graphql:\"name\"" +} + +func (t *Me_Me) GetName() string { + if t == nil { + t = &Me_Me{} + } + return t.Name +} + +type SubmitSceneDraft_SubmitSceneDraft struct { + ID *string "json:\"id,omitempty\" graphql:\"id\"" +} + +func (t *SubmitSceneDraft_SubmitSceneDraft) GetID() *string { + if t == nil { + t = &SubmitSceneDraft_SubmitSceneDraft{} + } + return t.ID +} + +type SubmitPerformerDraft_SubmitPerformerDraft struct { + ID *string "json:\"id,omitempty\" graphql:\"id\"" +} + +func (t *SubmitPerformerDraft_SubmitPerformerDraft) GetID() *string { + if t == nil { + t = &SubmitPerformerDraft_SubmitPerformerDraft{} + } + return t.ID +} + type FindSceneByFingerprint struct { FindSceneByFingerprint []*SceneFragment "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" } + +func (t *FindSceneByFingerprint) GetFindSceneByFingerprint() []*SceneFragment { + if t == nil { + t = &FindSceneByFingerprint{} + } + return t.FindSceneByFingerprint +} + type FindScenesByFullFingerprints struct { FindScenesByFullFingerprints []*SceneFragment "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" } + +func (t *FindScenesByFullFingerprints) GetFindScenesByFullFingerprints() []*SceneFragment { + if t == nil { + t = &FindScenesByFullFingerprints{} + } + return t.FindScenesByFullFingerprints +} + type FindScenesBySceneFingerprints struct { FindScenesBySceneFingerprints [][]*SceneFragment "json:\"findScenesBySceneFingerprints\" graphql:\"findScenesBySceneFingerprints\"" } + +func (t *FindScenesBySceneFingerprints) GetFindScenesBySceneFingerprints() [][]*SceneFragment { + if t == nil { + t = &FindScenesBySceneFingerprints{} + } + return t.FindScenesBySceneFingerprints +} + type SearchScene struct { SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\"" } + +func (t *SearchScene) GetSearchScene() []*SceneFragment { + if t == nil { + t = &SearchScene{} + } + return t.SearchScene +} + type SearchPerformer struct { SearchPerformer []*PerformerFragment "json:\"searchPerformer\" graphql:\"searchPerformer\"" } + +func (t *SearchPerformer) GetSearchPerformer() []*PerformerFragment { + if t == nil { + t = &SearchPerformer{} + } + return t.SearchPerformer +} + type FindPerformerByID struct { - FindPerformer *PerformerFragment "json:\"findPerformer\" graphql:\"findPerformer\"" + FindPerformer *PerformerFragment "json:\"findPerformer,omitempty\" graphql:\"findPerformer\"" } + +func (t *FindPerformerByID) GetFindPerformer() *PerformerFragment { + if t == nil { + t = &FindPerformerByID{} + } + return t.FindPerformer +} + type FindSceneByID struct { - FindScene *SceneFragment "json:\"findScene\" graphql:\"findScene\"" + FindScene *SceneFragment "json:\"findScene,omitempty\" graphql:\"findScene\"" +} + +func (t *FindSceneByID) GetFindScene() *SceneFragment { + if t == nil { + t = &FindSceneByID{} + } + return t.FindScene } + type FindStudio struct { - FindStudio *StudioFragment "json:\"findStudio\" graphql:\"findStudio\"" + FindStudio *StudioFragment "json:\"findStudio,omitempty\" graphql:\"findStudio\"" } + +func (t *FindStudio) GetFindStudio() *StudioFragment { + if t == nil { + t = &FindStudio{} + } + return t.FindStudio +} + type SubmitFingerprint struct { SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" } + +func (t *SubmitFingerprint) GetSubmitFingerprint() bool { + if t == nil { + t = &SubmitFingerprint{} + } + return t.SubmitFingerprint +} + type Me struct { - Me *struct { - Name string "json:\"name\" graphql:\"name\"" - } "json:\"me\" graphql:\"me\"" + Me *Me_Me "json:\"me,omitempty\" graphql:\"me\"" +} + +func (t *Me) GetMe() *Me_Me { + if t == nil { + t = &Me{} + } + return t.Me } + type SubmitSceneDraft struct { - SubmitSceneDraft struct { - ID *string "json:\"id\" graphql:\"id\"" - } "json:\"submitSceneDraft\" graphql:\"submitSceneDraft\"" + SubmitSceneDraft SubmitSceneDraft_SubmitSceneDraft "json:\"submitSceneDraft\" graphql:\"submitSceneDraft\"" +} + +func (t *SubmitSceneDraft) GetSubmitSceneDraft() *SubmitSceneDraft_SubmitSceneDraft { + if t == nil { + t = &SubmitSceneDraft{} + } + return &t.SubmitSceneDraft } + type SubmitPerformerDraft struct { - SubmitPerformerDraft struct { - ID *string "json:\"id\" graphql:\"id\"" - } "json:\"submitPerformerDraft\" graphql:\"submitPerformerDraft\"" + SubmitPerformerDraft SubmitPerformerDraft_SubmitPerformerDraft "json:\"submitPerformerDraft\" graphql:\"submitPerformerDraft\"" +} + +func (t *SubmitPerformerDraft) GetSubmitPerformerDraft() *SubmitPerformerDraft_SubmitPerformerDraft { + if t == nil { + t = &SubmitPerformerDraft{} + } + return &t.SubmitPerformerDraft } const FindSceneByFingerprintDocument = `query FindSceneByFingerprint ($fingerprint: FingerprintQueryInput!) { @@ -247,11 +785,6 @@ const FindSceneByFingerprintDocument = `query FindSceneByFingerprint ($fingerpri ... SceneFragment } } -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} fragment SceneFragment on Scene { id title @@ -279,28 +812,16 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } +fragment URLFragment on URL { + url + type +} fragment ImageFragment on Image { id url width height } -fragment TagFragment on Tag { - name - id -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment URLFragment on URL { - url - type -} fragment StudioFragment on Studio { name id @@ -315,6 +836,10 @@ fragment StudioFragment on Studio { ... ImageFragment } } +fragment TagFragment on Tag { + name + id +} fragment PerformerAppearanceFragment on PerformerAppearance { as performer { @@ -334,9 +859,7 @@ fragment PerformerFragment on Performer { images { ... ImageFragment } - birthdate { - ... FuzzyDateFragment - } + birth_date ethnicity country eye_color @@ -361,15 +884,28 @@ fragment MeasurementsFragment on Measurements { waist hip } +fragment BodyModificationFragment on BodyModification { + location + description +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} ` -func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) { - vars := map[string]interface{}{ +func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, interceptors ...clientv2.RequestInterceptor) (*FindSceneByFingerprint, error) { + vars := map[string]any{ "fingerprint": fingerprint, } var res FindSceneByFingerprint - if err := c.Client.Post(ctx, "FindSceneByFingerprint", FindSceneByFingerprintDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "FindSceneByFingerprint", FindSceneByFingerprintDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } @@ -381,16 +917,36 @@ const FindScenesByFullFingerprintsDocument = `query FindScenesByFullFingerprints ... SceneFragment } } -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash +fragment SceneFragment on Scene { + id + title + code + details + director duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} +fragment URLFragment on URL { + url + type } fragment ImageFragment on Image { id @@ -398,6 +954,20 @@ fragment ImageFragment on Image { width height } +fragment StudioFragment on Studio { + name + id + urls { + ... URLFragment + } + parent { + name + id + } + images { + ... ImageFragment + } +} fragment TagFragment on Tag { name id @@ -421,9 +991,7 @@ fragment PerformerFragment on Performer { images { ... ImageFragment } - birthdate { - ... FuzzyDateFragment - } + birth_date ethnicity country eye_color @@ -442,14 +1010,45 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip } fragment BodyModificationFragment on BodyModification { location description } +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} +` + +func (c *Client) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, interceptors ...clientv2.RequestInterceptor) (*FindScenesByFullFingerprints, error) { + vars := map[string]any{ + "fingerprints": fingerprints, + } + + var res FindScenesByFullFingerprints + if err := c.Client.Post(ctx, "FindScenesByFullFingerprints", FindScenesByFullFingerprintsDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + + return nil, err + } + + return &res, nil +} + +const FindScenesBySceneFingerprintsDocument = `query FindScenesBySceneFingerprints ($fingerprints: [[FingerprintQueryInput!]!]!) { + findScenesBySceneFingerprints(fingerprints: $fingerprints) { + ... SceneFragment + } +} fragment SceneFragment on Scene { id title @@ -471,85 +1070,22 @@ fragment SceneFragment on Scene { ... TagFragment } performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } -} -fragment URLFragment on URL { - url - type -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - parent { - name - id - } - images { - ... ImageFragment - } -} -` - -func (c *Client) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) { - vars := map[string]interface{}{ - "fingerprints": fingerprints, - } - - var res FindScenesByFullFingerprints - if err := c.Client.Post(ctx, "FindScenesByFullFingerprints", FindScenesByFullFingerprintsDocument, &res, vars, httpRequestOptions...); err != nil { - return nil, err - } - - return &res, nil -} - -const FindScenesBySceneFingerprintsDocument = `query FindScenesBySceneFingerprints ($fingerprints: [[FingerprintQueryInput!]!]!) { - findScenesBySceneFingerprints(fingerprints: $fingerprints) { - ... SceneFragment + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment } } +fragment URLFragment on URL { + url + type +} fragment ImageFragment on Image { id url width height } -fragment TagFragment on Tag { - name - id -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} -fragment URLFragment on URL { - url - type -} fragment StudioFragment on Studio { name id @@ -564,6 +1100,16 @@ fragment StudioFragment on Studio { ... ImageFragment } } +fragment TagFragment on Tag { + name + id +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} fragment PerformerFragment on Performer { id name @@ -577,9 +1123,7 @@ fragment PerformerFragment on Performer { images { ... ImageFragment } - birthdate { - ... FuzzyDateFragment - } + birth_date ethnicity country eye_color @@ -598,46 +1142,34 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip } -fragment SceneFragment on Scene { - id - title - code - details - director +fragment BodyModificationFragment on BodyModification { + location + description +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } } ` -func (c *Client) FindScenesBySceneFingerprints(ctx context.Context, fingerprints [][]*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesBySceneFingerprints, error) { - vars := map[string]interface{}{ +func (c *Client) FindScenesBySceneFingerprints(ctx context.Context, fingerprints [][]*FingerprintQueryInput, interceptors ...clientv2.RequestInterceptor) (*FindScenesBySceneFingerprints, error) { + vars := map[string]any{ "fingerprints": fingerprints, } var res FindScenesBySceneFingerprints - if err := c.Client.Post(ctx, "FindScenesBySceneFingerprints", FindScenesBySceneFingerprintsDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "FindScenesBySceneFingerprints", FindScenesBySceneFingerprintsDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } @@ -676,27 +1208,6 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} fragment URLFragment on URL { url type @@ -725,6 +1236,12 @@ fragment TagFragment on Tag { name id } +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} fragment PerformerFragment on Performer { id name @@ -738,9 +1255,7 @@ fragment PerformerFragment on Performer { images { ... ImageFragment } - birthdate { - ... FuzzyDateFragment - } + birth_date ethnicity country eye_color @@ -759,19 +1274,34 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} fragment BodyModificationFragment on BodyModification { location description } +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} ` -func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) { - vars := map[string]interface{}{ +func (c *Client) SearchScene(ctx context.Context, term string, interceptors ...clientv2.RequestInterceptor) (*SearchScene, error) { + vars := map[string]any{ "term": term, } var res SearchScene - if err := c.Client.Post(ctx, "SearchScene", SearchSceneDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "SearchScene", SearchSceneDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } @@ -796,9 +1326,7 @@ fragment PerformerFragment on Performer { images { ... ImageFragment } - birthdate { - ... FuzzyDateFragment - } + birth_date ethnicity country eye_color @@ -827,10 +1355,6 @@ fragment ImageFragment on Image { width height } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} fragment MeasurementsFragment on Measurements { band_size cup_size @@ -843,13 +1367,17 @@ fragment BodyModificationFragment on BodyModification { } ` -func (c *Client) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) { - vars := map[string]interface{}{ +func (c *Client) SearchPerformer(ctx context.Context, term string, interceptors ...clientv2.RequestInterceptor) (*SearchPerformer, error) { + vars := map[string]any{ "term": term, } var res SearchPerformer - if err := c.Client.Post(ctx, "SearchPerformer", SearchPerformerDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "SearchPerformer", SearchPerformerDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } @@ -861,26 +1389,6 @@ const FindPerformerByIDDocument = `query FindPerformerByID ($id: ID!) { ... PerformerFragment } } -fragment ImageFragment on Image { - id - url - width - height -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} fragment PerformerFragment on Performer { id name @@ -894,9 +1402,7 @@ fragment PerformerFragment on Performer { images { ... ImageFragment } - birthdate { - ... FuzzyDateFragment - } + birth_date ethnicity country eye_color @@ -919,15 +1425,35 @@ fragment URLFragment on URL { url type } +fragment ImageFragment on Image { + id + url + width + height +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment BodyModificationFragment on BodyModification { + location + description +} ` -func (c *Client) FindPerformerByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindPerformerByID, error) { - vars := map[string]interface{}{ +func (c *Client) FindPerformerByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindPerformerByID, error) { + vars := map[string]any{ "id": id, } var res FindPerformerByID - if err := c.Client.Post(ctx, "FindPerformerByID", FindPerformerByIDDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "FindPerformerByID", FindPerformerByIDDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } @@ -939,6 +1465,43 @@ const FindSceneByIDDocument = `query FindSceneByID ($id: ID!) { ... SceneFragment } } +fragment SceneFragment on Scene { + id + title + code + details + director + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} fragment StudioFragment on Studio { name id @@ -963,25 +1526,6 @@ fragment PerformerAppearanceFragment on PerformerAppearance { ... PerformerFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} -fragment ImageFragment on Image { - id - url - width - height -} -fragment URLFragment on URL { - url - type -} fragment PerformerFragment on Performer { id name @@ -995,9 +1539,7 @@ fragment PerformerFragment on Performer { images { ... ImageFragment } - birthdate { - ... FuzzyDateFragment - } + birth_date ethnicity country eye_color @@ -1026,42 +1568,24 @@ fragment BodyModificationFragment on BodyModification { location description } -fragment SceneFragment on Scene { - id - title - code - details - director +fragment FingerprintFragment on Fingerprint { + algorithm + hash duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } } ` -func (c *Client) FindSceneByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByID, error) { - vars := map[string]interface{}{ +func (c *Client) FindSceneByID(ctx context.Context, id string, interceptors ...clientv2.RequestInterceptor) (*FindSceneByID, error) { + vars := map[string]any{ "id": id, } var res FindSceneByID - if err := c.Client.Post(ctx, "FindSceneByID", FindSceneByIDDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "FindSceneByID", FindSceneByIDDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } @@ -1099,14 +1623,18 @@ fragment ImageFragment on Image { } ` -func (c *Client) FindStudio(ctx context.Context, id *string, name *string, httpRequestOptions ...client.HTTPRequestOption) (*FindStudio, error) { - vars := map[string]interface{}{ +func (c *Client) FindStudio(ctx context.Context, id *string, name *string, interceptors ...clientv2.RequestInterceptor) (*FindStudio, error) { + vars := map[string]any{ "id": id, "name": name, } var res FindStudio - if err := c.Client.Post(ctx, "FindStudio", FindStudioDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "FindStudio", FindStudioDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } @@ -1118,13 +1646,17 @@ const SubmitFingerprintDocument = `mutation SubmitFingerprint ($input: Fingerpri } ` -func (c *Client) SubmitFingerprint(ctx context.Context, input FingerprintSubmission, httpRequestOptions ...client.HTTPRequestOption) (*SubmitFingerprint, error) { - vars := map[string]interface{}{ +func (c *Client) SubmitFingerprint(ctx context.Context, input FingerprintSubmission, interceptors ...clientv2.RequestInterceptor) (*SubmitFingerprint, error) { + vars := map[string]any{ "input": input, } var res SubmitFingerprint - if err := c.Client.Post(ctx, "SubmitFingerprint", SubmitFingerprintDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "SubmitFingerprint", SubmitFingerprintDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } @@ -1138,11 +1670,15 @@ const MeDocument = `query Me { } ` -func (c *Client) Me(ctx context.Context, httpRequestOptions ...client.HTTPRequestOption) (*Me, error) { - vars := map[string]interface{}{} +func (c *Client) Me(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*Me, error) { + vars := map[string]any{} var res Me - if err := c.Client.Post(ctx, "Me", MeDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "Me", MeDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } @@ -1156,13 +1692,17 @@ const SubmitSceneDraftDocument = `mutation SubmitSceneDraft ($input: SceneDraftI } ` -func (c *Client) SubmitSceneDraft(ctx context.Context, input SceneDraftInput, httpRequestOptions ...client.HTTPRequestOption) (*SubmitSceneDraft, error) { - vars := map[string]interface{}{ +func (c *Client) SubmitSceneDraft(ctx context.Context, input SceneDraftInput, interceptors ...clientv2.RequestInterceptor) (*SubmitSceneDraft, error) { + vars := map[string]any{ "input": input, } var res SubmitSceneDraft - if err := c.Client.Post(ctx, "SubmitSceneDraft", SubmitSceneDraftDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "SubmitSceneDraft", SubmitSceneDraftDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } @@ -1176,15 +1716,34 @@ const SubmitPerformerDraftDocument = `mutation SubmitPerformerDraft ($input: Per } ` -func (c *Client) SubmitPerformerDraft(ctx context.Context, input PerformerDraftInput, httpRequestOptions ...client.HTTPRequestOption) (*SubmitPerformerDraft, error) { - vars := map[string]interface{}{ +func (c *Client) SubmitPerformerDraft(ctx context.Context, input PerformerDraftInput, interceptors ...clientv2.RequestInterceptor) (*SubmitPerformerDraft, error) { + vars := map[string]any{ "input": input, } var res SubmitPerformerDraft - if err := c.Client.Post(ctx, "SubmitPerformerDraft", SubmitPerformerDraftDocument, &res, vars, httpRequestOptions...); err != nil { + if err := c.Client.Post(ctx, "SubmitPerformerDraft", SubmitPerformerDraftDocument, &res, vars, interceptors...); err != nil { + if c.Client.ParseDataWhenErrors { + return &res, err + } + return nil, err } return &res, nil } + +var DocumentOperationNames = map[string]string{ + FindSceneByFingerprintDocument: "FindSceneByFingerprint", + FindScenesByFullFingerprintsDocument: "FindScenesByFullFingerprints", + FindScenesBySceneFingerprintsDocument: "FindScenesBySceneFingerprints", + SearchSceneDocument: "SearchScene", + SearchPerformerDocument: "SearchPerformer", + FindPerformerByIDDocument: "FindPerformerByID", + FindSceneByIDDocument: "FindSceneByID", + FindStudioDocument: "FindStudio", + SubmitFingerprintDocument: "SubmitFingerprint", + MeDocument: "Me", + SubmitSceneDraftDocument: "SubmitSceneDraft", + SubmitPerformerDraftDocument: "SubmitPerformerDraft", +} diff --git a/pkg/scraper/stashbox/graphql/generated_models.go b/pkg/scraper/stashbox/graphql/generated_models.go index 87f99db47a9..39022f6eaf7 100644 --- a/pkg/scraper/stashbox/graphql/generated_models.go +++ b/pkg/scraper/stashbox/graphql/generated_models.go @@ -80,7 +80,7 @@ type Draft struct { ID string `json:"id"` Created time.Time `json:"created"` Expires time.Time `json:"expires"` - Data DraftData `json:"data,omitempty"` + Data DraftData `json:"data"` } type DraftEntity struct { @@ -88,10 +88,12 @@ type DraftEntity struct { ID *string `json:"id,omitempty"` } -func (DraftEntity) IsSceneDraftStudio() {} -func (DraftEntity) IsSceneDraftTag() {} func (DraftEntity) IsSceneDraftPerformer() {} +func (DraftEntity) IsSceneDraftStudio() {} + +func (DraftEntity) IsSceneDraftTag() {} + type DraftEntityInput struct { Name string `json:"name"` ID *string `json:"id,omitempty"` @@ -114,7 +116,7 @@ type Edit struct { Target EditTarget `json:"target,omitempty"` TargetType TargetTypeEnum `json:"target_type"` // Objects to merge with the target. Only applicable to merges - MergeSources []EditTarget `json:"merge_sources,omitempty"` + MergeSources []EditTarget `json:"merge_sources"` Operation OperationEnum `json:"operation"` Bot bool `json:"bot"` Details EditDetails `json:"details,omitempty"` @@ -122,8 +124,8 @@ type Edit struct { OldDetails EditDetails `json:"old_details,omitempty"` // Entity specific options Options *PerformerEditOptions `json:"options,omitempty"` - Comments []*EditComment `json:"comments,omitempty"` - Votes []*EditVote `json:"votes,omitempty"` + Comments []*EditComment `json:"comments"` + Votes []*EditVote `json:"votes"` // = Accepted - Rejected VoteCount int `json:"vote_count"` // Is the edit considered destructive. @@ -179,11 +181,13 @@ type EditQueryInput struct { // Filter by user voted status Voted *UserVotedFilterEnum `json:"voted,omitempty"` // Filter to bot edits only - IsBot *bool `json:"is_bot,omitempty"` - Page int `json:"page"` - PerPage int `json:"per_page"` - Direction SortDirectionEnum `json:"direction"` - Sort EditSortEnum `json:"sort"` + IsBot *bool `json:"is_bot,omitempty"` + // Filter out user's own edits + IncludeUserSubmitted *bool `json:"include_user_submitted,omitempty"` + Page int `json:"page"` + PerPage int `json:"per_page"` + Direction SortDirectionEnum `json:"direction"` + Sort EditSortEnum `json:"sort"` } type EditVote struct { @@ -237,7 +241,7 @@ type FingerprintQueryInput struct { type FingerprintSubmission struct { SceneID string `json:"scene_id"` - Fingerprint *FingerprintInput `json:"fingerprint,omitempty"` + Fingerprint *FingerprintInput `json:"fingerprint"` Unmatch *bool `json:"unmatch,omitempty"` } @@ -246,6 +250,12 @@ type FuzzyDate struct { Accuracy DateAccuracyEnum `json:"accuracy"` } +type GenerateInviteCodeInput struct { + Keys *int `json:"keys,omitempty"` + Uses *int `json:"uses,omitempty"` + TTL *int `json:"ttl,omitempty"` +} + type GrantInviteInput struct { UserID string `json:"user_id"` Amount int `json:"amount"` @@ -257,7 +267,7 @@ type HairColorCriterionInput struct { } type IDCriterionInput struct { - Value []string `json:"value,omitempty"` + Value []string `json:"value"` Modifier CriterionModifier `json:"modifier"` } @@ -287,6 +297,12 @@ type IntCriterionInput struct { Modifier CriterionModifier `json:"modifier"` } +type InviteKey struct { + ID string `json:"id"` + Uses *int `json:"uses,omitempty"` + Expires *time.Time `json:"expires,omitempty"` +} + type Measurements struct { CupSize *string `json:"cup_size,omitempty"` BandSize *int `json:"band_size,omitempty"` @@ -300,10 +316,13 @@ type MultiIDCriterionInput struct { } type MultiStringCriterionInput struct { - Value []string `json:"value,omitempty"` + Value []string `json:"value"` Modifier CriterionModifier `json:"modifier"` } +type Mutation struct { +} + type NewUserInput struct { Email string `json:"email"` InviteKey *string `json:"invite_key,omitempty"` @@ -313,9 +332,9 @@ type Performer struct { ID string `json:"id"` Name string `json:"name"` Disambiguation *string `json:"disambiguation,omitempty"` - Aliases []string `json:"aliases,omitempty"` + Aliases []string `json:"aliases"` Gender *GenderEnum `json:"gender,omitempty"` - Urls []*URL `json:"urls,omitempty"` + Urls []*URL `json:"urls"` Birthdate *FuzzyDate `json:"birthdate,omitempty"` BirthDate *string `json:"birth_date,omitempty"` Age *int `json:"age,omitempty"` @@ -325,7 +344,7 @@ type Performer struct { HairColor *HairColorEnum `json:"hair_color,omitempty"` // Height in cm Height *int `json:"height,omitempty"` - Measurements *Measurements `json:"measurements,omitempty"` + Measurements *Measurements `json:"measurements"` CupSize *string `json:"cup_size,omitempty"` BandSize *int `json:"band_size,omitempty"` WaistSize *int `json:"waist_size,omitempty"` @@ -335,23 +354,24 @@ type Performer struct { CareerEndYear *int `json:"career_end_year,omitempty"` Tattoos []*BodyModification `json:"tattoos,omitempty"` Piercings []*BodyModification `json:"piercings,omitempty"` - Images []*Image `json:"images,omitempty"` + Images []*Image `json:"images"` Deleted bool `json:"deleted"` - Edits []*Edit `json:"edits,omitempty"` + Edits []*Edit `json:"edits"` SceneCount int `json:"scene_count"` - Scenes []*Scene `json:"scenes,omitempty"` - MergedIds []string `json:"merged_ids,omitempty"` - Studios []*PerformerStudio `json:"studios,omitempty"` + Scenes []*Scene `json:"scenes"` + MergedIds []string `json:"merged_ids"` + Studios []*PerformerStudio `json:"studios"` IsFavorite bool `json:"is_favorite"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` } -func (Performer) IsEditTarget() {} +func (Performer) IsEditTarget() {} + func (Performer) IsSceneDraftPerformer() {} type PerformerAppearance struct { - Performer *Performer `json:"performer,omitempty"` + Performer *Performer `json:"performer"` // Performing as alias As *string `json:"as,omitempty"` } @@ -466,11 +486,11 @@ type PerformerEdit struct { AddedImages []*Image `json:"added_images,omitempty"` RemovedImages []*Image `json:"removed_images,omitempty"` DraftID *string `json:"draft_id,omitempty"` - Aliases []string `json:"aliases,omitempty"` - Urls []*URL `json:"urls,omitempty"` - Images []*Image `json:"images,omitempty"` - Tattoos []*BodyModification `json:"tattoos,omitempty"` - Piercings []*BodyModification `json:"piercings,omitempty"` + Aliases []string `json:"aliases"` + Urls []*URL `json:"urls"` + Images []*Image `json:"images"` + Tattoos []*BodyModification `json:"tattoos"` + Piercings []*BodyModification `json:"piercings"` } func (PerformerEdit) IsEditDetails() {} @@ -501,7 +521,7 @@ type PerformerEditDetailsInput struct { } type PerformerEditInput struct { - Edit *EditInput `json:"edit,omitempty"` + Edit *EditInput `json:"edit"` // Not required for destroy type Details *PerformerEditDetailsInput `json:"details,omitempty"` // Controls aliases modification for merges and name modifications @@ -572,7 +592,7 @@ type PerformerScenesInput struct { } type PerformerStudio struct { - Studio *Studio `json:"studio,omitempty"` + Studio *Studio `json:"studio"` SceneCount int `json:"scene_count"` } @@ -601,55 +621,59 @@ type PerformerUpdateInput struct { ImageIds []string `json:"image_ids,omitempty"` } +// The query root for this schema +type Query struct { +} + type QueryEditsResultType struct { Count int `json:"count"` - Edits []*Edit `json:"edits,omitempty"` + Edits []*Edit `json:"edits"` } type QueryExistingSceneInput struct { Title *string `json:"title,omitempty"` StudioID *string `json:"studio_id,omitempty"` - Fingerprints []*FingerprintInput `json:"fingerprints,omitempty"` + Fingerprints []*FingerprintInput `json:"fingerprints"` } type QueryExistingSceneResult struct { - Edits []*Edit `json:"edits,omitempty"` - Scenes []*Scene `json:"scenes,omitempty"` + Edits []*Edit `json:"edits"` + Scenes []*Scene `json:"scenes"` } type QueryPerformersResultType struct { Count int `json:"count"` - Performers []*Performer `json:"performers,omitempty"` + Performers []*Performer `json:"performers"` } type QueryScenesResultType struct { Count int `json:"count"` - Scenes []*Scene `json:"scenes,omitempty"` + Scenes []*Scene `json:"scenes"` } type QuerySitesResultType struct { Count int `json:"count"` - Sites []*Site `json:"sites,omitempty"` + Sites []*Site `json:"sites"` } type QueryStudiosResultType struct { Count int `json:"count"` - Studios []*Studio `json:"studios,omitempty"` + Studios []*Studio `json:"studios"` } type QueryTagCategoriesResultType struct { Count int `json:"count"` - TagCategories []*TagCategory `json:"tag_categories,omitempty"` + TagCategories []*TagCategory `json:"tag_categories"` } type QueryTagsResultType struct { Count int `json:"count"` - Tags []*Tag `json:"tags,omitempty"` + Tags []*Tag `json:"tags"` } type QueryUsersResultType struct { Count int `json:"count"` - Users []*User `json:"users,omitempty"` + Users []*User `json:"users"` } type ResetPasswordInput struct { @@ -662,7 +686,7 @@ type RevokeInviteInput struct { } type RoleCriterionInput struct { - Value []RoleEnum `json:"value,omitempty"` + Value []RoleEnum `json:"value"` Modifier CriterionModifier `json:"modifier"` } @@ -672,17 +696,17 @@ type Scene struct { Details *string `json:"details,omitempty"` Date *string `json:"date,omitempty"` ReleaseDate *string `json:"release_date,omitempty"` - Urls []*URL `json:"urls,omitempty"` + Urls []*URL `json:"urls"` Studio *Studio `json:"studio,omitempty"` - Tags []*Tag `json:"tags,omitempty"` - Images []*Image `json:"images,omitempty"` - Performers []*PerformerAppearance `json:"performers,omitempty"` - Fingerprints []*Fingerprint `json:"fingerprints,omitempty"` + Tags []*Tag `json:"tags"` + Images []*Image `json:"images"` + Performers []*PerformerAppearance `json:"performers"` + Fingerprints []*Fingerprint `json:"fingerprints"` Duration *int `json:"duration,omitempty"` Director *string `json:"director,omitempty"` Code *string `json:"code,omitempty"` Deleted bool `json:"deleted"` - Edits []*Edit `json:"edits,omitempty"` + Edits []*Edit `json:"edits"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` } @@ -698,7 +722,7 @@ type SceneCreateInput struct { Performers []*PerformerAppearanceInput `json:"performers,omitempty"` TagIds []string `json:"tag_ids,omitempty"` ImageIds []string `json:"image_ids,omitempty"` - Fingerprints []*FingerprintEditInput `json:"fingerprints,omitempty"` + Fingerprints []*FingerprintEditInput `json:"fingerprints"` Duration *int `json:"duration,omitempty"` Director *string `json:"director,omitempty"` Code *string `json:"code,omitempty"` @@ -717,10 +741,10 @@ type SceneDraft struct { URL *URL `json:"url,omitempty"` Date *string `json:"date,omitempty"` Studio SceneDraftStudio `json:"studio,omitempty"` - Performers []SceneDraftPerformer `json:"performers,omitempty"` + Performers []SceneDraftPerformer `json:"performers"` Tags []SceneDraftTag `json:"tags,omitempty"` Image *Image `json:"image,omitempty"` - Fingerprints []*DraftFingerprint `json:"fingerprints,omitempty"` + Fingerprints []*DraftFingerprint `json:"fingerprints"` } func (SceneDraft) IsDraftData() {} @@ -745,11 +769,11 @@ type SceneEdit struct { Director *string `json:"director,omitempty"` Code *string `json:"code,omitempty"` DraftID *string `json:"draft_id,omitempty"` - Urls []*URL `json:"urls,omitempty"` - Performers []*PerformerAppearance `json:"performers,omitempty"` - Tags []*Tag `json:"tags,omitempty"` - Images []*Image `json:"images,omitempty"` - Fingerprints []*Fingerprint `json:"fingerprints,omitempty"` + Urls []*URL `json:"urls"` + Performers []*PerformerAppearance `json:"performers"` + Tags []*Tag `json:"tags"` + Images []*Image `json:"images"` + Fingerprints []*Fingerprint `json:"fingerprints"` } func (SceneEdit) IsEditDetails() {} @@ -771,7 +795,7 @@ type SceneEditDetailsInput struct { } type SceneEditInput struct { - Edit *EditInput `json:"edit,omitempty"` + Edit *EditInput `json:"edit"` // Not required for destroy type Details *SceneEditDetailsInput `json:"details,omitempty"` } @@ -829,7 +853,7 @@ type Site struct { Description *string `json:"description,omitempty"` URL *string `json:"url,omitempty"` Regex *string `json:"regex,omitempty"` - ValidTypes []ValidSiteTypeEnum `json:"valid_types,omitempty"` + ValidTypes []ValidSiteTypeEnum `json:"valid_types"` Icon string `json:"icon"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` @@ -840,7 +864,7 @@ type SiteCreateInput struct { Description *string `json:"description,omitempty"` URL *string `json:"url,omitempty"` Regex *string `json:"regex,omitempty"` - ValidTypes []ValidSiteTypeEnum `json:"valid_types,omitempty"` + ValidTypes []ValidSiteTypeEnum `json:"valid_types"` } type SiteDestroyInput struct { @@ -853,7 +877,7 @@ type SiteUpdateInput struct { Description *string `json:"description,omitempty"` URL *string `json:"url,omitempty"` Regex *string `json:"regex,omitempty"` - ValidTypes []ValidSiteTypeEnum `json:"valid_types,omitempty"` + ValidTypes []ValidSiteTypeEnum `json:"valid_types"` } type StashBoxConfig struct { @@ -865,6 +889,7 @@ type StashBoxConfig struct { VotingPeriod int `json:"voting_period"` MinDestructiveVotingPeriod int `json:"min_destructive_voting_period"` VoteCronInterval string `json:"vote_cron_interval"` + GuidelinesURL string `json:"guidelines_url"` } type StringCriterionInput struct { @@ -875,19 +900,20 @@ type StringCriterionInput struct { type Studio struct { ID string `json:"id"` Name string `json:"name"` - Urls []*URL `json:"urls,omitempty"` + Urls []*URL `json:"urls"` Parent *Studio `json:"parent,omitempty"` - ChildStudios []*Studio `json:"child_studios,omitempty"` - Images []*Image `json:"images,omitempty"` + ChildStudios []*Studio `json:"child_studios"` + Images []*Image `json:"images"` Deleted bool `json:"deleted"` IsFavorite bool `json:"is_favorite"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` - Performers *QueryPerformersResultType `json:"performers,omitempty"` + Performers *QueryPerformersResultType `json:"performers"` } +func (Studio) IsEditTarget() {} + func (Studio) IsSceneDraftStudio() {} -func (Studio) IsEditTarget() {} type StudioCreateInput struct { Name string `json:"name"` @@ -908,8 +934,8 @@ type StudioEdit struct { Parent *Studio `json:"parent,omitempty"` AddedImages []*Image `json:"added_images,omitempty"` RemovedImages []*Image `json:"removed_images,omitempty"` - Images []*Image `json:"images,omitempty"` - Urls []*URL `json:"urls,omitempty"` + Images []*Image `json:"images"` + Urls []*URL `json:"urls"` } func (StudioEdit) IsEditDetails() {} @@ -922,7 +948,7 @@ type StudioEditDetailsInput struct { } type StudioEditInput struct { - Edit *EditInput `json:"edit,omitempty"` + Edit *EditInput `json:"edit"` // Not required for destroy type Details *StudioEditDetailsInput `json:"details,omitempty"` } @@ -956,15 +982,16 @@ type Tag struct { ID string `json:"id"` Name string `json:"name"` Description *string `json:"description,omitempty"` - Aliases []string `json:"aliases,omitempty"` + Aliases []string `json:"aliases"` Deleted bool `json:"deleted"` - Edits []*Edit `json:"edits,omitempty"` + Edits []*Edit `json:"edits"` Category *TagCategory `json:"category,omitempty"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` } -func (Tag) IsEditTarget() {} +func (Tag) IsEditTarget() {} + func (Tag) IsSceneDraftTag() {} type TagCategory struct { @@ -1008,7 +1035,7 @@ type TagEdit struct { AddedAliases []string `json:"added_aliases,omitempty"` RemovedAliases []string `json:"removed_aliases,omitempty"` Category *TagCategory `json:"category,omitempty"` - Aliases []string `json:"aliases,omitempty"` + Aliases []string `json:"aliases"` } func (TagEdit) IsEditDetails() {} @@ -1021,7 +1048,7 @@ type TagEditDetailsInput struct { } type TagEditInput struct { - Edit *EditInput `json:"edit,omitempty"` + Edit *EditInput `json:"edit"` // Not required for destroy type Details *TagEditDetailsInput `json:"details,omitempty"` } @@ -1034,7 +1061,6 @@ type TagQueryInput struct { // Filter to search name - assumes like query unless quoted Name *string `json:"name,omitempty"` // Filter to category ID - IsFavorite *bool `json:"is_favorite,omitempty"` CategoryID *string `json:"category_id,omitempty"` Page int `json:"page"` PerPage int `json:"per_page"` @@ -1053,7 +1079,7 @@ type TagUpdateInput struct { type URL struct { URL string `json:"url"` Type string `json:"type"` - Site *Site `json:"site,omitempty"` + Site *Site `json:"site"` } type URLInput struct { @@ -1071,14 +1097,15 @@ type User struct { // Should not be visible to other users APIKey *string `json:"api_key,omitempty"` // Vote counts by type - VoteCount *UserVoteCount `json:"vote_count,omitempty"` + VoteCount *UserVoteCount `json:"vote_count"` // Edit counts by status - EditCount *UserEditCount `json:"edit_count,omitempty"` + EditCount *UserEditCount `json:"edit_count"` // Calls to the API from this user over a configurable time period - APICalls int `json:"api_calls"` - InvitedBy *User `json:"invited_by,omitempty"` - InviteTokens *int `json:"invite_tokens,omitempty"` - ActiveInviteCodes []string `json:"active_invite_codes,omitempty"` + APICalls int `json:"api_calls"` + InvitedBy *User `json:"invited_by,omitempty"` + InviteTokens *int `json:"invite_tokens,omitempty"` + ActiveInviteCodes []string `json:"active_invite_codes,omitempty"` + InviteCodes []*InviteKey `json:"invite_codes,omitempty"` } type UserChangePasswordInput struct { @@ -1092,7 +1119,7 @@ type UserCreateInput struct { Name string `json:"name"` // Password in plain text Password string `json:"password"` - Roles []RoleEnum `json:"roles,omitempty"` + Roles []RoleEnum `json:"roles"` Email string `json:"email"` InvitedByID *string `json:"invited_by_id,omitempty"` } diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index 0b0cf68d67e..670ec95980b 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -13,7 +13,7 @@ import ( "strconv" "strings" - "github.com/Yamashou/gqlgenc/client" + "github.com/Yamashou/gqlgenc/clientv2" "github.com/Yamashou/gqlgenc/graphqljson" "github.com/gofrs/uuid/v5" "golang.org/x/text/cases" @@ -89,12 +89,13 @@ type Client struct { // NewClient returns a new instance of a stash-box client. func NewClient(box models.StashBox, repo Repository) *Client { - authHeader := func(req *http.Request) { + authHeader := func(ctx context.Context, req *http.Request, gqlInfo *clientv2.GQLRequestInfo, res interface{}, next clientv2.RequestInterceptorFunc) error { req.Header.Set("ApiKey", box.APIKey) + return next(ctx, req, gqlInfo, res) } client := &graphql.Client{ - Client: client.NewClient(http.DefaultClient, box.Endpoint, authHeader), + Client: clientv2.NewClient(http.DefaultClient, box.Endpoint, nil, authHeader), } return &Client{ @@ -627,7 +628,7 @@ func performerFragmentToScrapedPerformer(p graphql.PerformerFragment) *models.Sc Name: &p.Name, Disambiguation: p.Disambiguation, Country: p.Country, - Measurements: formatMeasurements(p.Measurements), + Measurements: formatMeasurements(*p.Measurements), CareerLength: formatCareerLength(p.CareerStartYear, p.CareerEndYear), Tattoos: formatBodyModifications(p.Tattoos), Piercings: formatBodyModifications(p.Piercings), @@ -647,9 +648,8 @@ func performerFragmentToScrapedPerformer(p graphql.PerformerFragment) *models.Sc sp.Height = &hs } - if p.Birthdate != nil { - b := p.Birthdate.Date - sp.Birthdate = &b + if p.BirthDate != nil { + sp.Birthdate = padFuzzyDate(p.BirthDate) } if p.Gender != nil { @@ -805,7 +805,7 @@ func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.Scen } for _, p := range s.Performers { - sp := performerFragmentToScrapedPerformer(p.Performer) + sp := performerFragmentToScrapedPerformer(*p.Performer) err := match.ScrapedPerformer(ctx, pqb, sp, &c.box.Endpoint) if err != nil { @@ -1281,7 +1281,7 @@ func (c *Client) submitDraft(ctx context.Context, query string, input interface{ "input": input, } - r := &client.Request{ + r := &clientv2.Request{ Query: query, Variables: vars, OperationName: "", @@ -1341,7 +1341,7 @@ func (c *Client) submitDraft(ctx context.Context, query string, input interface{ if len(respGQL.Errors) > 0 { // try to parse standard graphql error - errors := &client.GqlErrorList{} + errors := &clientv2.GqlErrorList{} if e := json.Unmarshal(responseBytes, errors); e != nil { return fmt.Errorf("failed to parse graphql errors. Response content %s - %w ", string(responseBytes), e) } @@ -1355,3 +1355,20 @@ func (c *Client) submitDraft(ctx context.Context, query string, input interface{ return err } + +func padFuzzyDate(date *string) *string { + if date == nil { + return nil + } + + var paddedDate string + switch len(*date) { + case 10: + paddedDate = *date + case 7: + paddedDate = fmt.Sprintf("%s-01", *date) + case 4: + paddedDate = fmt.Sprintf("%s-01-01", *date) + } + return &paddedDate +} diff --git a/pkg/sliceutil/collections.go b/pkg/sliceutil/collections.go index 18930df259e..eff28fc40bc 100644 --- a/pkg/sliceutil/collections.go +++ b/pkg/sliceutil/collections.go @@ -1,26 +1,14 @@ // Package sliceutil provides utilities for working with slices. package sliceutil -// Index returns the first index of the provided value in the provided -// slice. It returns -1 if it is not found. -func Index[T comparable](vs []T, t T) int { - for i, v := range vs { - if v == t { - return i - } - } - return -1 -} - -// Contains returns whether the vs slice contains t. -func Contains[T comparable](vs []T, t T) bool { - return Index(vs, t) >= 0 -} +import ( + "slices" +) // AppendUnique appends toAdd to the vs slice if toAdd does not already // exist in the slice. It returns the new or unchanged slice. func AppendUnique[T comparable](vs []T, toAdd T) []T { - if Contains(vs, toAdd) { + if slices.Contains(vs, toAdd) { return vs } @@ -31,6 +19,13 @@ func AppendUnique[T comparable](vs []T, toAdd T) []T { // appends values that do not already exist in the slice. // It returns the new or unchanged slice. func AppendUniques[T comparable](vs []T, toAdd []T) []T { + if len(toAdd) == 0 { + return vs + } + + // Extend the slice's capacity to avoid multiple re-allocations even in the worst case + vs = slices.Grow(vs, len(toAdd)) + for _, v := range toAdd { vs = AppendUnique(vs, v) } @@ -41,9 +36,9 @@ func AppendUniques[T comparable](vs []T, toAdd []T) []T { // Exclude returns a copy of the vs slice, excluding all values // that are also present in the toExclude slice. func Exclude[T comparable](vs []T, toExclude []T) []T { - var ret []T + ret := make([]T, 0, len(vs)) for _, v := range vs { - if !Contains(toExclude, v) { + if !slices.Contains(toExclude, v) { ret = append(ret, v) } } @@ -53,8 +48,8 @@ func Exclude[T comparable](vs []T, toExclude []T) []T { // Unique returns a copy of the vs slice, with non-unique values removed. func Unique[T comparable](vs []T) []T { - distinctValues := make(map[T]struct{}) - var ret []T + distinctValues := make(map[T]struct{}, len(vs)) + ret := make([]T, 0, len(vs)) for _, v := range vs { if _, exists := distinctValues[v]; !exists { distinctValues[v] = struct{}{} @@ -66,7 +61,7 @@ func Unique[T comparable](vs []T) []T { // Delete returns a copy of the vs slice with toDel values removed. func Delete[T comparable](vs []T, toDel T) []T { - var ret []T + ret := make([]T, 0, len(vs)) for _, v := range vs { if v != toDel { ret = append(ret, v) @@ -79,7 +74,7 @@ func Delete[T comparable](vs []T, toDel T) []T { func Intersect[T comparable](a []T, b []T) []T { var ret []T for _, v := range a { - if Contains(b, v) { + if slices.Contains(b, v) { ret = append(ret, v) } } @@ -91,13 +86,13 @@ func Intersect[T comparable](a []T, b []T) []T { func NotIntersect[T comparable](a []T, b []T) []T { var ret []T for _, v := range a { - if !Contains(b, v) { + if !slices.Contains(b, v) { ret = append(ret, v) } } for _, v := range b { - if !Contains(a, v) { + if !slices.Contains(a, v) { ret = append(ret, v) } } @@ -166,8 +161,9 @@ func PtrsToValues[T any](vs []*T) []T { func ValuesToPtrs[T any](vs []T) []*T { ret := make([]*T, len(vs)) for i, v := range vs { - vv := v - ret[i] = &vv + // We can do this safely because go.mod indicates Go 1.22 + // See: https://go.dev/blog/loopvar-preview + ret[i] = &v } return ret } diff --git a/pkg/sliceutil/collections_test.go b/pkg/sliceutil/collections_test.go index 70d34946fce..ab739607eb3 100644 --- a/pkg/sliceutil/collections_test.go +++ b/pkg/sliceutil/collections_test.go @@ -1,6 +1,7 @@ package sliceutil import ( + "reflect" "testing" "github.com/stretchr/testify/assert" @@ -66,3 +67,85 @@ func TestSliceSame(t *testing.T) { }) } } + +func TestAppendUniques(t *testing.T) { + type args struct { + vs []int + toAdd []int + } + tests := []struct { + name string + args args + want []int + }{ + { + name: "append to empty slice", + args: args{ + vs: []int{}, + toAdd: []int{1, 2, 3}, + }, + want: []int{1, 2, 3}, + }, + { + name: "append all unique values", + args: args{ + vs: []int{1, 2, 3}, + toAdd: []int{4, 5, 6}, + }, + want: []int{1, 2, 3, 4, 5, 6}, + }, + { + name: "append with some duplicates", + args: args{ + vs: []int{1, 2, 3}, + toAdd: []int{3, 4, 5}, + }, + want: []int{1, 2, 3, 4, 5}, + }, + { + name: "append all duplicates", + args: args{ + vs: []int{1, 2, 3}, + toAdd: []int{1, 2, 3}, + }, + want: []int{1, 2, 3}, + }, + { + name: "append to nil slice", + args: args{ + vs: nil, + toAdd: []int{1, 2, 3}, + }, + want: []int{1, 2, 3}, + }, + { + name: "append empty slice", + args: args{ + vs: []int{1, 2, 3}, + toAdd: []int{}, + }, + want: []int{1, 2, 3}, + }, + { + name: "append nil to slice", + args: args{ + vs: []int{1, 2, 3}, + toAdd: nil, + }, + want: []int{1, 2, 3}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := AppendUniques(tt.args.vs, tt.args.toAdd); !reflect.DeepEqual(got, tt.want) { + t.Errorf("AppendUniques() = %v, want %v", got, tt.want) + } + }) + } +} + +func BenchmarkAppendUniques(b *testing.B) { + for i := 0; i < b.N; i++ { + AppendUniques([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, []int{3, 4, 4, 11, 12, 13, 14, 15, 16, 17, 18}) + } +} diff --git a/pkg/sliceutil/stringslice/string_collections.go b/pkg/sliceutil/stringslice/string_collections.go index e4b9ca6a915..f6ea1361c5f 100644 --- a/pkg/sliceutil/stringslice/string_collections.go +++ b/pkg/sliceutil/stringslice/string_collections.go @@ -33,8 +33,8 @@ func FromString(s string, sep string) []string { // Unique returns a slice containing only unique values from the provided slice. // The comparison is case-insensitive. func UniqueFold(s []string) []string { - seen := make(map[string]struct{}) - var ret []string + seen := make(map[string]struct{}, len(s)) + ret := make([]string, 0, len(s)) for _, v := range s { if _, exists := seen[strings.ToLower(v)]; exists { continue diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index b5bbacfc23f..cd3d49309e9 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -36,7 +36,7 @@ const ( cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE" ) -var appSchemaVersion uint = 67 +var appSchemaVersion uint = 69 //go:embed migrations/*.sql migrationsPostgres/*.sql var migrationsBox embed.FS diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index 435240d4fe6..faf607a41fe 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -6,12 +6,12 @@ import ( "errors" "fmt" "path/filepath" + "slices" "github.com/doug-martin/goqu/v9" "github.com/doug-martin/goqu/v9/exp" "github.com/jmoiron/sqlx" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" "gopkg.in/guregu/null.v4" "gopkg.in/guregu/null.v4/zero" ) @@ -412,7 +412,7 @@ func (qb *GalleryStore) FindMany(ctx context.Context, ids []int) ([]*models.Gall } for _, s := range unsorted { - i := sliceutil.Index(ids, s.ID) + i := slices.Index(ids, s.ID) galleries[i] = s } diff --git a/pkg/sqlite/gallery_chapter.go b/pkg/sqlite/gallery_chapter.go index f0d9c52980b..92ae63f5f83 100644 --- a/pkg/sqlite/gallery_chapter.go +++ b/pkg/sqlite/gallery_chapter.go @@ -5,13 +5,13 @@ import ( "database/sql" "errors" "fmt" + "slices" "github.com/doug-martin/goqu/v9" "github.com/doug-martin/goqu/v9/exp" "github.com/jmoiron/sqlx" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" ) const ( @@ -162,7 +162,7 @@ func (qb *GalleryChapterStore) FindMany(ctx context.Context, ids []int) ([]*mode } for _, s := range unsorted { - i := sliceutil.Index(ids, s.ID) + i := slices.Index(ids, s.ID) ret[i] = s } diff --git a/pkg/sqlite/group.go b/pkg/sqlite/group.go index 4d4e54a001c..0325a7b5b6d 100644 --- a/pkg/sqlite/group.go +++ b/pkg/sqlite/group.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "slices" "github.com/doug-martin/goqu/v9" "github.com/doug-martin/goqu/v9/exp" @@ -13,7 +14,6 @@ import ( "gopkg.in/guregu/null.v4/zero" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" ) const ( @@ -295,7 +295,7 @@ func (qb *GroupStore) FindMany(ctx context.Context, ids []int) ([]*models.Group, } for _, s := range unsorted { - i := sliceutil.Index(ids, s.ID) + i := slices.Index(ids, s.ID) ret[i] = s } diff --git a/pkg/sqlite/group_test.go b/pkg/sqlite/group_test.go index 4b8fe97ae03..556eaf800c4 100644 --- a/pkg/sqlite/group_test.go +++ b/pkg/sqlite/group_test.go @@ -6,6 +6,7 @@ package sqlite_test import ( "context" "fmt" + "slices" "strconv" "strings" "testing" @@ -1605,7 +1606,7 @@ func TestGroupReorderSubGroups(t *testing.T) { // get ids of groups newIDs := sliceutil.Map(gd, func(gd models.GroupIDDescription) int { return gd.GroupID }) - newIdxs := sliceutil.Map(newIDs, func(id int) int { return sliceutil.Index(idxToId, id) }) + newIdxs := sliceutil.Map(newIDs, func(id int) int { return slices.Index(idxToId, id) }) assert.ElementsMatch(t, tt.expectedIdxs, newIdxs) }) @@ -1733,7 +1734,7 @@ func TestGroupAddSubGroups(t *testing.T) { // get ids of groups newIDs := sliceutil.Map(gd, func(gd models.GroupIDDescription) int { return gd.GroupID }) - newIdxs := sliceutil.Map(newIDs, func(id int) int { return sliceutil.Index(idxToId, id) }) + newIdxs := sliceutil.Map(newIDs, func(id int) int { return slices.Index(idxToId, id) }) assert.ElementsMatch(t, tt.expectedIdxs, newIdxs) }) @@ -1828,7 +1829,7 @@ func TestGroupRemoveSubGroups(t *testing.T) { // get ids of groups newIDs := sliceutil.Map(gd, func(gd models.GroupIDDescription) int { return gd.GroupID }) - newIdxs := sliceutil.Map(newIDs, func(id int) int { return sliceutil.Index(idxToId, id) }) + newIdxs := sliceutil.Map(newIDs, func(id int) int { return slices.Index(idxToId, id) }) assert.ElementsMatch(t, tt.expectedIdxs, newIdxs) }) @@ -1883,7 +1884,7 @@ func TestGroupFindSubGroupIDs(t *testing.T) { } // get ids of groups - foundIdxs := sliceutil.Map(found, func(id int) int { return sliceutil.Index(groupIDs, id) }) + foundIdxs := sliceutil.Map(found, func(id int) int { return slices.Index(groupIDs, id) }) assert.ElementsMatch(t, tt.expectedIdxs, foundIdxs) }) diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index 7df2190fd0f..7031bf628bd 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "path/filepath" + "slices" "github.com/jmoiron/sqlx" "github.com/stashapp/stash/pkg/models" @@ -398,7 +399,7 @@ func (qb *ImageStore) FindMany(ctx context.Context, ids []int) ([]*models.Image, } for _, s := range unsorted { - i := sliceutil.Index(ids, s.ID) + i := slices.Index(ids, s.ID) images[i] = s } @@ -528,7 +529,10 @@ func (qb *ImageStore) GetFiles(ctx context.Context, id int) ([]models.File, erro return nil, err } - return files, nil + ret := make([]models.File, len(files)) + copy(ret, files) + + return ret, nil } func (qb *ImageStore) GetManyFileIDs(ctx context.Context, ids []int) ([][]models.FileID, error) { diff --git a/pkg/sqlite/migrations/62_performer_urls.up.sql b/pkg/sqlite/migrations/62_performer_urls.up.sql index cebfa86d616..d3073463731 100644 --- a/pkg/sqlite/migrations/62_performer_urls.up.sql +++ b/pkg/sqlite/migrations/62_performer_urls.up.sql @@ -144,9 +144,9 @@ INSERT INTO `performer_urls` FROM `performers` WHERE `performers`.`instagram` IS NOT NULL AND `performers`.`instagram` != ''; -DROP INDEX `performers_name_disambiguation_unique`; -DROP INDEX `performers_name_unique`; -DROP TABLE `performers`; +DROP INDEX IF EXISTS `performers_name_disambiguation_unique`; +DROP INDEX IF EXISTS `performers_name_unique`; +DROP TABLE IF EXISTS `performers`; ALTER TABLE `performers_new` rename to `performers`; CREATE UNIQUE INDEX `performers_name_disambiguation_unique` on `performers` (`name`, `disambiguation`) WHERE `disambiguation` IS NOT NULL; diff --git a/pkg/sqlite/migrations/68_image_studio_index.up.sql b/pkg/sqlite/migrations/68_image_studio_index.up.sql new file mode 100644 index 00000000000..3bb354e7390 --- /dev/null +++ b/pkg/sqlite/migrations/68_image_studio_index.up.sql @@ -0,0 +1,7 @@ +-- with the existing index, if no images have a studio id, then the index is +-- not used when filtering by studio id. The assumption with this change is that +-- most images don't have a studio id, so filtering by non-null studio id should +-- be faster with this index. This is a tradeoff, as filtering by null studio id +-- will be slower. +DROP INDEX index_images_on_studio_id; +CREATE INDEX `index_images_on_studio_id` on `images` (`studio_id`) WHERE `studio_id` IS NOT NULL; \ No newline at end of file diff --git a/pkg/sqlite/migrations/69_stash_id_updated_at.up.sql b/pkg/sqlite/migrations/69_stash_id_updated_at.up.sql new file mode 100644 index 00000000000..1ffb280bd48 --- /dev/null +++ b/pkg/sqlite/migrations/69_stash_id_updated_at.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `performer_stash_ids` ADD COLUMN `updated_at` datetime not null default '1970-01-01T00:00:00Z'; +ALTER TABLE `scene_stash_ids` ADD COLUMN `updated_at` datetime not null default '1970-01-01T00:00:00Z'; +ALTER TABLE `studio_stash_ids` ADD COLUMN `updated_at` datetime not null default '1970-01-01T00:00:00Z'; diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index 1b303d532c3..883ba403091 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -5,12 +5,12 @@ import ( "database/sql" "errors" "fmt" + "slices" "github.com/doug-martin/goqu/v9" "github.com/doug-martin/goqu/v9/exp" "github.com/jmoiron/sqlx" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" "github.com/stashapp/stash/pkg/utils" "gopkg.in/guregu/null.v4" "gopkg.in/guregu/null.v4/zero" @@ -398,7 +398,7 @@ func (qb *PerformerStore) FindMany(ctx context.Context, ids []int) ([]*models.Pe } for _, s := range unsorted { - i := sliceutil.Index(ids, s.ID) + i := slices.Index(ids, s.ID) ret[i] = s } diff --git a/pkg/sqlite/repository.go b/pkg/sqlite/repository.go index 3a3a08b331f..1d4961df85e 100644 --- a/pkg/sqlite/repository.go +++ b/pkg/sqlite/repository.go @@ -14,11 +14,6 @@ import ( const idColumn = "id" -type objectList interface { - Append(o interface{}) - New() interface{} -} - type repository struct { tableName string idColumn string @@ -125,17 +120,6 @@ func (r *repository) queryFunc(ctx context.Context, query string, args []interfa return nil } -func (r *repository) query(ctx context.Context, query string, args []interface{}, out objectList) error { - return r.queryFunc(ctx, query, args, false, func(rows *sqlx.Rows) error { - object := out.New() - if err := rows.StructScan(object); err != nil { - return err - } - out.Append(object) - return nil - }) -} - func (r *repository) queryStruct(ctx context.Context, query string, args []interface{}, out interface{}) error { if err := r.queryFunc(ctx, query, args, true, func(rows *sqlx.Rows) error { if err := rows.StructScan(out); err != nil { @@ -427,7 +411,7 @@ type stashIDRepository struct { type stashIDs []models.StashID func (s *stashIDs) Append(o interface{}) { - *s = append(*s, *o.(*models.StashID)) + *s = append(*s, o.(models.StashID)) } func (s *stashIDs) New() interface{} { @@ -435,10 +419,17 @@ func (s *stashIDs) New() interface{} { } func (r *stashIDRepository) get(ctx context.Context, id int) ([]models.StashID, error) { - query := fmt.Sprintf("SELECT stash_id, endpoint from %s WHERE %s = ?", r.tableName, r.idColumn) + query := fmt.Sprintf("SELECT stash_id, endpoint, updated_at from %s WHERE %s = ?", r.tableName, r.idColumn) var ret stashIDs - err := r.query(ctx, query, []interface{}{id}, &ret) - return []models.StashID(ret), err + err := r.queryFunc(ctx, query, []interface{}{id}, false, func(rows *sqlx.Rows) error { + var v stashIDRow + if err := rows.StructScan(&v); err != nil { + return err + } + ret.Append(v.resolve()) + return nil + }) + return ret, err } type filesRepository struct { diff --git a/pkg/sqlite/saved_filter.go b/pkg/sqlite/saved_filter.go index 8f58b05e76c..583e2406259 100644 --- a/pkg/sqlite/saved_filter.go +++ b/pkg/sqlite/saved_filter.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "github.com/doug-martin/goqu/v9" "github.com/doug-martin/goqu/v9/exp" @@ -13,7 +14,6 @@ import ( "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" ) const ( @@ -165,7 +165,7 @@ func (qb *SavedFilterStore) FindMany(ctx context.Context, ids []int, ignoreNotFo } for _, s := range unsorted { - i := sliceutil.Index(ids, s.ID) + i := slices.Index(ids, s.ID) ret[i] = s } diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 0ded274b2d7..950d788a4bb 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -504,7 +505,7 @@ func (qb *SceneStore) FindMany(ctx context.Context, ids []int) ([]*models.Scene, } for _, s := range unsorted { - i := sliceutil.Index(ids, s.ID) + i := slices.Index(ids, s.ID) scenes[i] = s } diff --git a/pkg/sqlite/scene_marker.go b/pkg/sqlite/scene_marker.go index c42304c7338..c8165335460 100644 --- a/pkg/sqlite/scene_marker.go +++ b/pkg/sqlite/scene_marker.go @@ -5,13 +5,13 @@ import ( "database/sql" "errors" "fmt" + "slices" "github.com/doug-martin/goqu/v9" "github.com/doug-martin/goqu/v9/exp" "github.com/jmoiron/sqlx" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" ) const sceneMarkerTable = "scene_markers" @@ -188,7 +188,7 @@ func (qb *SceneMarkerStore) FindMany(ctx context.Context, ids []int) ([]*models. } for _, s := range unsorted { - i := sliceutil.Index(ids, s.ID) + i := slices.Index(ids, s.ID) ret[i] = s } diff --git a/pkg/sqlite/scene_marker_test.go b/pkg/sqlite/scene_marker_test.go index 1be14ba8275..0b0e3adfc81 100644 --- a/pkg/sqlite/scene_marker_test.go +++ b/pkg/sqlite/scene_marker_test.go @@ -5,11 +5,11 @@ package sqlite_test import ( "context" + "slices" "strconv" "testing" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" "github.com/stashapp/stash/pkg/sliceutil/stringslice" "github.com/stretchr/testify/assert" ) @@ -133,7 +133,7 @@ func verifyIDs(t *testing.T, modifier models.CriterionModifier, values []int, re case models.CriterionModifierNotEquals: foundAll := true for _, v := range values { - if !sliceutil.Contains(results, v) { + if !slices.Contains(results, v) { foundAll = false break } diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index f775caeb933..8f7a9ac8eea 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -10,13 +10,13 @@ import ( "fmt" "os" "path/filepath" + "slices" "strconv" "testing" "time" "github.com/stashapp/stash/internal/manager/config" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" "github.com/stashapp/stash/pkg/sqlite" "github.com/stashapp/stash/pkg/txn" @@ -1605,7 +1605,7 @@ func getTagMarkerCount(id int) int { count := 0 idx := indexFromID(tagIDs, id) for _, s := range markerSpecs { - if s.primaryTagIdx == idx || sliceutil.Contains(s.tagIdxs, idx) { + if s.primaryTagIdx == idx || slices.Contains(s.tagIdxs, idx) { count++ } } diff --git a/pkg/sqlite/studio.go b/pkg/sqlite/studio.go index e06f12a6047..9e82bb532bf 100644 --- a/pkg/sqlite/studio.go +++ b/pkg/sqlite/studio.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "slices" "github.com/doug-martin/goqu/v9" "github.com/doug-martin/goqu/v9/exp" @@ -13,7 +14,6 @@ import ( "gopkg.in/guregu/null.v4/zero" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" "github.com/stashapp/stash/pkg/studio" ) @@ -305,7 +305,7 @@ func (qb *StudioStore) FindMany(ctx context.Context, ids []int) ([]*models.Studi } for _, s := range unsorted { - i := sliceutil.Index(ids, s.ID) + i := slices.Index(ids, s.ID) ret[i] = s } diff --git a/pkg/sqlite/table.go b/pkg/sqlite/table.go index 94872f22111..0a80ee84a8d 100644 --- a/pkg/sqlite/table.go +++ b/pkg/sqlite/table.go @@ -271,19 +271,21 @@ type stashIDTable struct { } type stashIDRow struct { - StashID null.String `db:"stash_id"` - Endpoint null.String `db:"endpoint"` + StashID null.String `db:"stash_id"` + Endpoint null.String `db:"endpoint"` + UpdatedAt Timestamp `db:"updated_at"` } func (r *stashIDRow) resolve() models.StashID { return models.StashID{ - StashID: r.StashID.String, - Endpoint: r.Endpoint.String, + StashID: r.StashID.String, + Endpoint: r.Endpoint.String, + UpdatedAt: r.UpdatedAt.Timestamp, } } func (t *stashIDTable) get(ctx context.Context, id int) ([]models.StashID, error) { - q := dialect.Select("endpoint", "stash_id").From(t.table.table).Where(t.idColumn.Eq(id)) + q := dialect.Select("endpoint", "stash_id", "updated_at").From(t.table.table).Where(t.idColumn.Eq(id)) const single = false var ret []models.StashID @@ -304,8 +306,8 @@ func (t *stashIDTable) get(ctx context.Context, id int) ([]models.StashID, error } func (t *stashIDTable) insertJoin(ctx context.Context, id int, v models.StashID) (sql.Result, error) { - q := dialect.Insert(t.table.table).Cols(t.idColumn.GetCol(), "endpoint", "stash_id").Vals( - goqu.Vals{id, v.Endpoint, v.StashID}, + var q = dialect.Insert(t.table.table).Cols(t.idColumn.GetCol(), "endpoint", "stash_id", "updated_at").Vals( + goqu.Vals{id, v.Endpoint, v.StashID, v.UpdatedAt}, ) ret, err := exec(ctx, q) if err != nil { diff --git a/pkg/sqlite/tag.go b/pkg/sqlite/tag.go index a97929e735b..5fdcf6b06cc 100644 --- a/pkg/sqlite/tag.go +++ b/pkg/sqlite/tag.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "slices" "strings" "github.com/doug-martin/goqu/v9" @@ -14,7 +15,6 @@ import ( "gopkg.in/guregu/null.v4/zero" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil" ) const ( @@ -312,7 +312,7 @@ func (qb *TagStore) FindMany(ctx context.Context, ids []int) ([]*models.Tag, err } for _, s := range unsorted { - i := sliceutil.Index(ids, s.ID) + i := slices.Index(ids, s.ID) ret[i] = s } diff --git a/pkg/studio/import.go b/pkg/studio/import.go index d880650787d..3aaceb09374 100644 --- a/pkg/studio/import.go +++ b/pkg/studio/import.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "github.com/stashapp/stash/pkg/models" @@ -80,7 +81,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 { diff --git a/ui/v2.5/graphql/data/performer-slim.graphql b/ui/v2.5/graphql/data/performer-slim.graphql index 1a4b9833bc8..56a30842ddb 100644 --- a/ui/v2.5/graphql/data/performer-slim.graphql +++ b/ui/v2.5/graphql/data/performer-slim.graphql @@ -27,6 +27,7 @@ fragment SlimPerformerData on Performer { stash_ids { endpoint stash_id + updated_at } rating100 death_date diff --git a/ui/v2.5/graphql/data/performer.graphql b/ui/v2.5/graphql/data/performer.graphql index 144382a4522..0aa60ce21bb 100644 --- a/ui/v2.5/graphql/data/performer.graphql +++ b/ui/v2.5/graphql/data/performer.graphql @@ -34,6 +34,7 @@ fragment PerformerData on Performer { stash_ids { stash_id endpoint + updated_at } rating100 details diff --git a/ui/v2.5/graphql/data/scene-slim.graphql b/ui/v2.5/graphql/data/scene-slim.graphql index 7e2a4ffad2d..d5899a24764 100644 --- a/ui/v2.5/graphql/data/scene-slim.graphql +++ b/ui/v2.5/graphql/data/scene-slim.graphql @@ -84,5 +84,6 @@ fragment SlimSceneData on Scene { stash_ids { endpoint stash_id + updated_at } } diff --git a/ui/v2.5/graphql/data/scene.graphql b/ui/v2.5/graphql/data/scene.graphql index ef58922295a..e4a6e5cc69f 100644 --- a/ui/v2.5/graphql/data/scene.graphql +++ b/ui/v2.5/graphql/data/scene.graphql @@ -71,6 +71,7 @@ fragment SceneData on Scene { stash_ids { endpoint stash_id + updated_at } sceneStreams { diff --git a/ui/v2.5/graphql/data/studio-slim.graphql b/ui/v2.5/graphql/data/studio-slim.graphql index 406a2ffa70a..cf101bd047c 100644 --- a/ui/v2.5/graphql/data/studio-slim.graphql +++ b/ui/v2.5/graphql/data/studio-slim.graphql @@ -5,6 +5,7 @@ fragment SlimStudioData on Studio { stash_ids { endpoint stash_id + updated_at } parent_studio { id diff --git a/ui/v2.5/graphql/data/studio.graphql b/ui/v2.5/graphql/data/studio.graphql index feb35136fed..25e77675549 100644 --- a/ui/v2.5/graphql/data/studio.graphql +++ b/ui/v2.5/graphql/data/studio.graphql @@ -28,6 +28,7 @@ fragment StudioData on Studio { stash_ids { stash_id endpoint + updated_at } details rating100 diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index ca68160622b..210b750fe0c 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -125,7 +125,7 @@ "terser": "^5.9.0", "ts-node": "^10.9.1", "typescript": "~4.8.4", - "vite": "^4.5.3", + "vite": "^4.5.5", "vite-plugin-compression": "^0.5.1", "vite-tsconfig-paths": "^4.0.5" } diff --git a/ui/v2.5/src/components/Galleries/Galleries.tsx b/ui/v2.5/src/components/Galleries/Galleries.tsx index db3db8dddc0..c845a153c89 100644 --- a/ui/v2.5/src/components/Galleries/Galleries.tsx +++ b/ui/v2.5/src/components/Galleries/Galleries.tsx @@ -5,7 +5,6 @@ import { useTitleProps } from "src/hooks/title"; import Gallery from "./GalleryDetails/Gallery"; import GalleryCreate from "./GalleryDetails/GalleryCreate"; import { GalleryList } from "./GalleryList"; -import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { View } from "../List/views"; import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { ErrorMessage } from "../Shared/ErrorMessage"; @@ -41,8 +40,6 @@ const GalleryImage: React.FC> = ({ }; const Galleries: React.FC = () => { - useScrollToTopOnMount(); - return ; }; diff --git a/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx b/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx index 0aa4dbd5472..913b2bc52c7 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx @@ -21,7 +21,7 @@ import { GroupDetailsPanel, } from "./GroupDetailsPanel"; import { GroupEditPanel } from "./GroupEditPanel"; -import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; +import { faRefresh, faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { ConfigurationContext } from "src/hooks/Config"; import { DetailImage } from "src/components/Shared/DetailImage"; @@ -39,8 +39,9 @@ import { TabTitleCounter, useTabKey, } from "src/components/Shared/DetailsPage/Tabs"; -import { Tab, Tabs } from "react-bootstrap"; +import { Button, Tab, Tabs } from "react-bootstrap"; import { GroupSubGroupsPanel } from "./GroupSubGroupsPanel"; +import { Icon } from "src/components/Shared/Icon"; const validTabs = ["default", "scenes", "subgroups"] as const; type TabKey = (typeof validTabs)[number]; @@ -130,6 +131,8 @@ const GroupPage: React.FC = ({ group, tabKey }) => { const showAllDetails = uiConfig?.showAllDetails ?? true; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; + const [focusedOnFront, setFocusedOnFront] = useState(true); + const [collapsed, setCollapsed] = useState(!showAllDetails); const loadStickyHeader = useLoadStickyHeader(); @@ -328,7 +331,13 @@ const GroupPage: React.FC = ({ group, tabKey }) => {
{!!activeFrontImage && ( - + )} {!!activeBackImage && ( @@ -336,9 +345,23 @@ const GroupPage: React.FC = ({ group, tabKey }) => { images={lightboxImages} index={lightboxImages.length - 1} > - + )} + {!!(activeFrontImage && activeBackImage) && ( + + )}
diff --git a/ui/v2.5/src/components/Groups/GroupDetails/GroupDetailsPanel.tsx b/ui/v2.5/src/components/Groups/GroupDetails/GroupDetailsPanel.tsx index 6a20eb9081a..d93b0646636 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/GroupDetailsPanel.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/GroupDetailsPanel.tsx @@ -36,7 +36,6 @@ interface IGroupDetailsPanel { export const GroupDetailsPanel: React.FC = ({ group, - collapsed, fullWidth, }) => { // Network state @@ -55,32 +54,6 @@ export const GroupDetailsPanel: React.FC = ({ ); } - function maybeRenderExtraDetails() { - if (!collapsed) { - return ( - <> - - - {group.containing_groups.length > 0 && ( - } - fullWidth={fullWidth} - /> - )} - - ); - } - } - return (
= ({ } fullWidth={fullWidth} /> - = ({ } fullWidth={fullWidth} /> - {maybeRenderExtraDetails()} + + + {group.containing_groups.length > 0 && ( + } + fullWidth={fullWidth} + /> + )}
); }; diff --git a/ui/v2.5/src/components/Groups/Groups.tsx b/ui/v2.5/src/components/Groups/Groups.tsx index a2e4d90834d..5ec7b4eaf06 100644 --- a/ui/v2.5/src/components/Groups/Groups.tsx +++ b/ui/v2.5/src/components/Groups/Groups.tsx @@ -5,12 +5,9 @@ import { useTitleProps } from "src/hooks/title"; import Group from "./GroupDetails/Group"; import GroupCreate from "./GroupDetails/GroupCreate"; import { GroupList } from "./GroupList"; -import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { View } from "../List/views"; const Groups: React.FC = () => { - useScrollToTopOnMount(); - return ; }; diff --git a/ui/v2.5/src/components/Groups/styles.scss b/ui/v2.5/src/components/Groups/styles.scss index 1b80045c73d..dd159ff21d1 100644 --- a/ui/v2.5/src/components/Groups/styles.scss +++ b/ui/v2.5/src/components/Groups/styles.scss @@ -41,8 +41,21 @@ } } -#group-page .rating-number .text-input { - width: auto; +#group-page { + .rating-number .text-input { + width: auto; + } + + // the detail element ids are the same as field type name + // which don't follow the correct convention + /* stylelint-disable selector-class-pattern */ + .collapsed { + .detail-item.tags, + .detail-item.containing_groups { + display: none; + } + } + /* stylelint-enable selector-class-pattern */ } .group-select-option { @@ -91,6 +104,62 @@ } } +button.flip { + display: none; +} + +#group-page .detail-header:not(.collapsed) { + .group-images { + padding: 0.375rem 0.75rem; + position: relative; + z-index: 1; + + button.btn-link { + padding: 0; + position: relative; + transition: all 0.3s; + z-index: 1; + } + + button:has(.active) { + z-index: 2; + } + + button:has(.inactive) { + opacity: 0.5; + padding: 0; + transform: rotateY(180deg); + } + + button.flip { + align-items: center; + border-radius: 50%; + bottom: -5px; + display: flex; + font-size: 20px; + height: 40px; + justify-content: center; + padding: 0; + position: absolute; + right: -5px; + width: 40px; + z-index: 2; + } + + img.active { + max-width: 22rem; + } + + img.inactive { + display: none; + } + } + + .detail-item .detail-item-title { + width: 150px; + } +} + .groups-list { list-style-type: none; padding-inline-start: 0; diff --git a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx index 0a77180ceaa..e18571cd0e8 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx @@ -279,7 +279,10 @@ const ImagePage: React.FC = ({ image }) => { ); const title = objectTitle(image); - const ImageView = isVideo(image.visual_files[0]) ? "video" : "img"; + const ImageView = + image.visual_files.length > 0 && isVideo(image.visual_files[0]) + ? "video" + : "img"; const resolution = useMemo(() => { return file?.width && file?.height @@ -362,19 +365,21 @@ const ImagePage: React.FC = ({ image }) => { {renderTabs()}
- + {image.visual_files.length > 0 && ( + + )}
); diff --git a/ui/v2.5/src/components/Images/Images.tsx b/ui/v2.5/src/components/Images/Images.tsx index c0a6b67c814..91edfdf7985 100644 --- a/ui/v2.5/src/components/Images/Images.tsx +++ b/ui/v2.5/src/components/Images/Images.tsx @@ -4,12 +4,9 @@ import { Helmet } from "react-helmet"; import { useTitleProps } from "src/hooks/title"; import Image from "./ImageDetails/Image"; import { ImageList } from "./ImageList"; -import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { View } from "../List/views"; const Images: React.FC = () => { - useScrollToTopOnMount(); - return ; }; diff --git a/ui/v2.5/src/components/List/CriterionEditor.tsx b/ui/v2.5/src/components/List/CriterionEditor.tsx index 96278560bed..ffc707807ce 100644 --- a/ui/v2.5/src/components/List/CriterionEditor.tsx +++ b/ui/v2.5/src/components/List/CriterionEditor.tsx @@ -61,6 +61,27 @@ const GenericCriterionEditor: React.FC = ({ const { options, modifierOptions } = criterion.criterionOption; + const showModifierSelector = useMemo(() => { + if ( + criterion instanceof PerformersCriterion || + criterion instanceof StudiosCriterion || + criterion instanceof TagsCriterion + ) { + return false; + } + + return modifierOptions && modifierOptions.length > 1; + }, [criterion, modifierOptions]); + + const alwaysShowFilter = useMemo(() => { + return ( + criterion instanceof StashIDCriterion || + criterion instanceof PerformersCriterion || + criterion instanceof StudiosCriterion || + criterion instanceof TagsCriterion + ); + }, [criterion]); + const onChangedModifierSelect = useCallback( (m: CriterionModifier) => { const newCriterion = cloneDeep(criterion); @@ -71,7 +92,7 @@ const GenericCriterionEditor: React.FC = ({ ); const modifierSelector = useMemo(() => { - if (!modifierOptions || modifierOptions.length === 0) { + if (!showModifierSelector) { return; } @@ -90,7 +111,13 @@ const GenericCriterionEditor: React.FC = ({ ))} ); - }, [modifierOptions, onChangedModifierSelect, criterion.modifier, intl]); + }, [ + showModifierSelector, + modifierOptions, + onChangedModifierSelect, + criterion.modifier, + intl, + ]); const valueControl = useMemo(() => { function onValueChanged(value: CriterionValue) { @@ -108,8 +135,9 @@ const GenericCriterionEditor: React.FC = ({ // Hide the value select if the modifier is "IsNull" or "NotNull" if ( - criterion.modifier === CriterionModifier.IsNull || - criterion.modifier === CriterionModifier.NotNull + !alwaysShowFilter && + (criterion.modifier === CriterionModifier.IsNull || + criterion.modifier === CriterionModifier.NotNull) ) { return; } @@ -229,7 +257,7 @@ const GenericCriterionEditor: React.FC = ({ return ( ); - }, [criterion, setCriterion, options]); + }, [criterion, setCriterion, options, alwaysShowFilter]); return (
diff --git a/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx b/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx index 8d96a8a8d42..c51c8a2d5f6 100644 --- a/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx @@ -70,7 +70,7 @@ export const HierarchicalLabelValueFilter: React.FC< if (inputType === "studios") { id = "include_sub_studios"; } else if (inputType === "groups") { - id = "include-sub-groups"; + id = "include_sub_groups"; } else if (type === "children") { id = "include_parent_tags"; } else { diff --git a/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx b/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx index e6f8f9fcf98..a53dc6effc5 100644 --- a/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx @@ -24,19 +24,23 @@ import { CriterionModifier } from "src/core/generated-graphql"; import { keyboardClickHandler } from "src/utils/keyboard"; import { useDebounce } from "src/hooks/debounce"; import useFocus from "src/utils/focus"; +import cx from "classnames"; import ScreenUtils from "src/utils/screen"; import { NumberField } from "src/utils/form"; interface ISelectedItem { - item: ILabeledId; + label: string; excluded?: boolean; onClick: () => void; + // true if the object is a special modifier value + modifier?: boolean; } const SelectedItem: React.FC = ({ - item, + label, excluded = false, onClick, + modifier = false, }) => { const iconClassName = excluded ? "exclude-icon" : "include-button"; const spanClassName = excluded @@ -61,21 +65,66 @@ const SelectedItem: React.FC = ({ } return ( - onClick()} - onKeyDown={keyboardClickHandler(onClick)} - onMouseEnter={() => onMouseOver()} - onMouseLeave={() => onMouseOut()} - onFocus={() => onMouseOver()} - onBlur={() => onMouseOut()} - tabIndex={0} - > -
- - {item.label} -
-
-
+
  • + onClick()} + onKeyDown={keyboardClickHandler(onClick)} + onMouseEnter={() => onMouseOver()} + onMouseLeave={() => onMouseOut()} + onFocus={() => onMouseOver()} + onBlur={() => onMouseOut()} + tabIndex={0} + > +
    + + {label} +
    +
    +
    +
  • + ); +}; + +const UnselectedItem: React.FC<{ + onSelect: (exclude: boolean) => void; + label: string; + canExclude: boolean; + // true if the object is a special modifier value + modifier?: boolean; +}> = ({ onSelect, label, canExclude, modifier = false }) => { + const includeIcon = ; + const excludeIcon = ; + + return ( +
  • + onSelect(false)} + onKeyDown={keyboardClickHandler(() => onSelect(false))} + tabIndex={0} + > +
    + {includeIcon} + {label} +
    +
    + {/* TODO item count */} + {/* {p.id} */} + {canExclude && ( + + )} +
    +
    +
  • ); }; @@ -83,6 +132,7 @@ interface ISelectableFilter { query: string; onQueryChange: (query: string) => void; modifier: CriterionModifier; + showModifierValues: boolean; inputFocus: ReturnType; canExclude: boolean; queryResults: ILabeledId[]; @@ -90,12 +140,31 @@ interface ISelectableFilter { excluded: ILabeledId[]; onSelect: (value: ILabeledId, exclude: boolean) => void; onUnselect: (value: ILabeledId) => void; + onSetModifier: (modifier: CriterionModifier) => void; + // true if the filter is for a single value + singleValue?: boolean; +} + +type SpecialValue = "any" | "none" | "any_of" | "only"; + +function modifierValueToModifier(key: SpecialValue): CriterionModifier { + switch (key) { + case "any": + return CriterionModifier.NotNull; + case "none": + return CriterionModifier.IsNull; + case "any_of": + return CriterionModifier.Includes; + case "only": + return CriterionModifier.Equals; + } } const SelectableFilter: React.FC = ({ query, onQueryChange, modifier, + showModifierValues, inputFocus, canExclude, queryResults, @@ -103,23 +172,73 @@ const SelectableFilter: React.FC = ({ excluded, onSelect, onUnselect, + onSetModifier, + singleValue, }) => { const intl = useIntl(); const objects = useMemo(() => { + if ( + modifier === CriterionModifier.IsNull || + modifier === CriterionModifier.NotNull + ) { + return []; + } return queryResults.filter( (p) => selected.find((s) => s.id === p.id) === undefined && excluded.find((s) => s.id === p.id) === undefined ); - }, [queryResults, selected, excluded]); + }, [modifier, queryResults, selected, excluded]); const includingOnly = modifier == CriterionModifier.Equals; const excludingOnly = modifier == CriterionModifier.Excludes || modifier == CriterionModifier.NotEquals; - const includeIcon = ; - const excludeIcon = ; + const modifierValues = useMemo(() => { + return { + any: modifier === CriterionModifier.NotNull, + none: modifier === CriterionModifier.IsNull, + any_of: !singleValue && modifier === CriterionModifier.Includes, + only: !singleValue && modifier === CriterionModifier.Equals, + }; + }, [modifier, singleValue]); + + const defaultModifier = useMemo(() => { + if (singleValue) { + return CriterionModifier.Includes; + } + return CriterionModifier.IncludesAll; + }, [singleValue]); + + const availableModifierValues: Record = useMemo(() => { + return { + any: + modifier === defaultModifier && + selected.length === 0 && + excluded.length === 0, + none: + modifier === defaultModifier && + selected.length === 0 && + excluded.length === 0, + any_of: + !singleValue && modifier === defaultModifier && selected.length > 1, + only: + !singleValue && + modifier === defaultModifier && + selected.length > 0 && + excluded.length === 0, + }; + }, [singleValue, defaultModifier, modifier, selected, excluded]); + + function onModifierValueSelect(key: SpecialValue) { + const m = modifierValueToModifier(key); + onSetModifier(m); + } + + function onModifierValueUnselect() { + onSetModifier(defaultModifier); + } return (
    @@ -130,50 +249,67 @@ const SelectableFilter: React.FC = ({ placeholder={`${intl.formatMessage({ id: "actions.search" })}…`} />
    @@ -184,6 +320,7 @@ interface IObjectsFilter> { criterion: T; setCriterion: (criterion: T) => void; useResults: (query: string) => { results: ILabeledId[]; loading: boolean }; + singleValue?: boolean; } export const ObjectsFilter = < @@ -192,6 +329,7 @@ export const ObjectsFilter = < criterion, setCriterion, useResults, + singleValue, }: IObjectsFilter) => { const [query, setQuery] = useState(""); const [displayQuery, setDisplayQuery] = useState(query); @@ -264,6 +402,15 @@ export const ObjectsFilter = < [criterion, setCriterion, setInputFocus] ); + const onSetModifier = useCallback( + (modifier: CriterionModifier) => { + let newCriterion: T = criterion.clone(); + newCriterion.modifier = modifier; + setCriterion(newCriterion); + }, + [criterion, setCriterion] + ); + const sortedSelected = useMemo(() => { const ret = criterion.value.items.slice(); ret.sort((a, b) => a.label.localeCompare(b.label)); @@ -288,6 +435,7 @@ export const ObjectsFilter = < query={displayQuery} onQueryChange={onQueryChange} modifier={criterion.modifier} + showModifierValues={!query} inputFocus={inputFocus} canExclude={canExclude} selected={sortedSelected} @@ -295,6 +443,8 @@ export const ObjectsFilter = < onSelect={onSelect} onUnselect={onUnselect} excluded={sortedExcluded} + onSetModifier={onSetModifier} + singleValue={singleValue} /> ); }; @@ -347,18 +497,18 @@ export const HierarchicalObjectsFilter = < return (
    - {criterion.modifier !== CriterionModifier.Equals && ( - - - onDepthChanged(criterion.value.depth !== 0 ? 0 : -1) - } - /> - - )} + + onDepthChanged(criterion.value.depth !== 0 ? 0 : -1)} + disabled={criterion.modifier === CriterionModifier.Equals} + /> + {criterion.value.depth !== 0 && ( diff --git a/ui/v2.5/src/components/List/Filters/StudiosFilter.tsx b/ui/v2.5/src/components/List/Filters/StudiosFilter.tsx index a99fdde3a54..50765847476 100644 --- a/ui/v2.5/src/components/List/Filters/StudiosFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/StudiosFilter.tsx @@ -45,6 +45,7 @@ const StudiosFilter: React.FC = ({ criterion={criterion} setCriterion={setCriterion} useResults={useStudioQuery} + singleValue /> ); }; diff --git a/ui/v2.5/src/components/List/ItemList.tsx b/ui/v2.5/src/components/List/ItemList.tsx index 0efe3b03da9..05639e8fb81 100644 --- a/ui/v2.5/src/components/List/ItemList.tsx +++ b/ui/v2.5/src/components/List/ItemList.tsx @@ -1,6 +1,7 @@ import React, { PropsWithChildren, useCallback, + useContext, useEffect, useMemo, useState, @@ -36,6 +37,7 @@ import { IItemListOperation, } from "./FilteredListToolbar"; import { PagedList } from "./PagedList"; +import { ConfigurationContext } from "src/hooks/Config"; interface IItemListProps { view?: View; @@ -304,18 +306,20 @@ export const ItemListContext = ( children, } = props; + const { configuration: config } = useContext(ConfigurationContext); + const emptyFilter = useMemo( () => providedDefaultFilter?.clone() ?? - new ListFilterModel(filterMode, undefined, { + new ListFilterModel(filterMode, config, { defaultSortBy: defaultSort, }), - [filterMode, defaultSort, providedDefaultFilter] + [config, filterMode, defaultSort, providedDefaultFilter] ); const [filter, setFilterState] = useState( () => - new ListFilterModel(filterMode, undefined, { defaultSortBy: defaultSort }) + new ListFilterModel(filterMode, config, { defaultSortBy: defaultSort }) ); const { defaultFilter, loading: defaultFilterLoading } = useDefaultFilter( diff --git a/ui/v2.5/src/components/List/styles.scss b/ui/v2.5/src/components/List/styles.scss index 9f9519b351f..f234e751126 100644 --- a/ui/v2.5/src/components/List/styles.scss +++ b/ui/v2.5/src/components/List/styles.scss @@ -303,6 +303,15 @@ input[type="range"].zoom-slider { padding-bottom: 0.15rem; padding-inline-start: 0; + .modifier-object { + font-style: italic; + + .selected-object-label, + .unselected-object-label { + opacity: 0.6; + } + } + .unselected-object { opacity: 0.8; } diff --git a/ui/v2.5/src/components/List/util.ts b/ui/v2.5/src/components/List/util.ts index 86aa6c6f67a..329eba289b0 100644 --- a/ui/v2.5/src/components/List/util.ts +++ b/ui/v2.5/src/components/List/util.ts @@ -191,39 +191,60 @@ export function useListKeyboardShortcuts(props: { } export function useListSelect(items: T[]) { - const [selectedIds, setSelectedIds] = useState>(new Set()); + const [itemsSelected, setItemsSelected] = useState([]); const [lastClickedId, setLastClickedId] = useState(); - const prevItems = usePrevious(items); + const selectedIds = useMemo(() => { + const newSelectedIds = new Set(); + itemsSelected.forEach((item) => { + newSelectedIds.add(item.id); + }); - useEffect(() => { - if (prevItems === items) { - return; - } + return newSelectedIds; + }, [itemsSelected]); - // filter out any selectedIds that are no longer in the list - const newSelectedIds = new Set(); + // const prevItems = usePrevious(items); - selectedIds.forEach((id) => { - if (items.some((item) => item.id === id)) { - newSelectedIds.add(id); - } - }); + // #5341 - HACK/TODO: this is a regression of previous behaviour. I don't like the idea + // of keeping selected items that are no longer in the list, since its not + // clear to the user that the item is still selected, but there is now an expectation of + // this behaviour. + // useEffect(() => { + // if (prevItems === items) { + // return; + // } + + // // filter out any selectedIds that are no longer in the list + // const newSelectedIds = new Set(); - setSelectedIds(newSelectedIds); - }, [prevItems, items, selectedIds]); + // selectedIds.forEach((id) => { + // if (items.some((item) => item.id === id)) { + // newSelectedIds.add(id); + // } + // }); + + // setSelectedIds(newSelectedIds); + // }, [prevItems, items, selectedIds]); function singleSelect(id: string, selected: boolean) { setLastClickedId(id); - const newSelectedIds = new Set(selectedIds); - if (selected) { - newSelectedIds.add(id); - } else { - newSelectedIds.delete(id); - } + setItemsSelected((prevSelected) => { + if (selected) { + // prevent duplicates + if (prevSelected.some((v) => v.id === id)) { + return prevSelected; + } - setSelectedIds(newSelectedIds); + const item = items.find((i) => i.id === id); + if (item) { + return [...prevSelected, item]; + } + return prevSelected; + } else { + return prevSelected.filter((item) => item.id !== id); + } + }); } function selectRange(startIndex: number, endIndex: number) { @@ -236,13 +257,12 @@ export function useListSelect(items: T[]) { } const subset = items.slice(start, end + 1); - const newSelectedIds = new Set(); - subset.forEach((item) => { - newSelectedIds.add(item.id); - }); + // prevent duplicates + const toAdd = subset.filter((item) => !selectedIds.has(item.id)); - setSelectedIds(newSelectedIds); + const newSelected = itemsSelected.concat(toAdd); + setItemsSelected(newSelected); } function multiSelect(id: string) { @@ -271,32 +291,19 @@ export function useListSelect(items: T[]) { } function onSelectAll() { - const newSelectedIds = new Set(); - items.forEach((item) => { - newSelectedIds.add(item.id); - }); - - setSelectedIds(newSelectedIds); + // #5341 - HACK/TODO: maintaining legacy behaviour of replacing selected items with + // all items on the current page. To be consistent with the existing behaviour, it + // should probably _add_ all items on the current page to the selected items. + setItemsSelected([...items]); setLastClickedId(undefined); } function onSelectNone() { - const newSelectedIds = new Set(); - setSelectedIds(newSelectedIds); + setItemsSelected([]); setLastClickedId(undefined); } - const getSelected = useMemo(() => { - let cached: T[] | undefined; - return () => { - if (cached) { - return cached; - } - - cached = items.filter((value) => selectedIds.has(value.id)); - return cached; - }; - }, [items, selectedIds]); + const getSelected = useCallback(() => itemsSelected, [itemsSelected]); return { selectedIds, diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index 94e27a3628d..e805c03e621 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -28,7 +28,7 @@ const PerformerDetailGroup: React.FC> = export const PerformerDetailsPanel: React.FC = PatchComponent("PerformerDetailsPanel", (props) => { - const { performer, collapsed, fullWidth } = props; + const { performer, fullWidth } = props; // Network state const intl = useIntl(); @@ -62,45 +62,9 @@ export const PerformerDetailsPanel: React.FC = ); } - function maybeRenderExtraDetails() { - if (!collapsed) { - /* Remove extra urls provided in details since they will be present by perfomr name */ - /* This code can be removed once multple urls are supported for performers */ - let details = performer?.details - ?.replace(/\[((?:http|www\.)[^\n\]]+)\]/gm, "") - .trim(); - return ( - <> - - - - - - - - ); - } - } + let details = performer?.details + ?.replace(/\[((?:http|www\.)[^\n\]]+)\]/gm, "") + .trim(); return ( @@ -190,7 +154,28 @@ export const PerformerDetailsPanel: React.FC = value={performer?.fake_tits} fullWidth={fullWidth} /> - {maybeRenderExtraDetails()} + + + + + + ); }); diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index e7d7a8b41e3..2adcb601e1e 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -282,7 +282,10 @@ export const PerformerEditPanel: React.FC = ({ formik.setFieldValue("penis_length", state.penis_length); } - const remoteSiteID = state.remote_site_id; + updateStashIDs(state.remote_site_id); + } + + function updateStashIDs(remoteSiteID: string | null | undefined) { if (remoteSiteID && (scraper as IStashBox).endpoint) { const newIDs = formik.values.stash_ids?.filter( @@ -291,6 +294,7 @@ export const PerformerEditPanel: React.FC = ({ newIDs?.push({ endpoint: (scraper as IStashBox).endpoint, stash_id: remoteSiteID, + updated_at: new Date().toISOString(), }); formik.setFieldValue("stash_ids", newIDs); } @@ -438,6 +442,7 @@ export const PerformerEditPanel: React.FC = ({ setScraper(undefined); } else { setScrapedPerformer(result); + updateStashIDs(performerResult.remote_site_id); } } diff --git a/ui/v2.5/src/components/Performers/Performers.tsx b/ui/v2.5/src/components/Performers/Performers.tsx index 96c44e938ef..d240ce988b6 100644 --- a/ui/v2.5/src/components/Performers/Performers.tsx +++ b/ui/v2.5/src/components/Performers/Performers.tsx @@ -5,12 +5,9 @@ import { useTitleProps } from "src/hooks/title"; import Performer from "./PerformerDetails/Performer"; import PerformerCreate from "./PerformerDetails/PerformerCreate"; import { PerformerList } from "./PerformerList"; -import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { View } from "../List/views"; const Performers: React.FC = () => { - useScrollToTopOnMount(); - return ; }; diff --git a/ui/v2.5/src/components/Performers/styles.scss b/ui/v2.5/src/components/Performers/styles.scss index c1f891f6b3e..786054d986b 100644 --- a/ui/v2.5/src/components/Performers/styles.scss +++ b/ui/v2.5/src/components/Performers/styles.scss @@ -40,6 +40,21 @@ .alias { font-weight: bold; } + + // the detail element ids are the same as field type name + // which don't follow the correct convention + /* stylelint-disable selector-class-pattern */ + .collapsed { + .detail-item.tattoos, + .detail-item.piercings, + .detail-item.career_length, + .detail-item.details, + .detail-item.tags, + .detail-item.stash_ids { + display: none; + } + } + /* stylelint-enable selector-class-pattern */ } .new-view { diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 897ac870bf3..6858e2cd15b 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -243,7 +243,6 @@ export const ScenePlayer: React.FC = ({ const [fullscreen, setFullscreen] = useState(false); const [showScrubber, setShowScrubber] = useState(false); - const initialTimestamp = useRef(-1); const started = useRef(false); const auto = useRef(false); const interactiveReady = useRef(false); @@ -451,9 +450,11 @@ export const ScenePlayer: React.FC = ({ if (!player) return; function canplay(this: VideoJsPlayer) { - if (initialTimestamp.current !== -1) { - this.currentTime(initialTimestamp.current); - initialTimestamp.current = -1; + // if we're seeking before starting, don't set the initial timestamp + // when starting from the beginning, there is a small delay before the event + // is triggered, so we can't just check if the time is 0 + if (this.currentTime() >= 0.1) { + return; } } @@ -657,7 +658,6 @@ export const ScenePlayer: React.FC = ({ startPosition = resumeTime; } - initialTimestamp.current = startPosition; setTime(startPosition); player.load(); @@ -665,6 +665,10 @@ export const ScenePlayer: React.FC = ({ player.ready(() => { player.vttThumbnails().src(scene.paths.vtt ?? null); + + if (startPosition) { + player.currentTime(startPosition); + } }); started.current = false; @@ -793,7 +797,6 @@ export const ScenePlayer: React.FC = ({ if (started.current) { getPlayer()?.currentTime(seconds); } else { - initialTimestamp.current = seconds; setTime(seconds); } } diff --git a/ui/v2.5/src/components/ScenePlayer/live.ts b/ui/v2.5/src/components/ScenePlayer/live.ts index e55456cb950..2ab51f763e2 100644 --- a/ui/v2.5/src/components/ScenePlayer/live.ts +++ b/ui/v2.5/src/components/ScenePlayer/live.ts @@ -1,3 +1,4 @@ +import { debounce } from "lodash-es"; import videojs, { VideoJsPlayer } from "video.js"; export interface ISource extends videojs.Tech.SourceObject { @@ -10,6 +11,9 @@ interface ICue extends TextTrackCue { _endTime?: number; } +// delay before loading new source after setting currentTime +const loadDelay = 200; + function offsetMiddleware(player: VideoJsPlayer) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- allow access to private tech methods let tech: any; @@ -50,6 +54,34 @@ function offsetMiddleware(player: VideoJsPlayer) { } } + const loadSource = debounce( + (seconds: number) => { + const srcUrl = new URL(source.src); + srcUrl.searchParams.set("start", seconds.toString()); + source.src = srcUrl.toString(); + + const poster = player.poster(); + const playbackRate = tech.playbackRate(); + seeking = tech.paused() ? 1 : 2; + player.poster(""); + tech.setSource(source); + tech.setPlaybackRate(playbackRate); + tech.one("canplay", () => { + player.poster(poster); + if (seeking === 1 || tech.scrubbing()) { + tech.pause(); + } + seeking = 0; + }); + tech.trigger("timeupdate"); + tech.trigger("pause"); + tech.trigger("seeking"); + tech.play(); + }, + loadDelay, + { leading: true } + ); + return { setTech(newTech: videojs.Tech) { tech = newTech; @@ -144,27 +176,7 @@ function offsetMiddleware(player: VideoJsPlayer) { updateOffsetStart(seconds); - const srcUrl = new URL(source.src); - srcUrl.searchParams.set("start", seconds.toString()); - source.src = srcUrl.toString(); - - const poster = player.poster(); - const playbackRate = tech.playbackRate(); - seeking = tech.paused() ? 1 : 2; - player.poster(""); - tech.setSource(source); - tech.setPlaybackRate(playbackRate); - tech.one("canplay", () => { - player.poster(poster); - if (seeking === 1) { - tech.pause(); - } - seeking = 0; - }); - tech.trigger("timeupdate"); - tech.trigger("pause"); - tech.trigger("seeking"); - tech.play(); + loadSource(seconds); return 0; }, diff --git a/ui/v2.5/src/components/ScenePlayer/source-selector.ts b/ui/v2.5/src/components/ScenePlayer/source-selector.ts index 7cf6cfd757d..be1126ff9a6 100644 --- a/ui/v2.5/src/components/ScenePlayer/source-selector.ts +++ b/ui/v2.5/src/components/ScenePlayer/source-selector.ts @@ -196,8 +196,12 @@ class SourceSelectorPlugin extends videojs.getPlugin("plugin") { console.log(`Trying next source in playlist: '${newSource.label}'`); this.menu.setSelectedSource(newSource); + const currentTime = player.currentTime(); player.src(newSource); player.load(); + player.one("canplay", () => { + player.currentTime(currentTime); + }); player.play(); } else { console.log("No more sources in playlist"); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 2eef3de1fb4..fb91cd44925 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -508,6 +508,7 @@ export const SceneEditPanel: React.FC = ({ return { endpoint, stash_id: updatedScene.remote_site_id, + updated_at: new Date().toISOString(), }; } @@ -521,6 +522,7 @@ export const SceneEditPanel: React.FC = ({ formik.values.stash_ids.concat({ endpoint, stash_id: updatedScene.remote_site_id, + updated_at: new Date().toISOString(), }) ); } diff --git a/ui/v2.5/src/components/Scenes/Scenes.tsx b/ui/v2.5/src/components/Scenes/Scenes.tsx index 7e8031ab5a7..a9124fb8fad 100644 --- a/ui/v2.5/src/components/Scenes/Scenes.tsx +++ b/ui/v2.5/src/components/Scenes/Scenes.tsx @@ -3,7 +3,6 @@ import { Route, Switch } from "react-router-dom"; import { Helmet } from "react-helmet"; import { useTitleProps } from "src/hooks/title"; import { lazyComponent } from "src/utils/lazyComponent"; -import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { View } from "../List/views"; const SceneList = lazyComponent(() => import("./SceneList")); @@ -12,14 +11,10 @@ const Scene = lazyComponent(() => import("./SceneDetails/Scene")); const SceneCreate = lazyComponent(() => import("./SceneDetails/SceneCreate")); const Scenes: React.FC = () => { - useScrollToTopOnMount(); - return ; }; const SceneMarkers: React.FC = () => { - useScrollToTopOnMount(); - const titleProps = useTitleProps({ id: "markers" }); return ( <> diff --git a/ui/v2.5/src/components/Shared/GridCard/GridCard.tsx b/ui/v2.5/src/components/Shared/GridCard/GridCard.tsx index 33aa24e32cd..db35f83f923 100644 --- a/ui/v2.5/src/components/Shared/GridCard/GridCard.tsx +++ b/ui/v2.5/src/components/Shared/GridCard/GridCard.tsx @@ -154,6 +154,7 @@ export const GridCard: React.FC = (props: ICardProps) => { if (props.selecting) { props.onSelectedChanged(!props.selected, shiftKey); event.preventDefault(); + event.stopPropagation(); } } diff --git a/ui/v2.5/src/components/Shared/StashID.tsx b/ui/v2.5/src/components/Shared/StashID.tsx index 14bcef6882c..00bddf58edf 100644 --- a/ui/v2.5/src/components/Shared/StashID.tsx +++ b/ui/v2.5/src/components/Shared/StashID.tsx @@ -7,7 +7,7 @@ import { ExternalLink } from "./ExternalLink"; export type LinkType = "performers" | "scenes" | "studios"; export const StashIDPill: React.FC<{ - stashID: StashId; + stashID: Pick; linkType: LinkType; }> = ({ stashID, linkType }) => { const { configuration } = React.useContext(ConfigurationContext); diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx index 5bf877b11f2..81e3897656d 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx @@ -13,7 +13,6 @@ interface IStudioDetailsPanel { export const StudioDetailsPanel: React.FC = ({ studio, - collapsed, fullWidth, }) => { function renderTagsField() { @@ -47,25 +46,6 @@ export const StudioDetailsPanel: React.FC = ({ ); } - function maybeRenderExtraDetails() { - if (!collapsed) { - return ( - <> - - - - ); - } - } - return (
    @@ -82,7 +62,12 @@ export const StudioDetailsPanel: React.FC = ({ } fullWidth={fullWidth} /> - {maybeRenderExtraDetails()} + +
    ); }; diff --git a/ui/v2.5/src/components/Studios/Studios.tsx b/ui/v2.5/src/components/Studios/Studios.tsx index e60dbdc06c2..545de936f52 100644 --- a/ui/v2.5/src/components/Studios/Studios.tsx +++ b/ui/v2.5/src/components/Studios/Studios.tsx @@ -5,12 +5,9 @@ import { useTitleProps } from "src/hooks/title"; import Studio from "./StudioDetails/Studio"; import StudioCreate from "./StudioDetails/StudioCreate"; import { StudioList } from "./StudioList"; -import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { View } from "../List/views"; const Studios: React.FC = () => { - useScrollToTopOnMount(); - return ; }; diff --git a/ui/v2.5/src/components/Studios/styles.scss b/ui/v2.5/src/components/Studios/styles.scss index 9d919f42d94..eaab21d1015 100644 --- a/ui/v2.5/src/components/Studios/styles.scss +++ b/ui/v2.5/src/components/Studios/styles.scss @@ -40,4 +40,14 @@ .rating-number .text-input { width: auto; } + + // the detail element ids are the same as field type name + // which don't follow the correct convention + /* stylelint-disable selector-class-pattern */ + .collapsed { + .detail-item.stash_ids { + display: none; + } + } + /* stylelint-enable selector-class-pattern */ } diff --git a/ui/v2.5/src/components/Tagger/PerformerModal.tsx b/ui/v2.5/src/components/Tagger/PerformerModal.tsx index 37a2009aa76..d4c8bba1636 100755 --- a/ui/v2.5/src/components/Tagger/PerformerModal.tsx +++ b/ui/v2.5/src/components/Tagger/PerformerModal.tsx @@ -272,6 +272,7 @@ const PerformerModal: React.FC = ({ { endpoint, stash_id: remoteSiteID, + updated_at: new Date().toISOString(), }, ]; } diff --git a/ui/v2.5/src/components/Tagger/context.tsx b/ui/v2.5/src/components/Tagger/context.tsx index dc35208c5f2..9b1e996de24 100644 --- a/ui/v2.5/src/components/Tagger/context.tsx +++ b/ui/v2.5/src/components/Tagger/context.tsx @@ -613,12 +613,14 @@ export const TaggerContext: React.FC = ({ children }) => { return { endpoint: e.endpoint, stash_id: e.stash_id, + updated_at: e.updated_at, }; }); stashIDs.push({ stash_id: performer.remote_site_id, endpoint: currentSource?.sourceInput.stash_box_endpoint, + updated_at: new Date().toISOString(), }); await updatePerformer({ @@ -770,12 +772,14 @@ export const TaggerContext: React.FC = ({ children }) => { return { endpoint: e.endpoint, stash_id: e.stash_id, + updated_at: e.updated_at, }; }); stashIDs.push({ stash_id: studio.remote_site_id, endpoint: currentSource?.sourceInput.stash_box_endpoint, + updated_at: new Date().toISOString(), }); await updateStudio({ diff --git a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx index 25f0ca88f73..6b202be54c7 100755 --- a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; @@ -13,24 +13,24 @@ import { } from "src/components/Performers/PerformerSelect"; import { getStashboxBase } from "src/utils/stashbox"; import { ExternalLink } from "src/components/Shared/ExternalLink"; +import { Link } from "react-router-dom"; -interface IPerformerName { +const PerformerLink: React.FC<{ performer: GQL.ScrapedPerformer | Performer; - id: string | undefined | null; - baseURL: string | undefined; -} - -const PerformerName: React.FC = ({ - performer, - id, - baseURL, -}) => { - const name = - baseURL && id ? ( - {performer.name} + url: string | undefined; + internal?: boolean; +}> = ({ performer, url, internal = false }) => { + const name = useMemo(() => { + if (!url) return performer.name; + + return internal ? ( + + {performer.name} + ) : ( - performer.name + {performer.name} ); + }, [url, performer.name, internal]); return ( <> @@ -115,10 +115,9 @@ const PerformerResult: React.FC = ({
    : -
    @@ -134,10 +133,10 @@ const PerformerResult: React.FC = ({ : -
    @@ -169,10 +168,9 @@ const PerformerResult: React.FC = ({
    : -
    diff --git a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx index cc8f6a132e6..4be285907b6 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx @@ -413,6 +413,7 @@ const StashSearchResult: React.FC = ({ return { endpoint: s.endpoint, stash_id: s.stash_id, + updated_at: s.updated_at, }; }) .filter( @@ -421,6 +422,7 @@ const StashSearchResult: React.FC = ({ { endpoint: currentSource.sourceInput.stash_box_endpoint, stash_id: scene.remote_site_id, + updated_at: new Date().toISOString(), }, ]; } else { diff --git a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx index b57c796ab28..249e34e7401 100644 --- a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx @@ -198,11 +198,13 @@ const StudioModal: React.FC = ({ // stashid handling code const remoteSiteID = studio.remote_site_id; + const timeNow = new Date().toISOString(); if (remoteSiteID && endpoint) { studioData.stash_ids = [ { endpoint, stash_id: remoteSiteID, + updated_at: timeNow, }, ]; } @@ -230,6 +232,7 @@ const StudioModal: React.FC = ({ { endpoint, stash_id: parentRemoteSiteID, + updated_at: timeNow, }, ]; } diff --git a/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx index 4d099d2a6e2..13ffe6bff82 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import cx from "classnames"; @@ -12,20 +12,24 @@ import { OptionalField } from "../IncludeButton"; import { faSave } from "@fortawesome/free-solid-svg-icons"; import { getStashboxBase } from "src/utils/stashbox"; import { ExternalLink } from "src/components/Shared/ExternalLink"; +import { Link } from "react-router-dom"; -interface IStudioName { +const StudioLink: React.FC<{ studio: GQL.ScrapedStudio | GQL.SlimStudioDataFragment; - id: string | undefined | null; - baseURL: string | undefined; -} - -const StudioName: React.FC = ({ studio, id, baseURL }) => { - const name = - baseURL && id ? ( - {studio.name} + url: string | undefined; + internal?: boolean; +}> = ({ studio, url, internal = false }) => { + const name = useMemo(() => { + if (!url) return studio.name; + + return internal ? ( + + {studio.name} + ) : ( - studio.name + {studio.name} ); + }, [url, studio.name, internal]); return {name}; }; @@ -82,10 +86,9 @@ const StudioResult: React.FC = ({
    : -
    @@ -101,10 +104,10 @@ const StudioResult: React.FC = ({ : - @@ -136,10 +139,9 @@ const StudioResult: React.FC = ({
    : -
    diff --git a/ui/v2.5/src/components/Tags/Tags.tsx b/ui/v2.5/src/components/Tags/Tags.tsx index 5ed35448e7f..806a0f7a6a5 100644 --- a/ui/v2.5/src/components/Tags/Tags.tsx +++ b/ui/v2.5/src/components/Tags/Tags.tsx @@ -5,11 +5,8 @@ import { useTitleProps } from "src/hooks/title"; import Tag from "./TagDetails/Tag"; import TagCreate from "./TagDetails/TagCreate"; import { TagList } from "./TagList"; -import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; const Tags: React.FC = () => { - useScrollToTopOnMount(); - return ; }; diff --git a/ui/v2.5/src/docs/en/Changelog/v0270.md b/ui/v2.5/src/docs/en/Changelog/v0270.md index cb8751e75af..24ebd6fbe26 100644 --- a/ui/v2.5/src/docs/en/Changelog/v0270.md +++ b/ui/v2.5/src/docs/en/Changelog/v0270.md @@ -16,6 +16,9 @@ * Added option to rescan all files in the Scan task. ([#5254](https://github.com/stashapp/stash/pull/5254)) ### 🎨 Improvements +* **[0.27.2]** Scene player now shows the starting position when resume time is set. ([#5379](https://github.com/stashapp/stash/pull/5379)) +* **[0.27.1]** Live transcode requests are now debounced to spawn fewer `ffmpeg` instances while scrubbing. ([#5340](https://github.com/stashapp/stash/pull/5340)) +* **[0.27.1]** Blobs location may now be set using environment variable `STASH_BLOBS`. ([#5345](https://github.com/stashapp/stash/pull/5345)) * Added button to view sub-studio/sub-tag content on Studio/Tag details pages. ([#5080](https://github.com/stashapp/stash/pull/5080)) * Made tagger settings persistent. ([#5165](https://github.com/stashapp/stash/pull/5165)) * Added birthdate and age to Performer select. ([#5076](https://github.com/stashapp/stash/pull/5076)) @@ -29,7 +32,24 @@ * Scene Player now allows interacting with the controls before playing video, and errors no longer prevent interacting with the Scene Player. ([#5145](https://github.com/stashapp/stash/pull/5145)) ### 🐛 Bug fixes -* **[0.27.1]** Fixed dropdowns not displaying correctly in the merge dialogs. +* **[0.27.2]** Fixed items being selected twice when selecting items in the Grid list. ([#5377](https://github.com/stashapp/stash/pull/5377)) +* **[0.27.2]** Fixed 62 migration error for some users. ([#5363](https://github.com/stashapp/stash/pull/5363)) +* **[0.27.2]** Fixed scenes incorrectly autoplaying on queue selection. ([#5379](https://github.com/stashapp/stash/pull/5379)) +* **[0.27.2]** Videos no longer begin playing when seeking before video has started. ([#5379](https://github.com/stashapp/stash/pull/5379)) +* **[0.27.2]** Videos will now resume from the correct time when switching sources due to error. ([#5379](https://github.com/stashapp/stash/pull/5379)) +* **[0.27.1]** Fixed UI infinite loop when sorting by random without a seed in the URL. ([#5319](https://github.com/stashapp/stash/pull/5319)) +* **[0.27.1]** Fixed dropdowns not displaying correctly in the merge dialogs. ([#5299](https://github.com/stashapp/stash/pull/5299)) +* **[0.27.1]** For single URLs, link icon now shows the dropdown menu instead of navigating to the URL. ([#5310](https://github.com/stashapp/stash/pull/5310)) +* **[0.27.1]** Fixed redirection when page > total pages to the last page instead of the first. ([#5321](https://github.com/stashapp/stash/pull/5321)) +* **[0.27.1]** Fixed display of rating criterion when using decimal rating system. ([#5334](https://github.com/stashapp/stash/pull/5334)) +* **[0.27.1]** Fixed parent/child Tags not showing in alphabetical order. ([#5320](https://github.com/stashapp/stash/pull/5320)) +* **[0.27.1]** Fixed performance issue when viewing studios where system has many images with no studios. ([#5335](https://github.com/stashapp/stash/pull/5335)) +* **[0.27.1]** Clicking on the video player timeline before video is started now plays the video from that point instead of playing from the beginning. ([#5340](https://github.com/stashapp/stash/pull/5340)) +* **[0.27.1]** Fixed UI crash when front page has filters using legacy `movies` scene filter. ([#5348](https://github.com/stashapp/stash/pull/5348)) +* **[0.27.1]** Restored legacy behaviour where selection is persisted when paging or changing filter. ([#5349](https://github.com/stashapp/stash/pull/5349)) +* **[0.27.1]** Fixed UI crash when navigating to image without files. ([#5325](https://github.com/stashapp/stash/pull/5325)) +* **[0.27.1]** Fixed panic when deleting image without files. ([#5328](https://github.com/stashapp/stash/pull/5328)) +* **[0.27.1]** Fixed matched performer and studio links not including base URL in Tagger. ([#5337](https://github.com/stashapp/stash/pull/5337)) * Fixed videos and images having incorrect dimensions when the orientation flag is set to a non-default value during scan. ([#5188](https://github.com/stashapp/stash/pull/5188), [#5189](https://github.com/stashapp/stash/pull/5189)) * Fixed mp4 videos being incorrectly transcoded when the file has opus audio codec. ([#5030](https://github.com/stashapp/stash/pull/5030)) * Fixed o-history being imported as view-history when importing from JSON. ([#5127](https://github.com/stashapp/stash/pull/5127)) diff --git a/ui/v2.5/src/locales/cs-CZ.json b/ui/v2.5/src/locales/cs-CZ.json index f2617b0f428..1384be8657b 100644 --- a/ui/v2.5/src/locales/cs-CZ.json +++ b/ui/v2.5/src/locales/cs-CZ.json @@ -135,7 +135,13 @@ "remove_date": "Odstranit datum", "add_manual_date": "Přidat datum ručně", "add_play": "Přidat přehrávání", - "view_history": "Zobrazit historii" + "view_history": "Zobrazit historii", + "reset_cover": "Obnovit výchozí obal", + "add_sub_groups": "Přidat podskupinu", + "set_cover": "Nastavit jako obal", + "remove_from_containing_group": "Odebrat ze skupiny", + "reset_resume_time": "Obnovit čas pokračování", + "reset_play_duration": "Obnovit dobu přehrávání" }, "actions_name": "Akce", "age": "Věk", @@ -531,7 +537,9 @@ "anonymise_database": "Vytvoří kopii databáze do adresáře záloh, anonymizuje všechna citlivá data. To pak může být poskytnuto ostatním pro účely odstraňování problémů a debuggování. Původní databáze se nemění. Anonymizovaná databáze používá formát názvu souboru {filename_format}.", "optimise_database": "Pokusit se zlepšit výkon analyzováním a opětovným sestavením celého databázového souboru.", "generate_clip_previews_during_scan": "Generování náhledů pro obrázkové klipy", - "optimise_database_warning": "Varování: Pokud je tato úloha spuštěna, všechny operace, které upravují databázi, selžou a v závislosti na velikosti databáze může její dokončení trvat několik minut. Vyžaduje také minimálně tolik volného místa na disku, jak je vaše databáze velká, ale doporučuje se 1,5x." + "optimise_database_warning": "Varování: Pokud je tato úloha spuštěna, všechny operace, které upravují databázi, selžou a v závislosti na velikosti databáze může její dokončení trvat několik minut. Vyžaduje také minimálně tolik volného místa na disku, jak je vaše databáze velká, ale doporučuje se 1,5x.", + "rescan": "Znovu skenovat soubory", + "rescan_tooltip": "Znovu skenovat každý soubor v cestě. Používá se k vynucení aktualizace metadat souboru a opětovnému skenování zip souborů." }, "tools": { "scene_duplicate_checker": "Kontrola na duplikaci scén", @@ -787,7 +795,8 @@ "performers": "{count, plural, one {Účinkující} other {Účinkující}}", "scenes": "{count, plural, one {Scéna} other {Scény}}", "studios": "{count, plural, one {Studio} other {Studia}}", - "tags": "{count, plural, one {Tag} other {Tagy}}" + "tags": "{count, plural, one {Tag} other {Tagy}}", + "groups": "{count, plural, one {Skupina} other {Skupiny}}" }, "country": "Země", "cover_image": "Obrázek obalu", @@ -1215,7 +1224,9 @@ "lazy_component_error_help": "Pokud jste nedávno aktualizovali Stash, znovu načtěte stránku nebo vymažte mezipaměť prohlížeče.", "something_went_wrong": "Něco se pokazilo.", "header": "Chyba", - "loading_type": "Chyba při načítání {type}" + "loading_type": "Chyba při načítání {type}", + "invalid_javascript_string": "Neplatný kód javascriptu : {error}", + "invalid_json_string": "Neplatný string JSON: {error}" }, "eye_color": "Barva očí", "fake_tits": "Falešná prsa", @@ -1275,7 +1286,8 @@ "last": "Poslední", "next": "Další", "previous": "Předchozí", - "first": "První" + "first": "První", + "current_total": "{current} z {total}" }, "parent_tag_count": "Počet nadřazených tagů", "parent_tags": "Nadřazené tagy", @@ -1461,5 +1473,23 @@ "years_old": "Let", "updated_at": "Aktualizováno", "stash_id": "Stash ID", - "sub_tag_of": "Počet sub-tagů od {parent}" + "sub_tag_of": "Počet sub-tagů od {parent}", + "containing_group": "Obsahující skupina", + "containing_group_count": "Počet obsahujících skupin", + "include_sub_group_content": "Zahrnout obsah podskupiny", + "group": "Skupina", + "group_count": "Počet skupin", + "group_scene_number": "Číslo scény", + "studio_count": "Počet studií", + "studio_tags": "Tagy studií", + "containing_groups": "Obsahující skupiny", + "groups": "Skupiny", + "sub_group_of": "Podskupina {parent}", + "sub_group_order": "Pořadí podskupin", + "sub_groups": "Podskupiny", + "sub_group": "Podskupina", + "sub_group_count": "Počet podskupin", + "include_sub_studio_content": "Zahrnout obsah podstudií", + "include_sub_tag_content": "Zahrnout obsah podtagů", + "include_sub_groups": "Zahrnout podskupiny" } diff --git a/ui/v2.5/src/locales/de-DE.json b/ui/v2.5/src/locales/de-DE.json index 40aca3495f9..4cc7b95f894 100644 --- a/ui/v2.5/src/locales/de-DE.json +++ b/ui/v2.5/src/locales/de-DE.json @@ -545,7 +545,8 @@ "transcodes": "Transkodierte Szenen", "sprites": "Sprites der Szenen" }, - "generate_sprites_during_scan_tooltip": "Die Anzahl an Bilder, die unter dem Video Player, zur einfacheren Navigation, angezeigt werden." + "generate_sprites_during_scan_tooltip": "Die Anzahl an Bilder, die unter dem Video Player, zur einfacheren Navigation, angezeigt werden.", + "optimise_database_warning": "Achtung: Während diese Aufgabe ausgeführt wird, schlagen alle Operationen, die die Datenbank verändern, fehl, und je nach Größe Ihrer Datenbank kann es mehrere Minuten dauern, bis sie abgeschlossen ist. Außerdem wird mindestens so viel freier Speicherplatz benötigt, wie Ihre Datenbank groß ist, empfohlen wird jedoch das 1,5-fache." }, "tools": { "scene_duplicate_checker": "Duplikatsprüfung für Szenen", diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 74073d1ccfb..6be0a654240 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -844,6 +844,12 @@ "not_matches_regex": "not matches regex", "not_null": "is not null" }, + "criterion_modifier_values": { + "any": "Any", + "any_of": "Any of", + "none": "None", + "only": "Only" + }, "custom": "Custom", "date": "Date", "date_format": "YYYY-MM-DD", @@ -1098,6 +1104,7 @@ "images": "Images", "include_parent_tags": "Include parent tags", "include_sub_group_content": "Include sub-group content", + "include_sub_groups": "Include sub-groups", "include_sub_studio_content": "Include sub-studio content", "include_sub_studios": "Include subsidiary studios", "include_sub_tag_content": "Include sub-tag content", diff --git a/ui/v2.5/src/locales/fr-FR.json b/ui/v2.5/src/locales/fr-FR.json index 5d80067522a..d930b93f28f 100644 --- a/ui/v2.5/src/locales/fr-FR.json +++ b/ui/v2.5/src/locales/fr-FR.json @@ -1490,5 +1490,6 @@ "sub_groups": "Groupes affiliés", "sub_group_of": "Groupe affilié de {parent}", "sub_group": "Groupe affilié", - "sub_group_count": "Nombre de groupes affiliés" + "sub_group_count": "Nombre de groupes affiliés", + "include_sub_groups": "Inclure les groupes affiliés" } diff --git a/ui/v2.5/src/locales/hu-HU.json b/ui/v2.5/src/locales/hu-HU.json index d4909ace6f2..be47f77a978 100644 --- a/ui/v2.5/src/locales/hu-HU.json +++ b/ui/v2.5/src/locales/hu-HU.json @@ -126,7 +126,14 @@ "view_random": "Véletlenszerű megtekintés", "enable": "Engedélyezés", "open_in_external_player": "Megnyitás külső lejátszóban", - "reload": "Újratöltés" + "reload": "Újratöltés", + "add_sub_groups": "Al kategóriák hozzá adása", + "add_manual_date": "Dátum Manuális hozzá adása", + "add_o": "O hozzá adása", + "choose_date": "Válassz egy dátumot", + "clean_generated": "Generált fájlok takarítása", + "clear_date_data": "Dátum adatok törlése", + "copy_to_clipboard": "Vágólapra másolás" }, "age": "Kor", "aliases": "Álnevek", @@ -398,7 +405,8 @@ }, "scene_gen": { "preview_exclude_end_time_desc": "Az utolsó x másodperc kihagyása a jelenetek bemutatóiból. Ez az érték lehet másodpercben, vagy a jelenet teljes hosszának százalékában (pl. 2%) is megadva.", - "preview_exclude_start_time_desc": "Az első x másodperc kihagyása a jelenetek bemutatóiból. Ez az érték lehet másodpercben, vagy a jelenet teljes hosszának százalékában (pl. 2%) is megadva." + "preview_exclude_start_time_desc": "Az első x másodperc kihagyása a jelenetek bemutatóiból. Ez az érték lehet másodpercben, vagy a jelenet teljes hosszának százalékában (pl. 2%) is megadva.", + "video_previews": "Előnézetek" }, "scrape_results_existing": "Létező", "set_image_url_title": "Kép URL" @@ -420,7 +428,10 @@ "medium": "Közepes" }, "search_accuracy_label": "Keresési Pontosság", - "title": "Megkettőzött Jelenetek" + "title": "Megkettőzött Jelenetek", + "duration_options": { + "any": "Akármelyik" + } }, "duplicated_phash": "Megkettőzőtt (phash)", "duration": "Hossz", @@ -516,7 +527,8 @@ "network_error": "Hálózati Hiba", "tag_status": "Címke Státusza", "update_performer": "Szereplő Frissítése", - "update_performers": "Szereplők Frissítése" + "update_performers": "Szereplők Frissítése", + "name_already_exists": "A név már létezik" }, "performer_tags": "Szereplő Címkék", "performers": "Szereplők", @@ -543,7 +555,8 @@ "nearly_there": "Már majdnem kész!" }, "errors": { - "something_went_wrong_while_setting_up_your_system": "Valami hiba történt a rendszer beállításakor. Itt a hibaüzenet: {error}" + "something_went_wrong_while_setting_up_your_system": "Valami hiba történt a rendszer beállításakor. Itt a hibaüzenet: {error}", + "something_went_wrong": "Valami elromlott" }, "folder": { "file_path": "Fájl elérési út" @@ -554,18 +567,23 @@ "migration_failed": "Sikertelen áttelepítés", "migration_irreversible_warning": "A séma áttelepítése nem visszafordítható folyamat. Amint az áttelepítés elkezdődik, az adatbázis összeegyeztethetetlen lesz a Stash előző verzióival.", "migration_required": "Áttelepítés szükséges", - "schema_too_old": "A jelenlegi Stash adatbázis verziója {databaseSchema} , amit át kell telepíteni {appSchema} verzióra. A Stash ezen verziója nem fog működni az adatbázis áttelepítése nélkül." + "schema_too_old": "A jelenlegi Stash adatbázis verziója {databaseSchema} , amit át kell telepíteni {appSchema} verzióra. A Stash ezen verziója nem fog működni az adatbázis áttelepítése nélkül.", + "perform_schema_migration": "sémamigráció végre hajtása" }, "success": { "help_links": "Ha problémába ütközöl, kérdésed, vagy javaslatod van, nyugodtan jelezd {githubLink}, vagy kérdezd meg a közösségtől {discordLink}.", - "support_us": "Támogatás" + "support_us": "Támogass minket" }, "welcome": { "config_path_logic_explained": "A Stash elöszőr a jelenlegi munkakönyvtárban próbálja a konfigurációs fájlját (config.yml) megkeresni. Ha ott nem találja, akkor a következő helyen próbálkozik: $HOME/.stash/config.yml (Windows rendszeren: %USERPROFILE%\\.stash\\config.yml). Meghatározhatja hogy a Stash egy bizonyos konfigurációs fájlt használjon, ammennyiben a -c '' or --config '' paraméterekkel indítja.", "unexpected_explained": "Amennyiben ez a képernyő váratlanul bukkant fel, próbáld újraindítani a Stasht a megfelelő munkakönyvtárban, vagy a -c flag-gel." }, "welcome_specific_config": { - "unable_to_locate_specified_config": "Ha ezt olvasod, akkor a Stash nem találta meg a konfigurációs fájlt, amit megadtál a parancssorban. Ez a varázsló végigvezet a lépéseken, hogy új konfigurációs fájlt tudj beállítani." + "unable_to_locate_specified_config": "Ha ezt olvasod, akkor a Stash nem találta meg a konfigurációs fájlt, amit megadtál a parancssorban. Ez a varázsló végigvezet a lépéseken, hogy új konfigurációs fájlt tudj beállítani.", + "config_path": "A Stash a következő konfigurációs fájl útvonalat fogja használni: {path}" + }, + "paths": { + "where_is_your_porn_located": "Hol található a pornód?" } }, "stashbox": { @@ -591,7 +609,10 @@ "created_entity": "{entity} Létrehozva", "merged_tags": "Összevont címkék", "saved_entity": "{entity} Mentve", - "updated_entity": "{entity} Frissítve" + "updated_entity": "{entity} Frissítve", + "started_generating": "Generálás megkezdve", + "started_importing": "Importálás megkezdve", + "default_filter_set": "Alapvető szűrő beállítva" }, "total": "Összesen", "true": "Igaz", @@ -617,5 +638,33 @@ }, "captions": "Feliratok", "chapters": "Fejezetek", - "circumcised": "Körülmetélt" + "circumcised": "Körülmetélt", + "weight_kg": "Súly (kg)", + "sub_tags": "Al címkék", + "sub_group": "Al csoport", + "unknown_date": "Ismeretlen dátum", + "urls": "URL-ek", + "validation": { + "blank": "${path} nem lehet üres" + }, + "studio_tags": "Stúdió címkék", + "type": "Típus", + "measurements": "Mérések", + "package_manager": { + "install": "Letöltés", + "version": "Verzió" + }, + "folder": "mappa", + "second": "Második", + "studio_tagger": { + "current_page": "Jelenlegi oldal", + "network_error": "Hálózati hiba", + "tag_status": "Címke státusz" + }, + "history": "Történet", + "sub_groups": "Al csoportok", + "sub_group_count": "Al csoportok száma", + "primary_file": "Elsődleges fájl", + "groups": "Csoportok", + "penis_length_cm": "Pénisz hosszúság (cm)" } diff --git a/ui/v2.5/src/locales/ko-KR.json b/ui/v2.5/src/locales/ko-KR.json index 524a35ab364..fb32e6f403c 100644 --- a/ui/v2.5/src/locales/ko-KR.json +++ b/ui/v2.5/src/locales/ko-KR.json @@ -1127,7 +1127,7 @@ "last": "마지막", "next": "다음", "previous": "이전", - "current_total": "{total} 중 {current}" + "current_total": "{current} / {total}" }, "parent_of": "{children}의 상위 태그", "parent_studios": "모회사 스튜디오", @@ -1477,16 +1477,16 @@ "group": "그룹", "group_count": "그룹 개수", "group_scene_number": "영상 번호", - "groups": "그룹들", + "groups": "그룹", "studio_tags": "스튜디오 태그", - "containing_groups": "그룹들 포함", + "containing_groups": "그룹 포함", "containing_group_count": "그룹 개수 포함", "studio_count": "스튜디오 개수", "containing_group": "그룹 포함", "include_sub_group_content": "서브그룹 컨텐츠 포함", "sub_group_count": "서브그룹 개수", "sub_group_order": "서브그룹 순서", - "sub_groups": "서브그룹들", + "sub_groups": "서브그룹", "sub_group": "서브그룹", "sub_group_of": "{parent}의 서브그룹", "include_sub_studio_content": "서브스튜디오 컨텐츠 포함", diff --git a/ui/v2.5/src/locales/nb_NO.json b/ui/v2.5/src/locales/nb_NO.json index cfc26530200..a6a72d8a049 100644 --- a/ui/v2.5/src/locales/nb_NO.json +++ b/ui/v2.5/src/locales/nb_NO.json @@ -4,6 +4,250 @@ "anonymise": "Anonymiser", "confirm": "Bekreft", "continue": "Fortsett", - "close": "Lukk" - } + "close": "Lukk", + "reset_cover": "Tilbakestill Standard Omslag", + "remove": "Fjern", + "running": "kjører", + "submit_stash_box": "Send til Stash-Box", + "delete_generated_supporting_files": "Slett genererte støttende filer", + "select_entity": "Velg {entityType}", + "copy_to_clipboard": "Kopier til utklippstavle", + "delete_file_and_funscript": "Slett fil (og funscript)", + "clear_front_image": "Fjern front bilde", + "next_action": "Neste", + "tasks": { + "clean_confirm_message": "Er du sikker du vil Rydde opp? Dette vil slette databaseinformasjon og generert innhold for alle scener og gallerier som ikke lenger finnes i filsystemet.", + "dry_mode_selected": "Tørrmodus valgt. Ingen faktisk sletting vil finne sted, kun logging.", + "import_warning": "Er du sikker på at du vil importere? Dette vil slette databasen og re-importere fra dine eksporterte metadata." + }, + "generate_thumb_default": "Generer standard miniatyrbilde", + "merge_into": "Slå sammen til", + "scrape_query": "Skrape forespørsel", + "reset_play_duration": "Tilbakestill avspillingsvarigheten", + "reset_resume_time": "Tilbakestill gjenoppta tid", + "save": "Lagre", + "save_delete_settings": "Bruk disse alternativene som standard når du sletter", + "save_filter": "Lagre filter", + "scan": "Skann", + "scrape": "Skrape", + "create": "Opprett", + "create_chapters": "Opprett Kapittel", + "create_marker": "Opprett Markør", + "delete": "Slett", + "delete_file": "Slett fil", + "disable": "Deaktiver", + "download": "Last ned", + "download_backup": "Last ned Sikkerhetskopi", + "edit": "Rediger", + "edit_entity": "Rediger {entityType}", + "enable": "Aktiver", + "export": "Eksporter", + "find": "Finn", + "finish": "Fullfør", + "from_file": "Fra fil…", + "from_url": "Fra URL…", + "generate": "Generer", + "generate_thumb_from_current": "Generer miniatyrbilde fra nåværende", + "hide": "Skjul", + "hide_configuration": "Skjul Konfigurasjon", + "identify": "Identifiser", + "ignore": "Ignorer", + "import": "Importer…", + "add_sub_groups": "Legg til undergrupper", + "create_entity": "Opprett {entityType}", + "delete_entity": "Slett {entityType}", + "encoding_image": "Omsetter bilde til kode…", + "merge": "Slå sammen", + "created_entity": "Opprettet {entity_type}: {entity_name}", + "merge_from": "Slå sammen fra", + "clean_generated": "Rydd opp i genererte filer", + "clear": "Tøm", + "clear_back_image": "Fjern bakbilde", + "clear_date_data": "Fjern dato data", + "clear_image": "Fjern Bilde", + "create_parent_studio": "Opprett foreldre studio", + "customise": "Tilpass", + "disallow": "Ikke tillat", + "download_anonymised": "Last ned anonymisert", + "export_all": "Eksporter alle…", + "full_export": "Full eksport", + "full_import": "Full Import", + "hash_migration": "hash migrering", + "make_primary": "Gjør til Primær", + "previous_action": "Tilbake", + "refresh": "Oppdater", + "reload": "Last på nytt", + "not_running": "Kjører ikke", + "open_in_external_player": "Åpne i ekstern spiller", + "remove_date": "Fjern dato", + "remove_from_containing_group": "Fjern fra Gruppe", + "remove_from_gallery": "Fjern fra Galleri", + "scrape_with": "Skrap med…", + "search": "Søk", + "select_all": "Velg Alle", + "select_folders": "Velg mapper", + "select_none": "Velg Ingen", + "selective_scan": "Selektiv Skann", + "set_as_default": "Sett som standard", + "set_front_image": "Frontbilde…", + "show": "Vis", + "show_configuration": "Vis Konfigurasjon", + "skip": "Hopp over", + "split": "Splitt", + "stop": "Stopp", + "submit_update": "Send inn oppdatering", + "submit": "Send inn", + "swap": "Bytt", + "temp_disable": "Deaktiver midlertidig…", + "temp_enable": "Aktiver midlertidig…", + "unset": "Velg bort", + "use_default": "Bruk standard", + "view_history": "Visningshistorikk", + "view_random": "Vis Tilfeldig", + "migrate_blobs": "Migrer Blobs", + "migrate_scene_screenshots": "Migrer Scene Skjermbilder", + "reassign": "Omplasser", + "reload_plugins": "Last inn plugins på nytt", + "reload_scrapers": "Last inn skrapere på nytt", + "scrape_scene_fragment": "Skrap etter fragment", + "set_back_image": "Baksidebilde…", + "set_cover": "Velg som Omslag", + "allow": "Tillat", + "allow_temporarily": "Tillat midlertidig", + "backup": "Sikkerhetskopi", + "browse_for_image": "Bla gjennom bilder…", + "cancel": "Avbryt", + "apply": "Bruk", + "assign_stashid_to_parent_studio": "Tildel Stash ID til eksisterende foreldre studio og oppdater metadata", + "add_to_entity": "Legg til til {entityType}", + "add_entity": "Legg til {entityType}", + "add_manual_date": "Legg til manuell dato", + "add_directory": "Legg til mappe", + "add_o": "Legg til O", + "add_play": "Legg til avspilling", + "auto_tag": "Automatisk Tagging", + "choose_date": "Velg en dato", + "clean": "Rydd opp", + "import_from_file": "Importer fra fil", + "logout": "Logg ut", + "overwrite": "Overskriv", + "preview": "Forhåndsvis", + "optimise_database": "Optimaliser Database", + "play_random": "Spill av Tilfeldig", + "open_random": "Åpne tilfeldig", + "play_selected": "Spill av valgte", + "rescan": "Skann på nytt", + "reshuffle": "Stokk om", + "rename_gen_files": "Gi nytt navn til genererte filer", + "selective_auto_tag": "Selektiv Automatisk Tagging" + }, + "component_tagger": { + "config": { + "mark_organized_desc": "Umiddelbart marker scenen som Organisert etter at Lagre-knappen er trykket.", + "mark_organized_label": "Merk som Organisert ved lagring", + "query_mode_auto_desc": "Bruk metadata hvis tilstede, eller filnavn", + "blacklist_label": "Svarteliste", + "query_mode_auto": "Auto", + "query_mode_dir_desc": "Bruker bare mappen til videofilen", + "query_mode_filename": "Filnavn", + "query_mode_label": "Forespørselsmodus", + "active_instance": "Aktiv stash-box instans:", + "blacklist_desc": "Svartelisteelementer er ekskludert fra forespørsler. Merk at de er regulære uttrykk og skiller mellom store og små bokstaver. Enkelte tegn må forutgås av en omvendt skråstrek: {chars_require_escape}", + "query_mode_dir": "Mappe", + "query_mode_filename_desc": "Bruker kun filnavn", + "query_mode_metadata": "Metadata", + "query_mode_metadata_desc": "Bruker kun metadata", + "query_mode_path": "Filbane", + "query_mode_path_desc": "Bruker hele filbanen", + "set_cover_desc": "Bytt ut scenens omslag hvis en finnes.", + "source": "Kilde" + } + }, + "config": { + "dlna": { + "default_ip_whitelist": "Standard IP hviteliste", + "allowed_ip_temporarily": "Tillatt IP midlertidig", + "default_ip_whitelist_desc": "Standard IP-adresser gir tilgang til DLNA. Bruk {wildcard} for å tillatte alle IP-adresser.", + "recent_ip_addresses": "Nylige IP-adresser", + "disabled_dlna_temporarily": "Deaktivert DLNA midlertidig", + "disallowed_ip": "Ikke tillatt IP", + "enabled_by_default": "Aktivert som standard", + "enabled_dlna_temporarily": "Aktiverte DLNA midlertidig", + "network_interfaces": "Grensesnitt", + "allow_temp_ip": "Tillatt {tempIP}", + "allowed_ip_addresses": "Tillatt IP adresser", + "server_port": "Serverport", + "server_display_name": "Server Visningsnavn" + }, + "about": { + "stash_open_collective": "Støtt oss gjennom {url}", + "version": "Versjon", + "new_version_notice": "[NY]", + "release_date": "Utgivelsesdato:", + "stash_discord": "Bli med på vår {url} kanal", + "check_for_new_version": "Sjekk for ny versjon", + "latest_version": "Siste versjon", + "latest_version_build_hash": "Siste Versjon Build Hash:", + "build_time": "Kompileringstid:" + }, + "advanced_mode": "Avansert Modus", + "categories": { + "about": "Om", + "changelog": "Endringslogg", + "interface": "Grensesnitt", + "logs": "Logger", + "plugins": "Plugins", + "security": "Sikkerhet", + "services": "Tjenester", + "system": "System", + "metadata_providers": "Metadataleverandører", + "scraping": "Skarping", + "tools": "Verktøy", + "tasks": "Oppgaver" + }, + "general": { + "auth": { + "username": "Brukernavn", + "api_key": "API-nøkkel", + "log_file": "Log-fil", + "generate_api_key": "Generer API-nøkkel", + "log_to_terminal": "Log til terminal", + "password": "Passord" + }, + "db_path_head": "Database filbane", + "ffmpeg": { + "download_ffmpeg": { + "heading": "Last ned FFmpeg" + }, + "hardware_acceleration": { + "heading": "FFmpeg maskinvare encoding" + } + }, + "database": "Database" + } + }, + "appears_with": "Opptrer med", + "ascending": "Stigende", + "also_known_as": "Også kjent som", + "audio_codec": "Lyd Codec", + "average_resolution": "Gjennomsnittlig Oppløsning", + "actions_name": "Handlinger", + "age": "Alder", + "aliases": "Aliaser", + "birthdate": "Fødselsdato", + "bitrate": "Bithastighet", + "blobs_storage_type": { + "database": "Database", + "filesystem": "Filsystem" + }, + "captions": "Undertekster", + "career_length": "Karriere Lengde", + "chapters": "Kapitler", + "circumcised": "Omskåret", + "between_and": "og", + "circumcised_types": { + "CUT": "Omskåret", + "UNCUT": "Ikke omskåret" + }, + "birth_year": "Fødselsår" } diff --git a/ui/v2.5/src/locales/zh-CN.json b/ui/v2.5/src/locales/zh-CN.json index ce3aa6f178d..2262043efb6 100644 --- a/ui/v2.5/src/locales/zh-CN.json +++ b/ui/v2.5/src/locales/zh-CN.json @@ -140,8 +140,8 @@ "set_cover": "设置为封面", "reset_play_duration": "重置播放时长", "reset_resume_time": "重置恢复时间", - "add_sub_groups": "添加子群组", - "remove_from_containing_group": "从群组中移除" + "add_sub_groups": "添加子集合", + "remove_from_containing_group": "从集合中移除" }, "actions_name": "操作", "age": "年龄", @@ -807,7 +807,7 @@ "scenes": "{count, plural, one {短片} other {短片}}", "studios": "{count, plural, one {工作室} other {工作室}}", "tags": "{count, plural, one {标签} other {标签}}", - "groups": "{count, plural, one {群组} other {群组}}" + "groups": "{count, plural, one {集合} other {集合}}" }, "country": "国家", "cover_image": "封面图片", @@ -1476,19 +1476,20 @@ "o_count": "高潮次数", "studio_tags": "工作室标签", "studio_count": "工作室计数", - "group": "群组", - "group_count": "群组总计", + "group": "集合", + "group_count": "集合总计", "group_scene_number": "短片序号", - "groups": "群组", + "groups": "集合", "include_sub_studio_content": "包括子工作室内容", "include_sub_tag_content": "包括子标签内容", - "include_sub_group_content": "包括子群组内容", - "containing_group": "包含群组", - "containing_group_count": "包含群组计数", - "containing_groups": "包含群组", - "sub_group": "子群组", - "sub_group_count": "子群组计数", - "sub_group_of": "{parent}的子群组", - "sub_group_order": "子群组排序", - "sub_groups": "子群组" + "include_sub_group_content": "包括子集合内容", + "containing_group": "包含的集合", + "containing_group_count": "包含的集合计数", + "containing_groups": "被包含于集合", + "sub_group": "子集合", + "sub_group_count": "子集合计数", + "sub_group_of": "{parent}的子集合", + "sub_group_order": "子集合排序", + "sub_groups": "子集合", + "include_sub_groups": "包括子组" } diff --git a/ui/v2.5/src/models/list-filter/criteria/groups.ts b/ui/v2.5/src/models/list-filter/criteria/groups.ts index f7fd42492de..762ebf6e8b7 100644 --- a/ui/v2.5/src/models/list-filter/criteria/groups.ts +++ b/ui/v2.5/src/models/list-filter/criteria/groups.ts @@ -42,3 +42,13 @@ export const SubGroupsCriterionOption = new BaseGroupsCriterionOption( "sub_groups", "sub_groups" ); + +// redirects to GroupsCriterion +export const LegacyMoviesCriterionOption = new CriterionOption({ + messageID: "groups", + type: "movies", + modifierOptions, + defaultModifier, + inputType, + makeCriterion: () => new GroupsCriterion(GroupsCriterionOption), +}); diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index 965fa31be10..7496da6b62d 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -8,7 +8,10 @@ import { } from "./criteria/criterion"; import { HasMarkersCriterionOption } from "./criteria/has-markers"; import { SceneIsMissingCriterionOption } from "./criteria/is-missing"; -import { GroupsCriterionOption } from "./criteria/groups"; +import { + GroupsCriterionOption, + LegacyMoviesCriterionOption, +} from "./criteria/groups"; import { GalleriesCriterionOption } from "./criteria/galleries"; import { OrganizedCriterionOption } from "./criteria/organized"; import { PerformersCriterionOption } from "./criteria/performers"; @@ -106,6 +109,7 @@ const criterionOptions = [ // StudioTagsCriterionOption, StudiosCriterionOption, GroupsCriterionOption, + LegacyMoviesCriterionOption, GalleriesCriterionOption, createStringCriterionOption("url"), StashIDCriterionOption, diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index b632609ab8b..48e37c0461d 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -148,6 +148,7 @@ export type CriterionType = | "studios" | "scenes" | "groups" + | "movies" // legacy | "containing_groups" | "containing_group_count" | "sub_groups" diff --git a/ui/v2.5/src/utils/stashIds.ts b/ui/v2.5/src/utils/stashIds.ts index 3240b2a0f56..289ce9c9d70 100644 --- a/ui/v2.5/src/utils/stashIds.ts +++ b/ui/v2.5/src/utils/stashIds.ts @@ -1,5 +1,8 @@ -export const getStashIDs = (ids?: { stash_id: string; endpoint: string }[]) => - (ids ?? []).map(({ stash_id, endpoint }) => ({ +export const getStashIDs = ( + ids?: { stash_id: string; endpoint: string; updated_at: string }[] +) => + (ids ?? []).map(({ stash_id, endpoint, updated_at }) => ({ stash_id, endpoint, + updated_at, })); diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index 911fa135f20..733e10d2565 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -3718,9 +3718,9 @@ dotenv@^16.0.0: integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== dset@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.2.tgz#89c436ca6450398396dc6538ea00abc0c54cd45a" - integrity sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q== + version "3.1.4" + resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.4.tgz#f8eaf5f023f068a036d08cd07dc9ffb7d0065248" + integrity sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA== electron-to-chromium@^1.4.648: version "1.4.648" @@ -6224,9 +6224,9 @@ path-root@^0.1.1: path-root-regex "^0.1.0" path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + version "1.9.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24" + integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g== dependencies: isarray "0.0.1" @@ -6894,9 +6894,9 @@ rimraf@^3.0.2: glob "^7.1.3" rollup@^3.27.1: - version "3.29.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" - integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + version "3.29.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54" + integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w== optionalDependencies: fsevents "~2.3.2" @@ -8008,10 +8008,10 @@ vite-tsconfig-paths@^4.0.5: globrex "^0.1.2" tsconfck "^2.0.1" -vite@^4.5.3: - version "4.5.3" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.3.tgz#d88a4529ea58bae97294c7e2e6f0eab39a50fb1a" - integrity sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg== +vite@^4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.5.tgz#639b9feca5c0a3bfe3c60cb630ef28bf219d742e" + integrity sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ== dependencies: esbuild "^0.18.10" postcss "^8.4.27"