mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
145 Commits
v0.27.0
...
docs-updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0df4077ace | ||
|
|
b14b2796f9 | ||
|
|
4bfc93b7ae | ||
|
|
c0d5d1e5a7 | ||
|
|
bac0b0a379 | ||
|
|
d9b4e62420 | ||
|
|
c8d74f0bcf | ||
|
|
18381664aa | ||
|
|
e9a67eb51f | ||
|
|
2ec264ed62 | ||
|
|
e5446a2336 | ||
|
|
db7d45792e | ||
|
|
5d3d02e1e7 | ||
|
|
2541e9d1eb | ||
|
|
cc6917f29d | ||
|
|
9636ff7c16 | ||
|
|
81f642b8b8 | ||
|
|
6f848f7f1c | ||
|
|
720bbcb5c0 | ||
|
|
8ce31a2831 | ||
|
|
7a4ff20d66 | ||
|
|
daed09e487 | ||
|
|
529e4f6514 | ||
|
|
6d451d52ea | ||
|
|
4d61c88661 | ||
|
|
bc923929bb | ||
|
|
193b175618 | ||
|
|
913a58057a | ||
|
|
a621514c71 | ||
|
|
c2bc31387c | ||
|
|
9b7e20351a | ||
|
|
df5566771a | ||
|
|
cbcc1994e8 | ||
|
|
bfdc4bac59 | ||
|
|
a3f8c36536 | ||
|
|
0f32311f6e | ||
|
|
fdb2dd9a8b | ||
|
|
ea5073fef4 | ||
|
|
ce2d779dbc | ||
|
|
a391fa4345 | ||
|
|
23e36b12fe | ||
|
|
59014f14ca | ||
|
|
bf3a0e7944 | ||
|
|
5f595f8ca7 | ||
|
|
4d447c3340 | ||
|
|
661e9eba51 | ||
|
|
b49157f968 | ||
|
|
7f58309143 | ||
|
|
4f45ea8e7d | ||
|
|
ccf79d077f | ||
|
|
f23450c380 | ||
|
|
f65976cf4d | ||
|
|
b8af147a8d | ||
|
|
1e05766571 | ||
|
|
587fd9e6b8 | ||
|
|
e97f647a43 | ||
|
|
b6ace42973 | ||
|
|
46d424fbaf | ||
|
|
d915787840 | ||
|
|
57e044e689 | ||
|
|
3f90e57861 | ||
|
|
0296b63be5 | ||
|
|
e041ad190f | ||
|
|
3ea49c6c2e | ||
|
|
c8032f04fa | ||
|
|
50a900e83c | ||
|
|
638398808b | ||
|
|
d2daf6c69f | ||
|
|
dd40c07a6d | ||
|
|
d95e35783a | ||
|
|
3078cb39c1 | ||
|
|
5a8725b233 | ||
|
|
b0a10399d7 | ||
|
|
9f7d00d83f | ||
|
|
b30bd8d2fe | ||
|
|
8bacaa17f4 | ||
|
|
4d43763a39 | ||
|
|
44d764d832 | ||
|
|
726296bb54 | ||
|
|
4ed522c5f8 | ||
|
|
b7592374aa | ||
|
|
077cd774f3 | ||
|
|
b5cb52bb5e | ||
|
|
0621d87133 | ||
|
|
cacfe5a268 | ||
|
|
8c8be22fe4 | ||
|
|
a0e09bbe5c | ||
|
|
4be793d4b3 | ||
|
|
60bb6bf50b | ||
|
|
7f8349469a | ||
|
|
6ad0951878 | ||
|
|
e097f2b3f4 | ||
|
|
3c81d3b154 | ||
|
|
ef2231f97b | ||
|
|
f81202660c | ||
|
|
6c5bf5f052 | ||
|
|
5f690d96bd | ||
|
|
64fed3553a | ||
|
|
a18c538c1f | ||
|
|
41d1b45fb9 | ||
|
|
602f95dd29 | ||
|
|
2a454e5a1e | ||
|
|
a100f8ffc8 | ||
|
|
527c282b92 | ||
|
|
e8125d08db | ||
|
|
0d40056f8c | ||
|
|
180a0fa8dd | ||
|
|
b1d5dc2a0e | ||
|
|
89f539ee24 | ||
|
|
f949fab231 | ||
|
|
edb66bd4e4 | ||
|
|
1b7e729750 | ||
|
|
7fb8f9172e | ||
|
|
069a4b1f80 | ||
|
|
c6bcdd89be | ||
|
|
093de3bce2 | ||
|
|
8c5ebf3797 | ||
|
|
33e46bad64 | ||
|
|
eca41dc7b4 | ||
|
|
33ca4f8887 | ||
|
|
76648fee66 | ||
|
|
6d07ecf751 | ||
|
|
5283eb8ce3 | ||
|
|
32c48443b5 | ||
|
|
ad00bee393 | ||
|
|
a54996d8a2 | ||
|
|
b6db4c31ca | ||
|
|
f82e24762b | ||
|
|
35b74be585 | ||
|
|
7199d2b5ac | ||
|
|
4697271294 | ||
|
|
3e4515e62a | ||
|
|
58c58beb4a | ||
|
|
f05518860f | ||
|
|
9b567fa6f4 | ||
|
|
c92de09ece | ||
|
|
9765b6d50e | ||
|
|
c6c3754f02 | ||
|
|
76a5b2a06a | ||
|
|
93a2ee1277 | ||
|
|
be6431ac13 | ||
|
|
4dd8dd948e | ||
|
|
e253ba71f8 | ||
|
|
30fc2d1209 | ||
|
|
cef5b46f93 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -12,7 +12,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:9
|
||||
COMPILER_IMAGE: stashapp/compiler:10
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
4
.github/workflows/golangci-lint.yml
vendored
4
.github/workflows/golangci-lint.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:9
|
||||
COMPILER_IMAGE: stashapp/compiler:10
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run: docker exec -t build /bin/bash -c "make generate-backend"
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||
version: latest
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,6 +21,9 @@ vendor
|
||||
# GraphQL generated output
|
||||
internal/api/generated_*.go
|
||||
|
||||
# Generated locale files
|
||||
ui/login/locales/*
|
||||
|
||||
####
|
||||
# Visual Studio
|
||||
####
|
||||
|
||||
@@ -48,8 +48,6 @@ linters-settings:
|
||||
ignore-generated-header: true
|
||||
severity: error
|
||||
confidence: 0.8
|
||||
error-code: 1
|
||||
warning-code: 1
|
||||
rules:
|
||||
- name: blank-imports
|
||||
disabled: true
|
||||
|
||||
9
Makefile
9
Makefile
@@ -281,6 +281,10 @@ generate-ui:
|
||||
generate-backend: touch-ui
|
||||
go generate ./cmd/stash
|
||||
|
||||
.PHONY: generate-login-locale
|
||||
generate-login-locale:
|
||||
go generate ./ui
|
||||
|
||||
.PHONY: generate-dataloaders
|
||||
generate-dataloaders:
|
||||
go generate ./internal/api/loaders
|
||||
@@ -351,7 +355,10 @@ ifdef STASH_SOURCEMAPS
|
||||
endif
|
||||
|
||||
.PHONY: ui
|
||||
ui: ui-env
|
||||
ui: ui-only generate-login-locale
|
||||
|
||||
.PHONY: ui-only
|
||||
ui-only: ui-env
|
||||
cd ui/v2.5 && yarn build
|
||||
|
||||
.PHONY: zip-ui
|
||||
|
||||
@@ -24,6 +24,11 @@ For further information you can consult the [documentation](https://docs.stashap
|
||||
|
||||
# Installing Stash
|
||||
|
||||
#### Windows Users:
|
||||
|
||||
As of version 0.27.0, Stash doesn't support anymore _Windows 7, 8, Server 2008 and Server 2012._
|
||||
Windows 10 or Server 2016 are at least required.
|
||||
|
||||
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS | <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
|
||||
:---:|:---:|:---:|:---:
|
||||
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release](https://github.com/stashapp/stash/releases/latest/download/Stash.app.zip) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/Stash.app.zip)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub>[Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
|
||||
@@ -76,9 +81,9 @@ For more help you can:
|
||||
# Customization
|
||||
|
||||
## Themes and CSS Customization
|
||||
There is a [directory of community-created themes](https://docs.stashapp.cc/user-interface-ui/themes) on Stash-Docs, along with instructions on how to install them.
|
||||
There is a [directory of community-created themes](https://docs.stashapp.cc/themes/list) on Stash-Docs.
|
||||
|
||||
You can also change the Stash interface to fit your desired style with various snippets from [Custom CSS snippets](https://docs.stashapp.cc/user-interface-ui/custom-css-snippets).
|
||||
You can also change the Stash interface to fit your desired style with various snippets from [Custom CSS snippets](https://docs.stashapp.cc/themes/custom-css-snippets).
|
||||
|
||||
# For Developers
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This dockerfile should be built with `make docker-build` from the stash root.
|
||||
|
||||
# Build Frontend
|
||||
FROM node:alpine as frontend
|
||||
FROM node:20-alpine AS frontend
|
||||
RUN apk add --no-cache make git
|
||||
## cache node_modules separately
|
||||
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
|
||||
@@ -13,19 +13,22 @@ RUN make pre-ui
|
||||
RUN make generate-ui
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui-only
|
||||
|
||||
# Build Backend
|
||||
FROM golang:1.22-alpine as backend
|
||||
FROM golang:1.22.8-alpine AS backend
|
||||
RUN apk add --no-cache make alpine-sdk
|
||||
WORKDIR /stash
|
||||
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
||||
COPY ./graphql /stash/graphql/
|
||||
COPY ./scripts /stash/scripts/
|
||||
COPY ./pkg /stash/pkg/
|
||||
COPY ./cmd /stash/cmd
|
||||
COPY ./internal /stash/internal
|
||||
COPY ./cmd /stash/cmd/
|
||||
COPY ./internal /stash/internal/
|
||||
# needed for generate-login-locale
|
||||
COPY ./ui /stash/ui/
|
||||
RUN make generate-backend generate-login-locale
|
||||
COPY --from=frontend /stash /stash/
|
||||
RUN make generate-backend
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN make flags-release flags-pie stash
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This dockerfile should be built with `make docker-cuda-build` from the stash root.
|
||||
|
||||
# Build Frontend
|
||||
FROM node:alpine as frontend
|
||||
FROM node:20-alpine AS frontend
|
||||
RUN apk add --no-cache make git
|
||||
## cache node_modules separately
|
||||
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
|
||||
@@ -13,19 +13,22 @@ RUN make pre-ui
|
||||
RUN make generate-ui
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui-only
|
||||
|
||||
# Build Backend
|
||||
FROM golang:1.22-bullseye as backend
|
||||
FROM golang:1.22.8-bullseye AS backend
|
||||
RUN apt update && apt install -y build-essential golang
|
||||
WORKDIR /stash
|
||||
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
||||
COPY ./graphql /stash/graphql/
|
||||
COPY ./scripts /stash/scripts/
|
||||
COPY ./pkg /stash/pkg/
|
||||
COPY ./cmd /stash/cmd
|
||||
COPY ./internal /stash/internal
|
||||
# needed for generate-login-locale
|
||||
COPY ./ui /stash/ui/
|
||||
RUN make generate-backend generate-login-locale
|
||||
COPY --from=frontend /stash /stash/
|
||||
RUN make generate-backend
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN make flags-release flags-pie stash
|
||||
|
||||
@@ -4,7 +4,7 @@ This dockerfile is used to build a stash docker container using the current sour
|
||||
|
||||
# Building the docker container
|
||||
|
||||
From the top-level directory (should contain `main.go` file):
|
||||
From the top-level directory (should contain `tools.go` file):
|
||||
|
||||
```
|
||||
make docker-build
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22
|
||||
FROM golang:1.22.8
|
||||
|
||||
LABEL maintainer="https://discord.gg/2TsNFKt"
|
||||
|
||||
@@ -26,9 +26,9 @@ RUN apt-get update && \
|
||||
|
||||
# FreeBSD cross-compilation setup
|
||||
# https://github.com/smartmontools/docker-build/blob/6b8c92560d17d325310ba02d9f5a4b250cb0764a/Dockerfile#L66
|
||||
ENV FREEBSD_VERSION 12.4
|
||||
ENV FREEBSD_VERSION 13.4
|
||||
ENV FREEBSD_DOWNLOAD_URL http://ftp.plusline.de/FreeBSD/releases/amd64/${FREEBSD_VERSION}-RELEASE/base.txz
|
||||
ENV FREEBSD_SHA 581c7edacfd2fca2bdf5791f667402d22fccd8a5e184635e0cac075564d57aa8
|
||||
ENV FREEBSD_SHA 8e13b0a93daba349b8d28ad246d7beb327659b2ef4fe44d89f447392daec5a7c
|
||||
|
||||
RUN cd /tmp && \
|
||||
curl -o base.txz $FREEBSD_DOWNLOAD_URL && \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
user=stashapp
|
||||
repo=compiler
|
||||
version=9
|
||||
version=10
|
||||
|
||||
latest:
|
||||
docker build -t ${user}/${repo}:latest .
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Docker Installation (for most 64-bit GNU/Linux systems)
|
||||
StashApp is supported on most systems that support Docker and docker-compose. Your OS likely ships with or makes available the necessary packages.
|
||||
StashApp is supported on most systems that support Docker. Your OS likely ships with or makes available the necessary packages.
|
||||
|
||||
## Dependencies
|
||||
Only `docker` and `docker-compose` are required. For the most part your understanding of the technologies can be superficial. So long as you can follow commands and are open to reading a bit, you should be fine.
|
||||
Only `docker` is required. For the most part your understanding of the technologies can be superficial. So long as you can follow commands and are open to reading a bit, you should be fine.
|
||||
|
||||
Installation instructions are available below, and if your distrobution's repository ships a current version of docker, you may use that.
|
||||
Installation instructions are available below, and if your distributions's repository ships a current version of docker, you may use that.
|
||||
https://docs.docker.com/engine/install/
|
||||
|
||||
On some distributions, `docker compose` is shipped seperately, usually as `docker-cli-compose`. docker-compose is not recommended.
|
||||
|
||||
### Get the docker-compose.yml file
|
||||
|
||||
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/develop/docker/production/docker-compose.yml) in the repository, or if you have curl, you can make your Linux console do it for you:
|
||||
@@ -19,7 +21,7 @@ curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/deve
|
||||
Once you have that file where you want it, modify the settings as you please, and then run:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Installing StashApp this way will by default bind stash to port 9999. This is available in your web browser locally at http://localhost:9999 or on your network as http://YOUR-LOCAL-IP:9999
|
||||
@@ -29,9 +31,9 @@ Good luck and have fun!
|
||||
### Docker
|
||||
Docker is effectively a cross-platform software package repository. It allows you to ship an entire environment in what's referred to as a container. Containers are intended to hold everything that is needed to run an application from one place to another, making it easy for everyone along the way to reproduce the environment.
|
||||
|
||||
The StashApp docker container ships with everything you need to automatically build and run stash, including ffmpeg.
|
||||
The StashApp docker container ships with everything you need to automatically run stash, including ffmpeg.
|
||||
|
||||
### docker-compose
|
||||
Docker Compose lets you specify how and where to run your containers, and to manage their environment. The docker-compose.yml file in this folder gets you a fully working instance of StashApp exactly as you would need it to have a reasonable instance for testing / developing on. If you are deploying a live instance for production, a reverse proxy (such as NGINX or Traefik) is recommended, but not required.
|
||||
### docker compose
|
||||
Docker Compose lets you specify how and where to run your containers, and to manage their environment. The docker-compose.yml file in this folder gets you a fully working instance of StashApp exactly as you would need it to have a reasonable instance for testing / developing on. If you are deploying a live instance for production, a [reverse proxy](https://docs.stashapp.cc/guides/reverse-proxy/) (such as NGINX or Traefik) is recommended, but not required.
|
||||
|
||||
The latest version is always recommended.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# APPNICENAME=Stash
|
||||
# APPDESCRIPTION=An organizer for your porn, written in Go
|
||||
version: '3.4'
|
||||
services:
|
||||
stash:
|
||||
image: stashapp/stash:latest
|
||||
@@ -27,10 +26,12 @@ services:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
## Adjust below paths (the left part) to your liking.
|
||||
## E.g. you can change ./config:/root/.stash to ./stash:/root/.stash
|
||||
|
||||
## The left part is the path on your host, the right part is the path in the stash container.
|
||||
|
||||
## Keep configs, scrapers, and plugins here.
|
||||
- ./config:/root/.stash
|
||||
## Point this at your collection.
|
||||
## The left side is where your collection is on your host, the right side is where it will be in stash.
|
||||
- ./data:/data
|
||||
## This is where your stash's metadata lives
|
||||
- ./metadata:/metadata
|
||||
|
||||
41
go.mod
41
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
|
||||
@@ -19,13 +19,14 @@ require (
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/httplog v0.3.1
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/gofrs/uuid/v5 v5.1.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/hasura/go-graphql-client v0.13.1
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
@@ -39,7 +40,6 @@ require (
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cast v1.6.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
@@ -47,27 +47,29 @@ 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.31.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.33.0
|
||||
golang.org/x/sys v0.28.0
|
||||
golang.org/x/term v0.27.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/time v0.10.0
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/agnivade/levenshtein v1.2.0 // indirect
|
||||
github.com/antchfx/xpath v1.2.3 // indirect
|
||||
github.com/asticode/go-astikit v0.20.0 // indirect
|
||||
github.com/asticode/go-astits v1.8.0 // indirect
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
@@ -77,7 +79,6 @@ require (
|
||||
github.com/gobwas/ws v1.3.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
@@ -108,12 +109,12 @@ require (
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.2 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.5 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.10.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
|
||||
)
|
||||
|
||||
102
go.sum
102
go.sum
@@ -51,26 +51,23 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/99designs/gqlgen v0.17.2/go.mod h1:K5fzLKwtph+FFgh9j7nFbRUdBKvTcGnsta51fsMTn3o=
|
||||
github.com/99designs/gqlgen v0.17.49 h1:b3hNGexHd33fBSAd4NDT/c3NCcQzcAVkknhN9ym36YQ=
|
||||
github.com/99designs/gqlgen v0.17.49/go.mod h1:tC8YFVZMed81x7UJ7ORUwXF4Kn6SXuucFqQBhN8+BU0=
|
||||
github.com/99designs/gqlgen v0.17.55 h1:3vzrNWYyzSZjGDFo68e5j9sSauLxfKvLp+6ioRokVtM=
|
||||
github.com/99designs/gqlgen v0.17.55/go.mod h1:3Bq768f8hgVPGZxL8aY9MaYmbxa6llPM/qu1IGH1EJo=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0=
|
||||
github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U=
|
||||
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552 h1:eukVk+mGmbSZppLw8WJGpEUgMC570eb32y7FOsPW4Kc=
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552/go.mod h1:LKbO1i6L1lSlwWx4NHWVECxubHNKFz2YQoEMGXAFVy8=
|
||||
github.com/Yamashou/gqlgenc v0.0.6 h1:wfMTtuVSrX2N1z5/ssecxx+E7l1fa0FOq5mwFW47oY4=
|
||||
github.com/Yamashou/gqlgenc v0.0.6/go.mod h1:WOXjogecRGpD1WKgxnnyHJo0/Dxn44p/LNRoE6mtFQo=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
|
||||
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/Yamashou/gqlgenc v0.25.3 h1:mVV8/Ho8EDZUQKQZbQqqXGNq8jc8aQfPpHhZOnTkMNE=
|
||||
github.com/Yamashou/gqlgenc v0.25.3/go.mod h1:G0g1N81xpIklVdnyboW1zwOHcj/n4hNfhTwfN29Rjig=
|
||||
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
|
||||
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -156,23 +153,24 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
|
||||
github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
@@ -246,12 +244,10 @@ github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K
|
||||
github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0=
|
||||
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid/v5 v5.1.0 h1:S5rqVKIigghZTCBKPCw0Y+bXkn26K3TB5mvQq2Ix8dk=
|
||||
github.com/gofrs/uuid/v5 v5.1.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
@@ -344,7 +340,6 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
@@ -399,6 +394,8 @@ github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoI
|
||||
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hasura/go-graphql-client v0.13.1 h1:kKbjhxhpwz58usVl+Xvgah/TDha5K2akNTRQdsEHN6U=
|
||||
github.com/hasura/go-graphql-client v0.13.1/go.mod h1:k7FF7h53C+hSNFRG3++DdVZWIuHdCaTbI7siTJ//zGQ=
|
||||
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
|
||||
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
|
||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||
@@ -435,7 +432,6 @@ github.com/kermieisinthehouse/gosx-notifier v0.1.2 h1:KV0KBeKK2B24kIHY7iK0jgS64Q
|
||||
github.com/kermieisinthehouse/gosx-notifier v0.1.2/go.mod h1:xyWT07azFtUOcHl96qMVvKhvKzsMcS7rKTHQyv8WTho=
|
||||
github.com/kermieisinthehouse/systray v1.2.4 h1:pdH5vnl+KKjRrVCRU4g/2W1/0HVzuuJ6WXHlPPHYY6s=
|
||||
github.com/kermieisinthehouse/systray v1.2.4/go.mod h1:axh6C/jNuSyC0QGtidZJURc9h+h41HNoMySoLVrhVR4=
|
||||
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
|
||||
@@ -459,7 +455,6 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
|
||||
@@ -468,7 +463,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@@ -505,7 +499,6 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
@@ -587,7 +580,6 @@ github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+
|
||||
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
||||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
@@ -599,12 +591,8 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5P
|
||||
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
|
||||
github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
@@ -664,24 +652,21 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
|
||||
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
|
||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/vearutop/statigz v1.4.0 h1:RQL0KG3j/uyA/PFpHeZ/L6l2ta920/MxlOAIGEOuwmU=
|
||||
github.com/vearutop/statigz v1.4.0/go.mod h1:LYTolBLiz9oJISwiVKnOQoIwhO1LWX1A7OECawGS8XE=
|
||||
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
|
||||
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8=
|
||||
github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww=
|
||||
github.com/vektah/gqlparser/v2 v2.5.18 h1:zSND3GtutylAQ1JpWnTHcqtaRZjl+y3NROeW8vuNo6Y=
|
||||
github.com/vektah/gqlparser/v2 v2.5.18/go.mod h1:6HLzf7JKv9Fi3APymudztFQNmLXR5qJeEo6BOFcXVfc=
|
||||
github.com/vektra/mockery/v2 v2.10.0 h1:MiiQWxwdq7/ET6dCXLaJzSGEN17k758H7JHS9kOdiks=
|
||||
github.com/vektra/mockery/v2 v2.10.0/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
|
||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e h1:GruPsb+44XvYAzuAgJW1d1WHqmcI73L2XSjsbx/eJZw=
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e/go.mod h1:wA8kQ8WFipMciY9WcWzqQgZordm/P7l8IZdvx1crwmc=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -728,8 +713,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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -770,10 +755,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -823,8 +807,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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -854,8 +838,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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -938,7 +922,6 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -947,13 +930,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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.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.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -966,11 +949,13 @@ 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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
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=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -1013,7 +998,6 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
@@ -1030,11 +1014,9 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
14
gqlgen.yml
14
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
|
||||
|
||||
@@ -20,7 +17,7 @@ autobind:
|
||||
- github.com/stashapp/stash/pkg/scraper
|
||||
- github.com/stashapp/stash/internal/identify
|
||||
- github.com/stashapp/stash/internal/dlna
|
||||
- github.com/stashapp/stash/pkg/scraper/stashbox
|
||||
- github.com/stashapp/stash/pkg/stashbox
|
||||
|
||||
models:
|
||||
# Scalars
|
||||
@@ -38,12 +35,6 @@ models:
|
||||
model: github.com/stashapp/stash/internal/api.BoolMap
|
||||
PluginConfigMap:
|
||||
model: github.com/stashapp/stash/internal/api.PluginConfigMap
|
||||
# define to force resolvers
|
||||
Image:
|
||||
model: github.com/stashapp/stash/pkg/models.Image
|
||||
fields:
|
||||
title:
|
||||
resolver: true
|
||||
VideoFile:
|
||||
fields:
|
||||
# override float fields - #1572
|
||||
@@ -132,9 +123,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:
|
||||
|
||||
@@ -45,6 +45,7 @@ type Query {
|
||||
findSceneMarkers(
|
||||
scene_marker_filter: SceneMarkerFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindSceneMarkersResultType!
|
||||
|
||||
findImage(id: ID, checksum: String): Image
|
||||
@@ -173,6 +174,12 @@ type Query {
|
||||
input: ScrapeSingleGroupInput!
|
||||
): [ScrapedGroup!]!
|
||||
|
||||
"Scrape for a single image"
|
||||
scrapeSingleImage(
|
||||
source: ScraperSourceInput!
|
||||
input: ScrapeSingleImageInput!
|
||||
): [ScrapedImage!]!
|
||||
|
||||
"Scrapes content based on a URL"
|
||||
scrapeURL(url: String!, ty: ScrapeContentType!): ScrapedContent
|
||||
|
||||
@@ -182,6 +189,8 @@ type Query {
|
||||
scrapeSceneURL(url: String!): ScrapedScene
|
||||
"Scrapes a complete gallery record based on a URL"
|
||||
scrapeGalleryURL(url: String!): ScrapedGallery
|
||||
"Scrapes a complete image record based on a URL"
|
||||
scrapeImageURL(url: String!): ScrapedImage
|
||||
"Scrapes a complete movie record based on a URL"
|
||||
scrapeMovieURL(url: String!): ScrapedMovie
|
||||
@deprecated(reason: "Use scrapeGroupURL instead")
|
||||
@@ -300,6 +309,7 @@ type Mutation {
|
||||
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
|
||||
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
|
||||
sceneMarkerDestroy(id: ID!): Boolean!
|
||||
sceneMarkersDestroy(ids: [ID!]!): Boolean!
|
||||
|
||||
sceneAssignFile(input: AssignSceneFileInput!): Boolean!
|
||||
|
||||
|
||||
@@ -91,6 +91,12 @@ input StashIDCriterionInput {
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input CustomFieldCriterionInput {
|
||||
field: String!
|
||||
value: [Any!]
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input PerformerFilterType {
|
||||
AND: PerformerFilterType
|
||||
OR: PerformerFilterType
|
||||
@@ -182,6 +188,8 @@ input PerformerFilterType {
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
|
||||
custom_fields: [CustomFieldCriterionInput!]
|
||||
}
|
||||
|
||||
input SceneMarkerFilterType {
|
||||
@@ -193,6 +201,8 @@ input SceneMarkerFilterType {
|
||||
performers: MultiCriterionInput
|
||||
"Filter to only include scene markers from these scenes"
|
||||
scenes: MultiCriterionInput
|
||||
"Filter by duration (in seconds)"
|
||||
duration: FloatCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
@@ -532,6 +542,9 @@ input TagFilterType {
|
||||
"Filter by tag name"
|
||||
name: StringCriterionInput
|
||||
|
||||
"Filter by tag sort_name"
|
||||
sort_name: StringCriterionInput
|
||||
|
||||
"Filter by tag aliases"
|
||||
aliases: StringCriterionInput
|
||||
|
||||
|
||||
@@ -338,3 +338,10 @@ type SystemStatus {
|
||||
input MigrateInput {
|
||||
backupPath: String!
|
||||
}
|
||||
|
||||
input CustomFieldsInput {
|
||||
"If populated, the entire custom fields map will be replaced with this value"
|
||||
full: Map
|
||||
"If populated, only the keys in this map will be updated"
|
||||
partial: Map
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ type Performer {
|
||||
updated_at: Time!
|
||||
groups: [Group!]!
|
||||
movies: [Movie!]! @deprecated(reason: "use groups instead")
|
||||
|
||||
custom_fields: Map!
|
||||
}
|
||||
|
||||
input PerformerCreateInput {
|
||||
@@ -93,6 +95,8 @@ input PerformerCreateInput {
|
||||
hair_color: String
|
||||
weight: Int
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
custom_fields: Map
|
||||
}
|
||||
|
||||
input PerformerUpdateInput {
|
||||
@@ -129,6 +133,8 @@ input PerformerUpdateInput {
|
||||
hair_color: String
|
||||
weight: Int
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input BulkUpdateStrings {
|
||||
@@ -167,6 +173,8 @@ input BulkPerformerUpdateInput {
|
||||
hair_color: String
|
||||
weight: Int
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input PerformerDestroyInput {
|
||||
|
||||
@@ -2,7 +2,10 @@ type SceneMarker {
|
||||
id: ID!
|
||||
scene: Scene!
|
||||
title: String!
|
||||
"The required start time of the marker (in seconds). Supports decimals."
|
||||
seconds: Float!
|
||||
"The optional end time of the marker (in seconds). Supports decimals."
|
||||
end_seconds: Float
|
||||
primary_tag: Tag!
|
||||
tags: [Tag!]!
|
||||
created_at: Time!
|
||||
@@ -18,7 +21,10 @@ type SceneMarker {
|
||||
|
||||
input SceneMarkerCreateInput {
|
||||
title: String!
|
||||
"The required start time of the marker (in seconds). Supports decimals."
|
||||
seconds: Float!
|
||||
"The optional end time of the marker (in seconds). Supports decimals."
|
||||
end_seconds: Float
|
||||
scene_id: ID!
|
||||
primary_tag_id: ID!
|
||||
tag_ids: [ID!]
|
||||
@@ -27,7 +33,10 @@ input SceneMarkerCreateInput {
|
||||
input SceneMarkerUpdateInput {
|
||||
id: ID!
|
||||
title: String
|
||||
"The start time of the marker (in seconds). Supports decimals."
|
||||
seconds: Float
|
||||
"The end time of the marker (in seconds). Supports decimals."
|
||||
end_seconds: Float
|
||||
scene_id: ID
|
||||
primary_tag_id: ID
|
||||
tag_ids: [ID!]
|
||||
|
||||
@@ -10,6 +10,7 @@ enum ScrapeType {
|
||||
"Type of the content a scraper generates"
|
||||
enum ScrapeContentType {
|
||||
GALLERY
|
||||
IMAGE
|
||||
MOVIE
|
||||
GROUP
|
||||
PERFORMER
|
||||
@@ -22,6 +23,7 @@ union ScrapedContent =
|
||||
| ScrapedTag
|
||||
| ScrapedScene
|
||||
| ScrapedGallery
|
||||
| ScrapedImage
|
||||
| ScrapedMovie
|
||||
| ScrapedGroup
|
||||
| ScrapedPerformer
|
||||
@@ -41,6 +43,8 @@ type Scraper {
|
||||
scene: ScraperSpec
|
||||
"Details for gallery scraper"
|
||||
gallery: ScraperSpec
|
||||
"Details for image scraper"
|
||||
image: ScraperSpec
|
||||
"Details for movie scraper"
|
||||
movie: ScraperSpec @deprecated(reason: "use group")
|
||||
"Details for group scraper"
|
||||
@@ -128,6 +132,26 @@ input ScrapedGalleryInput {
|
||||
# no studio, tags or performers
|
||||
}
|
||||
|
||||
type ScrapedImage {
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
photographer: String
|
||||
urls: [String!]
|
||||
date: String
|
||||
studio: ScrapedStudio
|
||||
tags: [ScrapedTag!]
|
||||
performers: [ScrapedPerformer!]
|
||||
}
|
||||
|
||||
input ScrapedImageInput {
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
urls: [String!]
|
||||
date: String
|
||||
}
|
||||
|
||||
input ScraperSourceInput {
|
||||
"Index of the configured stash-box instance to use. Should be unset if scraper_id is set"
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
@@ -190,6 +214,15 @@ input ScrapeSingleGalleryInput {
|
||||
gallery_input: ScrapedGalleryInput
|
||||
}
|
||||
|
||||
input ScrapeSingleImageInput {
|
||||
"Instructs to query by string"
|
||||
query: String
|
||||
"Instructs to query by image id"
|
||||
image_id: ID
|
||||
"Instructs to query by image fragment"
|
||||
image_input: ScrapedImageInput
|
||||
}
|
||||
|
||||
input ScrapeSingleMovieInput {
|
||||
"Instructs to query by string"
|
||||
query: String
|
||||
|
||||
@@ -2,22 +2,27 @@ type StashBox {
|
||||
endpoint: String!
|
||||
api_key: String!
|
||||
name: String!
|
||||
max_requests_per_minute: Int!
|
||||
}
|
||||
|
||||
input StashBoxInput {
|
||||
endpoint: String!
|
||||
api_key: String!
|
||||
name: String!
|
||||
# defaults to 240
|
||||
max_requests_per_minute: Int
|
||||
}
|
||||
|
||||
type StashID {
|
||||
endpoint: String!
|
||||
stash_id: String!
|
||||
updated_at: Time!
|
||||
}
|
||||
|
||||
input StashIDInput {
|
||||
endpoint: String!
|
||||
stash_id: String!
|
||||
updated_at: Time
|
||||
}
|
||||
|
||||
input StashBoxFingerprintSubmissionInput {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
type Tag {
|
||||
id: ID!
|
||||
name: String!
|
||||
"Value that does not appear in the UI but overrides name for sorting"
|
||||
sort_name: String
|
||||
description: String
|
||||
aliases: [String!]!
|
||||
ignore_auto_tag: Boolean!
|
||||
@@ -25,6 +27,8 @@ type Tag {
|
||||
|
||||
input TagCreateInput {
|
||||
name: String!
|
||||
"Value that does not appear in the UI but overrides name for sorting"
|
||||
sort_name: String
|
||||
description: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
@@ -39,6 +43,8 @@ input TagCreateInput {
|
||||
input TagUpdateInput {
|
||||
id: ID!
|
||||
name: String
|
||||
"Value that does not appear in the UI but overrides name for sorting"
|
||||
sort_name: String
|
||||
description: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
@@ -30,11 +30,6 @@ fragment TagFragment on Tag {
|
||||
id
|
||||
}
|
||||
|
||||
fragment FuzzyDateFragment on FuzzyDate {
|
||||
date
|
||||
accuracy
|
||||
}
|
||||
|
||||
fragment MeasurementsFragment on Measurements {
|
||||
band_size
|
||||
cup_size
|
||||
@@ -54,15 +49,16 @@ fragment PerformerFragment on Performer {
|
||||
aliases
|
||||
gender
|
||||
merged_ids
|
||||
deleted
|
||||
merged_into_id
|
||||
urls {
|
||||
...URLFragment
|
||||
}
|
||||
images {
|
||||
...ImageFragment
|
||||
}
|
||||
birthdate {
|
||||
...FuzzyDateFragment
|
||||
}
|
||||
birth_date
|
||||
death_date
|
||||
ethnicity
|
||||
country
|
||||
eye_color
|
||||
|
||||
@@ -16,12 +16,12 @@ import (
|
||||
|
||||
const (
|
||||
tripwireActivatedErrMsg = "Stash is exposed to the public internet without authentication, and is not serving any more content to protect your privacy. " +
|
||||
"More information and fixes are available at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
|
||||
"More information and fixes are available at https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet"
|
||||
|
||||
externalAccessErrMsg = "You have attempted to access Stash over the internet, and authentication is not enabled. " +
|
||||
"This is extremely dangerous! The whole world can see your your stash page and browse your files! " +
|
||||
"Stash is not answering any other requests to protect your privacy. " +
|
||||
"Please read the log entry or visit https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
|
||||
"Please read the log entry or visit https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet"
|
||||
)
|
||||
|
||||
func allowUnauthenticated(r *http.Request) bool {
|
||||
|
||||
@@ -335,13 +335,13 @@ func (t changesetTranslator) updateStringsBulk(value *BulkUpdateStrings, field s
|
||||
}
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateStashIDs(value []models.StashID, field string) *models.UpdateStashIDs {
|
||||
func (t changesetTranslator) updateStashIDs(value models.StashIDInputs, field string) *models.UpdateStashIDs {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.UpdateStashIDs{
|
||||
StashIDs: value,
|
||||
StashIDs: value.ToStashIDs(),
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
}
|
||||
|
||||
65
internal/api/json.go
Normal file
65
internal/api/json.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// jsonNumberToNumber converts a JSON number to either a float64 or int64.
|
||||
func jsonNumberToNumber(n json.Number) interface{} {
|
||||
if strings.Contains(string(n), ".") {
|
||||
f, _ := n.Float64()
|
||||
return f
|
||||
}
|
||||
ret, _ := n.Int64()
|
||||
return ret
|
||||
}
|
||||
|
||||
// anyJSONNumberToNumber converts a JSON number using jsonNumberToNumber, otherwise it returns the existing value
|
||||
func anyJSONNumberToNumber(v any) any {
|
||||
if n, ok := v.(json.Number); ok {
|
||||
return jsonNumberToNumber(n)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// ConvertMapJSONNumbers converts all JSON numbers in a map to either float64 or int64.
|
||||
func convertMapJSONNumbers(m map[string]interface{}) (ret map[string]interface{}) {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret = make(map[string]interface{})
|
||||
for k, v := range m {
|
||||
if n, ok := v.(json.Number); ok {
|
||||
ret[k] = jsonNumberToNumber(n)
|
||||
} else if mm, ok := v.(map[string]interface{}); ok {
|
||||
ret[k] = convertMapJSONNumbers(mm)
|
||||
} else {
|
||||
ret[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func convertCustomFieldCriterionValues(c models.CustomFieldCriterionInput) models.CustomFieldCriterionInput {
|
||||
nv := make([]any, len(c.Value))
|
||||
for i, v := range c.Value {
|
||||
nv[i] = anyJSONNumberToNumber(v)
|
||||
}
|
||||
c.Value = nv
|
||||
return c
|
||||
}
|
||||
|
||||
func convertCustomFieldCriterionInputJSONNumbers(c []models.CustomFieldCriterionInput) []models.CustomFieldCriterionInput {
|
||||
ret := make([]models.CustomFieldCriterionInput, len(c))
|
||||
for i, v := range c {
|
||||
ret[i] = convertCustomFieldCriterionValues(v)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
60
internal/api/json_test.go
Normal file
60
internal/api/json_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvertMapJSONNumbers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input map[string]interface{}
|
||||
expected map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "Convert JSON numbers to numbers",
|
||||
input: map[string]interface{}{
|
||||
"int": json.Number("12"),
|
||||
"float": json.Number("12.34"),
|
||||
"string": "foo",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"int": int64(12),
|
||||
"float": 12.34,
|
||||
"string": "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Convert JSON numbers to numbers in nested maps",
|
||||
input: map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"int": json.Number("56"),
|
||||
"float": json.Number("56.78"),
|
||||
"nested-string": "bar",
|
||||
},
|
||||
"int": json.Number("12"),
|
||||
"float": json.Number("12.34"),
|
||||
"string": "foo",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"int": int64(56),
|
||||
"float": 56.78,
|
||||
"nested-string": "bar",
|
||||
},
|
||||
"int": int64(12),
|
||||
"float": 12.34,
|
||||
"string": "foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := convertMapJSONNumbers(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
221
internal/api/loaders/customfieldsloader_gen.go
Normal file
221
internal/api/loaders/customfieldsloader_gen.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// CustomFieldsLoaderConfig captures the config to create a new CustomFieldsLoader
|
||||
type CustomFieldsLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]models.CustomFieldMap, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewCustomFieldsLoader creates a new CustomFieldsLoader given a fetch, wait, and maxBatch
|
||||
func NewCustomFieldsLoader(config CustomFieldsLoaderConfig) *CustomFieldsLoader {
|
||||
return &CustomFieldsLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// CustomFieldsLoader batches and caches requests
|
||||
type CustomFieldsLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]models.CustomFieldMap, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]models.CustomFieldMap
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *customFieldsLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type customFieldsLoaderBatch struct {
|
||||
keys []int
|
||||
data []models.CustomFieldMap
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a CustomFieldMap by key, batching and caching will be applied automatically
|
||||
func (l *CustomFieldsLoader) Load(key int) (models.CustomFieldMap, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a CustomFieldMap.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *CustomFieldsLoader) LoadThunk(key int) func() (models.CustomFieldMap, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (models.CustomFieldMap, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &customFieldsLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (models.CustomFieldMap, error) {
|
||||
<-batch.done
|
||||
|
||||
var data models.CustomFieldMap
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *CustomFieldsLoader) LoadAll(keys []int) ([]models.CustomFieldMap, []error) {
|
||||
results := make([]func() (models.CustomFieldMap, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
customFieldMaps := make([]models.CustomFieldMap, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
customFieldMaps[i], errors[i] = thunk()
|
||||
}
|
||||
return customFieldMaps, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a CustomFieldMaps.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *CustomFieldsLoader) LoadAllThunk(keys []int) func() ([]models.CustomFieldMap, []error) {
|
||||
results := make([]func() (models.CustomFieldMap, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]models.CustomFieldMap, []error) {
|
||||
customFieldMaps := make([]models.CustomFieldMap, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
customFieldMaps[i], errors[i] = thunk()
|
||||
}
|
||||
return customFieldMaps, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *CustomFieldsLoader) Prime(key int, value models.CustomFieldMap) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
l.unsafeSet(key, value)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *CustomFieldsLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *CustomFieldsLoader) unsafeSet(key int, value models.CustomFieldMap) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]models.CustomFieldMap{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *customFieldsLoaderBatch) keyIndex(l *CustomFieldsLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *customFieldsLoaderBatch) startTimer(l *CustomFieldsLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *customFieldsLoaderBatch) end(l *CustomFieldsLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
//go:generate go run github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden CustomFieldsLoader int github.com/stashapp/stash/pkg/models.CustomFieldMap
|
||||
//go:generate go run github.com/vektah/dataloaden SceneOCountLoader int int
|
||||
//go:generate go run github.com/vektah/dataloaden ScenePlayCountLoader int int
|
||||
//go:generate go run github.com/vektah/dataloaden SceneOHistoryLoader int []time.Time
|
||||
@@ -51,13 +52,16 @@ type Loaders struct {
|
||||
ImageFiles *ImageFileIDsLoader
|
||||
GalleryFiles *GalleryFileIDsLoader
|
||||
|
||||
GalleryByID *GalleryLoader
|
||||
ImageByID *ImageLoader
|
||||
PerformerByID *PerformerLoader
|
||||
StudioByID *StudioLoader
|
||||
TagByID *TagLoader
|
||||
GroupByID *GroupLoader
|
||||
FileByID *FileLoader
|
||||
GalleryByID *GalleryLoader
|
||||
ImageByID *ImageLoader
|
||||
|
||||
PerformerByID *PerformerLoader
|
||||
PerformerCustomFields *CustomFieldsLoader
|
||||
|
||||
StudioByID *StudioLoader
|
||||
TagByID *TagLoader
|
||||
GroupByID *GroupLoader
|
||||
FileByID *FileLoader
|
||||
}
|
||||
|
||||
type Middleware struct {
|
||||
@@ -88,6 +92,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchPerformers(ctx),
|
||||
},
|
||||
PerformerCustomFields: &CustomFieldsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchPerformerCustomFields(ctx),
|
||||
},
|
||||
StudioByID: &StudioLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
@@ -214,6 +223,18 @@ func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*mo
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchPerformerCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
|
||||
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Performer.GetCustomFieldsBulk(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*models.Studio, []error) {
|
||||
return func(keys []int) (ret []*models.Studio, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -138,10 +137,6 @@ func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context)
|
||||
return r.repository.WithReadTxn(ctx, fn)
|
||||
}
|
||||
|
||||
func (r *Resolver) stashboxRepository() stashbox.Repository {
|
||||
return stashbox.NewRepository(r.repository)
|
||||
}
|
||||
|
||||
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.Wall(ctx, q)
|
||||
|
||||
@@ -18,11 +18,6 @@ func (r *imageResolver) getFiles(ctx context.Context, obj *models.Image) ([]mode
|
||||
return files, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string, error) {
|
||||
ret := obj.GetTitle()
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) VisualFiles(ctx context.Context, obj *models.Image) ([]VisualFile, error) {
|
||||
files, err := r.getFiles(ctx, obj)
|
||||
if err != nil {
|
||||
|
||||
@@ -268,6 +268,19 @@ func (r *performerResolver) Groups(ctx context.Context, obj *models.Performer) (
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) CustomFields(ctx context.Context, obj *models.Performer) (map[string]interface{}, error) {
|
||||
m, err := loaders.From(ctx).PerformerCustomFields.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Group, err error) {
|
||||
return r.Groups(ctx, obj)
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
@@ -643,10 +644,14 @@ func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]int
|
||||
c := config.GetInstance()
|
||||
|
||||
if input != nil {
|
||||
// #5483 - convert JSON numbers to float64 or int64
|
||||
input = convertMapJSONNumbers(input)
|
||||
c.SetUIConfiguration(input)
|
||||
}
|
||||
|
||||
if partial != nil {
|
||||
// #5483 - convert JSON numbers to float64 or int64
|
||||
partial = convertMapJSONNumbers(partial)
|
||||
// merge partial into existing config
|
||||
existing := c.GetUIConfiguration()
|
||||
utils.MergeMaps(existing, partial)
|
||||
@@ -664,6 +669,14 @@ func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, v
|
||||
c := config.GetInstance()
|
||||
|
||||
cfg := utils.NestedMap(c.GetUIConfiguration())
|
||||
|
||||
// #5483 - convert JSON numbers to float64 or int64
|
||||
if m, ok := value.(map[string]interface{}); ok {
|
||||
value = convertMapJSONNumbers(m)
|
||||
} else if n, ok := value.(json.Number); ok {
|
||||
value = jsonNumberToNumber(n)
|
||||
}
|
||||
|
||||
cfg.Set(key, value)
|
||||
|
||||
return r.ConfigureUI(ctx, cfg, nil)
|
||||
@@ -671,6 +684,9 @@ func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, v
|
||||
|
||||
func (r *mutationResolver) ConfigurePlugin(ctx context.Context, pluginID string, input map[string]interface{}) (map[string]interface{}, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
// #5483 - convert JSON numbers to float64 or int64
|
||||
input = convertMapJSONNumbers(input)
|
||||
c.SetPluginConfiguration(pluginID, input)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
|
||||
@@ -28,7 +28,7 @@ func (r *mutationResolver) getImage(ctx context.Context, id int) (ret *models.Im
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageUpdate(ctx context.Context, input ImageUpdateInput) (ret *models.Image, err error) {
|
||||
func (r *mutationResolver) ImageUpdate(ctx context.Context, input models.ImageUpdateInput) (ret *models.Image, err error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (r *mutationResolver) ImageUpdate(ctx context.Context, input ImageUpdateInp
|
||||
return r.getImage(ctx, ret.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdateInput) (ret []*models.Image, err error) {
|
||||
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*models.ImageUpdateInput) (ret []*models.Image, err error) {
|
||||
inputMaps := getUpdateInputMaps(ctx)
|
||||
|
||||
// Start the transaction and save the image
|
||||
@@ -89,7 +89,7 @@ func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdat
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInput, translator changesetTranslator) (*models.Image, error) {
|
||||
func (r *mutationResolver) imageUpdate(ctx context.Context, input models.ImageUpdateInput, translator changesetTranslator) (*models.Image, error) {
|
||||
imageID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
|
||||
@@ -58,7 +58,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
newPerformer.Height = input.HeightCm
|
||||
newPerformer.Weight = input.Weight
|
||||
newPerformer.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds)
|
||||
newPerformer.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
|
||||
|
||||
newPerformer.URLs = models.NewRelatedStrings([]string{})
|
||||
if input.URL != nil {
|
||||
@@ -108,7 +108,13 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
return err
|
||||
}
|
||||
|
||||
err = qb.Create(ctx, &newPerformer)
|
||||
i := &models.CreatePerformerInput{
|
||||
Performer: &newPerformer,
|
||||
// convert json.Numbers to int/float
|
||||
CustomFields: convertMapJSONNumbers(input.CustomFields),
|
||||
}
|
||||
|
||||
err = qb.Create(ctx, i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -290,6 +296,11 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
updatedPerformer.CustomFields = input.CustomFields
|
||||
// convert json.Numbers to int/float
|
||||
updatedPerformer.CustomFields.Full = convertMapJSONNumbers(updatedPerformer.CustomFields.Full)
|
||||
updatedPerformer.CustomFields.Partial = convertMapJSONNumbers(updatedPerformer.CustomFields.Partial)
|
||||
|
||||
var imageData []byte
|
||||
imageIncluded := translator.hasField("image")
|
||||
if input.Image != nil {
|
||||
|
||||
@@ -50,7 +50,7 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr
|
||||
newScene.Director = translator.string(input.Director)
|
||||
newScene.Rating = input.Rating100
|
||||
newScene.Organized = translator.bool(input.Organized)
|
||||
newScene.StashIDs = models.NewRelatedStashIDs(input.StashIds)
|
||||
newScene.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
|
||||
|
||||
newScene.Date, err = translator.datePtr(input.Date)
|
||||
if err != nil {
|
||||
@@ -655,6 +655,13 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
|
||||
newMarker.PrimaryTagID = primaryTagID
|
||||
newMarker.SceneID = sceneID
|
||||
|
||||
if input.EndSeconds != nil {
|
||||
if err := validateSceneMarkerEndSeconds(newMarker.Seconds, *input.EndSeconds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newMarker.EndSeconds = input.EndSeconds
|
||||
}
|
||||
|
||||
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
@@ -680,6 +687,13 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
|
||||
return r.getSceneMarker(ctx, newMarker.ID)
|
||||
}
|
||||
|
||||
func validateSceneMarkerEndSeconds(seconds, endSeconds float64) error {
|
||||
if endSeconds < seconds {
|
||||
return fmt.Errorf("end_seconds (%f) must be greater than or equal to seconds (%f)", endSeconds, seconds)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) {
|
||||
markerID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -695,6 +709,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
|
||||
|
||||
updatedMarker.Title = translator.optionalString(input.Title, "title")
|
||||
updatedMarker.Seconds = translator.optionalFloat64(input.Seconds, "seconds")
|
||||
updatedMarker.EndSeconds = translator.optionalFloat64(input.EndSeconds, "end_seconds")
|
||||
updatedMarker.SceneID, err = translator.optionalIntFromString(input.SceneID, "scene_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting scene id: %w", err)
|
||||
@@ -735,6 +750,26 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
|
||||
return fmt.Errorf("scene marker with id %d not found", markerID)
|
||||
}
|
||||
|
||||
// Validate end_seconds
|
||||
shouldValidateEndSeconds := (updatedMarker.Seconds.Set || updatedMarker.EndSeconds.Set) && !updatedMarker.EndSeconds.Null
|
||||
if shouldValidateEndSeconds {
|
||||
seconds := existingMarker.Seconds
|
||||
if updatedMarker.Seconds.Set {
|
||||
seconds = updatedMarker.Seconds.Value
|
||||
}
|
||||
|
||||
endSeconds := existingMarker.EndSeconds
|
||||
if updatedMarker.EndSeconds.Set {
|
||||
endSeconds = &updatedMarker.EndSeconds.Value
|
||||
}
|
||||
|
||||
if endSeconds != nil {
|
||||
if err := validateSceneMarkerEndSeconds(seconds, *endSeconds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newMarker, err := qb.UpdatePartial(ctx, markerID, updatedMarker)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -749,7 +784,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
|
||||
}
|
||||
|
||||
// remove the marker preview if the scene changed or if the timestamp was changed
|
||||
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds {
|
||||
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds || existingMarker.EndSeconds != newMarker.EndSeconds {
|
||||
seconds := int(existingMarker.Seconds)
|
||||
if err := fileDeleter.MarkMarkerFiles(existingScene, seconds); err != nil {
|
||||
return err
|
||||
@@ -779,11 +814,16 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) {
|
||||
markerID, err := strconv.Atoi(id)
|
||||
return r.SceneMarkersDestroy(ctx, []string{id})
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkersDestroy(ctx context.Context, markerIDs []string) (bool, error) {
|
||||
ids, err := stringslice.StringSliceToIntSlice(markerIDs)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
return false, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
var markers []*models.SceneMarker
|
||||
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
|
||||
|
||||
fileDeleter := &scene.FileDeleter{
|
||||
@@ -796,35 +836,45 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
|
||||
qb := r.repository.SceneMarker
|
||||
sqb := r.repository.Scene
|
||||
|
||||
marker, err := qb.Find(ctx, markerID)
|
||||
for _, markerID := range ids {
|
||||
marker, err := qb.Find(ctx, markerID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if marker == nil {
|
||||
return fmt.Errorf("scene marker with id %d not found", markerID)
|
||||
}
|
||||
|
||||
s, err := sqb.Find(ctx, marker.SceneID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return fmt.Errorf("scene with id %d not found", marker.SceneID)
|
||||
}
|
||||
|
||||
markers = append(markers, marker)
|
||||
|
||||
if err := scene.DestroyMarker(ctx, s, marker, qb, fileDeleter); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if marker == nil {
|
||||
return fmt.Errorf("scene marker with id %d not found", markerID)
|
||||
}
|
||||
|
||||
s, err := sqb.Find(ctx, marker.SceneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return fmt.Errorf("scene with id %d not found", marker.SceneID)
|
||||
}
|
||||
|
||||
return scene.DestroyMarker(ctx, s, marker, qb, fileDeleter)
|
||||
return nil
|
||||
}); err != nil {
|
||||
fileDeleter.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
// perform the post-commit actions
|
||||
fileDeleter.Commit()
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerDestroyPost, id, nil)
|
||||
for _, marker := range markers {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, marker.ID, hook.SceneMarkerDestroyPost, markerIDs, nil)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/stashbox"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input StashBoxFingerprintSubmissionInput) (bool, error) {
|
||||
@@ -15,8 +19,23 @@ func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input
|
||||
return false, err
|
||||
}
|
||||
|
||||
ids, err := stringslice.StringSliceToIntSlice(input.SceneIds)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
client := r.newStashBoxClient(*b)
|
||||
return client.SubmitStashBoxFingerprints(ctx, input.SceneIds)
|
||||
|
||||
var scenes []*models.Scene
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
scenes, err = r.sceneService.FindMany(ctx, ids, scene.LoadStashIDs, scene.LoadFiles)
|
||||
return err
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return client.SubmitFingerprints(ctx, scenes)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) {
|
||||
@@ -69,17 +88,76 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
|
||||
logger.Errorf("Error getting scene cover: %v", err)
|
||||
}
|
||||
|
||||
if err := scene.LoadURLs(ctx, r.repository.Scene); err != nil {
|
||||
return fmt.Errorf("loading scene URLs: %w", err)
|
||||
draft, err := r.makeSceneDraft(ctx, scene, cover)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err = client.SubmitSceneDraft(ctx, scene, cover)
|
||||
res, err = client.SubmitSceneDraft(ctx, *draft)
|
||||
return err
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) makeSceneDraft(ctx context.Context, s *models.Scene, cover []byte) (*stashbox.SceneDraft, error) {
|
||||
if err := s.LoadURLs(ctx, r.repository.Scene); err != nil {
|
||||
return nil, fmt.Errorf("loading scene URLs: %w", err)
|
||||
}
|
||||
|
||||
if err := s.LoadStashIDs(ctx, r.repository.Scene); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
draft := &stashbox.SceneDraft{
|
||||
Scene: s,
|
||||
}
|
||||
|
||||
pqb := r.repository.Performer
|
||||
sqb := r.repository.Studio
|
||||
|
||||
if s.StudioID != nil {
|
||||
var err error
|
||||
draft.Studio, err = sqb.Find(ctx, *s.StudioID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if draft.Studio == nil {
|
||||
return nil, fmt.Errorf("studio with id %d not found", *s.StudioID)
|
||||
}
|
||||
|
||||
if err := draft.Studio.LoadStashIDs(ctx, r.repository.Studio); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// submit all file fingerprints
|
||||
if err := s.LoadFiles(ctx, r.repository.Scene); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scenePerformers, err := pqb.FindBySceneID(ctx, s.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range scenePerformers {
|
||||
if err := p.LoadStashIDs(ctx, pqb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
draft.Performers = scenePerformers
|
||||
|
||||
draft.Tags, err = r.repository.Tag.FindBySceneID(ctx, s.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
draft.Cover = cover
|
||||
|
||||
return draft, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
|
||||
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
@@ -105,7 +183,22 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
|
||||
return fmt.Errorf("performer with id %d not found", id)
|
||||
}
|
||||
|
||||
res, err = client.SubmitPerformerDraft(ctx, performer)
|
||||
pqb := r.repository.Performer
|
||||
if err := performer.LoadAliases(ctx, pqb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := performer.LoadURLs(ctx, pqb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := performer.LoadStashIDs(ctx, pqb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
img, _ := pqb.GetImage(ctx, performer.ID)
|
||||
|
||||
res, err = client.SubmitPerformerDraft(ctx, performer, img)
|
||||
return err
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||
newTag := models.NewTag()
|
||||
|
||||
newTag.Name = input.Name
|
||||
newTag.SortName = translator.string(input.SortName)
|
||||
newTag.Aliases = models.NewRelatedStrings(input.Aliases)
|
||||
newTag.Favorite = translator.bool(input.Favorite)
|
||||
newTag.Description = translator.string(input.Description)
|
||||
@@ -102,6 +103,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||
updatedTag := models.NewTagPartial()
|
||||
|
||||
updatedTag.Name = translator.optionalString(input.Name, "name")
|
||||
updatedTag.SortName = translator.optionalString(input.SortName, "sort_name")
|
||||
updatedTag.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedTag.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
updatedTag.Description = translator.optionalString(input.Description, "description")
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
"golang.org/x/text/collate"
|
||||
)
|
||||
|
||||
@@ -241,7 +240,7 @@ func makeConfigUIResult() map[string]interface{} {
|
||||
|
||||
func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input config.StashBoxInput) (*StashBoxValidationResult, error) {
|
||||
box := models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}
|
||||
client := stashbox.NewClient(box, r.stashboxRepository())
|
||||
client := r.newStashBoxClient(box)
|
||||
|
||||
user, err := client.GetUser(ctx)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -32,6 +32,11 @@ func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *mod
|
||||
}
|
||||
}
|
||||
|
||||
// #5682 - convert JSON numbers to float64 or int64
|
||||
if performerFilter != nil {
|
||||
performerFilter.CustomFields = convertCustomFieldCriterionInputJSONNumbers(performerFilter.CustomFields)
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var performers []*models.Performer
|
||||
var err error
|
||||
|
||||
@@ -2,13 +2,13 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
@@ -119,11 +119,11 @@ func (r *queryResolver) FindScenes(
|
||||
result, err = r.repository.Scene.Query(ctx, models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
Count: slices.Contains(fields, "count"),
|
||||
},
|
||||
SceneFilter: sceneFilter,
|
||||
TotalDuration: sliceutil.Contains(fields, "duration"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
TotalDuration: slices.Contains(fields, "duration"),
|
||||
TotalSize: slices.Contains(fields, "filesize"),
|
||||
})
|
||||
if err == nil {
|
||||
scenes, err = result.Resolve(ctx)
|
||||
@@ -174,11 +174,11 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
|
||||
result, err := r.repository.Scene.Query(ctx, models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: queryFilter,
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
Count: slices.Contains(fields, "count"),
|
||||
},
|
||||
SceneFilter: sceneFilter,
|
||||
TotalDuration: sliceutil.Contains(fields, "duration"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
TotalDuration: slices.Contains(fields, "duration"),
|
||||
TotalSize: slices.Contains(fields, "filesize"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,14 +4,31 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (ret *FindSceneMarkersResultType, err error) {
|
||||
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType, ids []string) (ret *FindSceneMarkersResultType, err error) {
|
||||
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
sceneMarkers, total, err := r.repository.SceneMarker.Query(ctx, sceneMarkerFilter, filter)
|
||||
var sceneMarkers []*models.SceneMarker
|
||||
var err error
|
||||
var total int
|
||||
|
||||
if len(idInts) > 0 {
|
||||
sceneMarkers, err = r.repository.SceneMarker.FindMany(ctx, idInts)
|
||||
total = len(sceneMarkers)
|
||||
} else {
|
||||
sceneMarkers, total, err = r.repository.SceneMarker.Query(ctx, sceneMarkerFilter, filter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &FindSceneMarkersResultType{
|
||||
Count: total,
|
||||
SceneMarkers: sceneMarkers,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,15 +4,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
@@ -34,7 +30,7 @@ func (r *queryResolver) ScrapePerformerURL(ctx context.Context, url string) (*mo
|
||||
return marshalScrapedPerformer(content)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string, query string) ([]*scraper.ScrapedScene, error) {
|
||||
func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string, query string) ([]*models.ScrapedScene, error) {
|
||||
if query == "" {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -49,119 +45,10 @@ func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filterSceneTags(ret)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func compileRegexps(patterns []string) []*regexp.Regexp {
|
||||
excludePatterns := patterns
|
||||
var excludeRegexps []*regexp.Regexp
|
||||
|
||||
for _, excludePattern := range excludePatterns {
|
||||
reg, err := regexp.Compile(strings.ToLower(excludePattern))
|
||||
if err != nil {
|
||||
logger.Errorf("Invalid tag exclusion pattern: %v", err)
|
||||
} else {
|
||||
excludeRegexps = append(excludeRegexps, reg)
|
||||
}
|
||||
}
|
||||
|
||||
return excludeRegexps
|
||||
}
|
||||
|
||||
// filterSceneTags removes tags matching excluded tag patterns from the provided scraped scenes
|
||||
func filterTags(excludeRegexps []*regexp.Regexp, tags []*models.ScrapedTag) (newTags []*models.ScrapedTag, ignoredTags []string) {
|
||||
if len(excludeRegexps) == 0 {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
ignore := false
|
||||
for _, reg := range excludeRegexps {
|
||||
if reg.MatchString(strings.ToLower(t.Name)) {
|
||||
ignore = true
|
||||
ignoredTags = sliceutil.AppendUnique(ignoredTags, t.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ignore {
|
||||
newTags = append(newTags, t)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// filterSceneTags removes tags matching excluded tag patterns from the provided scraped scenes
|
||||
func filterSceneTags(scenes []*scraper.ScrapedScene) {
|
||||
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())
|
||||
|
||||
var ignoredTags []string
|
||||
|
||||
for _, s := range scenes {
|
||||
var ignored []string
|
||||
s.Tags, ignored = filterTags(excludeRegexps, s.Tags)
|
||||
ignoredTags = sliceutil.AppendUniques(ignoredTags, ignored)
|
||||
}
|
||||
|
||||
if len(ignoredTags) > 0 {
|
||||
logger.Debugf("Scraping ignored tags: %s", strings.Join(ignoredTags, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// filterGalleryTags removes tags matching excluded tag patterns from the provided scraped galleries
|
||||
func filterGalleryTags(g []*scraper.ScrapedGallery) {
|
||||
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())
|
||||
|
||||
var ignoredTags []string
|
||||
|
||||
for _, s := range g {
|
||||
var ignored []string
|
||||
s.Tags, ignored = filterTags(excludeRegexps, s.Tags)
|
||||
ignoredTags = sliceutil.AppendUniques(ignoredTags, ignored)
|
||||
}
|
||||
|
||||
if len(ignoredTags) > 0 {
|
||||
logger.Debugf("Scraping ignored tags: %s", strings.Join(ignoredTags, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// filterGalleryTags removes tags matching excluded tag patterns from the provided scraped galleries
|
||||
func filterPerformerTags(p []*models.ScrapedPerformer) {
|
||||
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())
|
||||
|
||||
var ignoredTags []string
|
||||
|
||||
for _, s := range p {
|
||||
var ignored []string
|
||||
s.Tags, ignored = filterTags(excludeRegexps, s.Tags)
|
||||
ignoredTags = sliceutil.AppendUniques(ignoredTags, ignored)
|
||||
}
|
||||
|
||||
if len(ignoredTags) > 0 {
|
||||
logger.Debugf("Scraping ignored tags: %s", strings.Join(ignoredTags, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// filterGroupTags removes tags matching excluded tag patterns from the provided scraped movies
|
||||
func filterGroupTags(p []*models.ScrapedMovie) {
|
||||
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())
|
||||
|
||||
var ignoredTags []string
|
||||
|
||||
for _, s := range p {
|
||||
var ignored []string
|
||||
s.Tags, ignored = filterTags(excludeRegexps, s.Tags)
|
||||
ignoredTags = sliceutil.AppendUniques(ignoredTags, ignored)
|
||||
}
|
||||
|
||||
if len(ignoredTags) > 0 {
|
||||
logger.Debugf("Scraping ignored tags: %s", strings.Join(ignoredTags, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*scraper.ScrapedScene, error) {
|
||||
func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*models.ScrapedScene, error) {
|
||||
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeScene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -172,14 +59,10 @@ func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*scrape
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ret != nil {
|
||||
filterSceneTags([]*scraper.ScrapedScene{ret})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*scraper.ScrapedGallery, error) {
|
||||
func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*models.ScrapedGallery, error) {
|
||||
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeGallery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -190,11 +73,16 @@ func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*scra
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ret != nil {
|
||||
filterGalleryTags([]*scraper.ScrapedGallery{ret})
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeImageURL(ctx context.Context, url string) (*models.ScrapedImage, error) {
|
||||
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return marshalScrapedImage(content)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models.ScrapedMovie, error) {
|
||||
@@ -208,8 +96,6 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filterGroupTags([]*models.ScrapedMovie{ret})
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -224,8 +110,6 @@ func (r *queryResolver) ScrapeGroupURL(ctx context.Context, url string) (*models
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filterGroupTags([]*models.ScrapedMovie{ret})
|
||||
|
||||
// convert to scraped group
|
||||
group := &models.ScrapedGroup{
|
||||
StoredID: ret.StoredID,
|
||||
@@ -246,8 +130,8 @@ func (r *queryResolver) ScrapeGroupURL(ctx context.Context, url string) (*models
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*scraper.ScrapedScene, error) {
|
||||
var ret []*scraper.ScrapedScene
|
||||
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*models.ScrapedScene, error) {
|
||||
var ret []*models.ScrapedScene
|
||||
|
||||
var sceneID int
|
||||
if input.SceneID != nil {
|
||||
@@ -299,9 +183,14 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So
|
||||
|
||||
switch {
|
||||
case input.SceneID != nil:
|
||||
ret, err = client.FindStashBoxSceneByFingerprints(ctx, sceneID)
|
||||
var fps []models.Fingerprints
|
||||
fps, err = r.getScenesFingerprints(ctx, []int{sceneID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret, err = client.FindSceneByFingerprints(ctx, fps[0])
|
||||
case input.Query != nil:
|
||||
ret, err = client.QueryStashBoxScene(ctx, *input.Query)
|
||||
ret, err = client.QueryScene(ctx, *input.Query)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: scene_id or query must be set", ErrInput)
|
||||
}
|
||||
@@ -309,16 +198,19 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO - this should happen after any scene is scraped
|
||||
if err := r.matchScenesRelationships(ctx, ret, *source.StashBoxEndpoint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: scraper_id or stash_box_index must be set", ErrInput)
|
||||
}
|
||||
|
||||
filterSceneTags(ret)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.Source, input ScrapeMultiScenesInput) ([][]*scraper.ScrapedScene, error) {
|
||||
func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.Source, input ScrapeMultiScenesInput) ([][]*models.ScrapedScene, error) {
|
||||
if source.ScraperID != nil {
|
||||
return nil, ErrNotImplemented
|
||||
} else if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
|
||||
@@ -334,12 +226,89 @@ func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.So
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.FindStashBoxScenesByFingerprints(ctx, sceneIDs)
|
||||
fps, err := r.getScenesFingerprints(ctx, sceneIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret, err := client.FindScenesByFingerprints(ctx, fps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// match relationships - this mutates the existing scenes so we can
|
||||
// just flatten the slice and pass it in
|
||||
flat := sliceutil.Flatten(ret)
|
||||
|
||||
if err := r.matchScenesRelationships(ctx, flat, *source.StashBoxEndpoint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("scraper_id or stash_box_index must be set")
|
||||
}
|
||||
|
||||
func (r *queryResolver) getScenesFingerprints(ctx context.Context, ids []int) ([]models.Fingerprints, error) {
|
||||
fingerprints := make([]models.Fingerprints, len(ids))
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
for i, sceneID := range ids {
|
||||
scene, err := qb.Find(ctx, sceneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if scene == nil {
|
||||
return fmt.Errorf("scene with id %d not found", sceneID)
|
||||
}
|
||||
|
||||
if err := scene.LoadFiles(ctx, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var sceneFPs models.Fingerprints
|
||||
|
||||
for _, f := range scene.Files.List() {
|
||||
sceneFPs = append(sceneFPs, f.Fingerprints...)
|
||||
}
|
||||
|
||||
fingerprints[i] = sceneFPs
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fingerprints, nil
|
||||
}
|
||||
|
||||
// matchSceneRelationships accepts scraped scenes and attempts to match its relationships to existing stash models.
|
||||
func (r *queryResolver) matchScenesRelationships(ctx context.Context, ss []*models.ScrapedScene, endpoint string) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
matcher := match.SceneRelationships{
|
||||
PerformerFinder: r.repository.Performer,
|
||||
TagFinder: r.repository.Tag,
|
||||
StudioFinder: r.repository.Studio,
|
||||
}
|
||||
|
||||
for _, s := range ss {
|
||||
if err := matcher.MatchRelationships(ctx, s, endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.Source, input ScrapeSingleStudioInput) ([]*models.ScrapedStudio, error) {
|
||||
if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
|
||||
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
|
||||
@@ -350,7 +319,7 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
var ret []*models.ScrapedStudio
|
||||
out, err := client.FindStashBoxStudio(ctx, *input.Query)
|
||||
out, err := client.FindStudio(ctx, *input.Query)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -359,6 +328,17 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S
|
||||
}
|
||||
|
||||
if len(ret) > 0 {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
for _, studio := range ret {
|
||||
if err := match.ScrapedStudioHierarchy(ctx, r.repository.Studio, studio, *source.StashBoxEndpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -404,29 +384,33 @@ func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scrape
|
||||
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
var res []*stashbox.StashBoxPerformerQueryResult
|
||||
var query string
|
||||
switch {
|
||||
case input.PerformerID != nil:
|
||||
res, err = client.FindStashBoxPerformersByNames(ctx, []string{*input.PerformerID})
|
||||
names, err := r.findPerformerNames(ctx, []string{*input.PerformerID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query = names[0]
|
||||
case input.Query != nil:
|
||||
res, err = client.QueryStashBoxPerformer(ctx, *input.Query)
|
||||
query = *input.Query
|
||||
default:
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
if query == "" {
|
||||
return nil, nil
|
||||
}
|
||||
ret, err = client.QueryPerformer(ctx, query)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
ret = res[0].Results
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("scraper_id or stash_box_index must be set")
|
||||
}
|
||||
|
||||
filterPerformerTags(ret)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -434,6 +418,11 @@ func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source scrape
|
||||
if source.ScraperID != nil {
|
||||
return nil, ErrNotImplemented
|
||||
} else if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
|
||||
names, err := r.findPerformerNames(ctx, input.PerformerIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -441,14 +430,40 @@ func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source scrape
|
||||
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
return client.FindStashBoxPerformersByPerformerNames(ctx, input.PerformerIds)
|
||||
return client.QueryPerformers(ctx, names)
|
||||
}
|
||||
|
||||
return nil, errors.New("scraper_id or stash_box_index must be set")
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.Source, input ScrapeSingleGalleryInput) ([]*scraper.ScrapedGallery, error) {
|
||||
var ret []*scraper.ScrapedGallery
|
||||
func (r *queryResolver) findPerformerNames(ctx context.Context, performerIDs []string) ([]string, error) {
|
||||
ids, err := stringslice.StringSliceToIntSlice(performerIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
names := make([]string, len(ids))
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
p, err := r.repository.Performer.FindMany(ctx, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, pp := range p {
|
||||
names[i] = pp.Name
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.Source, input ScrapeSingleGalleryInput) ([]*models.ScrapedGallery, error) {
|
||||
var ret []*models.ScrapedGallery
|
||||
|
||||
if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
|
||||
return nil, ErrNotSupported
|
||||
@@ -487,10 +502,42 @@ func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
filterGalleryTags(ret)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleImage(ctx context.Context, source scraper.Source, input ScrapeSingleImageInput) ([]*models.ScrapedImage, error) {
|
||||
if source.StashBoxIndex != nil {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
if source.ScraperID == nil {
|
||||
return nil, fmt.Errorf("%w: scraper_id must be set", ErrInput)
|
||||
}
|
||||
|
||||
var c scraper.ScrapedContent
|
||||
|
||||
switch {
|
||||
case input.ImageID != nil:
|
||||
imageID, err := strconv.Atoi(*input.ImageID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: image id is not an integer: '%s'", ErrInput, *input.ImageID)
|
||||
}
|
||||
c, err = r.scraperCache().ScrapeID(ctx, *source.ScraperID, imageID, scraper.ScrapeContentTypeImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marshalScrapedImages([]scraper.ScrapedContent{c})
|
||||
case input.ImageInput != nil:
|
||||
c, err := r.scraperCache().ScrapeFragment(ctx, *source.ScraperID, scraper.Input{Image: input.ImageInput})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marshalScrapedImages([]scraper.ScrapedContent{c})
|
||||
default:
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleMovie(ctx context.Context, source scraper.Source, input ScrapeSingleMovieInput) ([]*models.ScrapedMovie, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
// marshalScrapedScenes converts ScrapedContent into ScrapedScene. If conversion fails, an
|
||||
// error is returned to the caller.
|
||||
func marshalScrapedScenes(content []scraper.ScrapedContent) ([]*scraper.ScrapedScene, error) {
|
||||
var ret []*scraper.ScrapedScene
|
||||
func marshalScrapedScenes(content []scraper.ScrapedContent) ([]*models.ScrapedScene, error) {
|
||||
var ret []*models.ScrapedScene
|
||||
for _, c := range content {
|
||||
if c == nil {
|
||||
// graphql schema requires scenes to be non-nil
|
||||
@@ -18,9 +18,9 @@ func marshalScrapedScenes(content []scraper.ScrapedContent) ([]*scraper.ScrapedS
|
||||
}
|
||||
|
||||
switch s := c.(type) {
|
||||
case *scraper.ScrapedScene:
|
||||
case *models.ScrapedScene:
|
||||
ret = append(ret, s)
|
||||
case scraper.ScrapedScene:
|
||||
case models.ScrapedScene:
|
||||
ret = append(ret, &s)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedScene", models.ErrConversion)
|
||||
@@ -55,8 +55,8 @@ func marshalScrapedPerformers(content []scraper.ScrapedContent) ([]*models.Scrap
|
||||
|
||||
// marshalScrapedGalleries converts ScrapedContent into ScrapedGallery. If
|
||||
// conversion fails, an error is returned.
|
||||
func marshalScrapedGalleries(content []scraper.ScrapedContent) ([]*scraper.ScrapedGallery, error) {
|
||||
var ret []*scraper.ScrapedGallery
|
||||
func marshalScrapedGalleries(content []scraper.ScrapedContent) ([]*models.ScrapedGallery, error) {
|
||||
var ret []*models.ScrapedGallery
|
||||
for _, c := range content {
|
||||
if c == nil {
|
||||
// graphql schema requires galleries to be non-nil
|
||||
@@ -64,9 +64,9 @@ func marshalScrapedGalleries(content []scraper.ScrapedContent) ([]*scraper.Scrap
|
||||
}
|
||||
|
||||
switch g := c.(type) {
|
||||
case *scraper.ScrapedGallery:
|
||||
case *models.ScrapedGallery:
|
||||
ret = append(ret, g)
|
||||
case scraper.ScrapedGallery:
|
||||
case models.ScrapedGallery:
|
||||
ret = append(ret, &g)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedGallery", models.ErrConversion)
|
||||
@@ -76,6 +76,27 @@ func marshalScrapedGalleries(content []scraper.ScrapedContent) ([]*scraper.Scrap
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func marshalScrapedImages(content []scraper.ScrapedContent) ([]*models.ScrapedImage, error) {
|
||||
var ret []*models.ScrapedImage
|
||||
for _, c := range content {
|
||||
if c == nil {
|
||||
// graphql schema requires images to be non-nil
|
||||
continue
|
||||
}
|
||||
|
||||
switch g := c.(type) {
|
||||
case *models.ScrapedImage:
|
||||
ret = append(ret, g)
|
||||
case models.ScrapedImage:
|
||||
ret = append(ret, &g)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedImage", models.ErrConversion)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// marshalScrapedMovies converts ScrapedContent into ScrapedMovie. If conversion
|
||||
// fails, an error is returned.
|
||||
func marshalScrapedMovies(content []scraper.ScrapedContent) ([]*models.ScrapedMovie, error) {
|
||||
@@ -110,7 +131,7 @@ func marshalScrapedPerformer(content scraper.ScrapedContent) (*models.ScrapedPer
|
||||
}
|
||||
|
||||
// marshalScrapedScene will marshal a single scraped scene
|
||||
func marshalScrapedScene(content scraper.ScrapedContent) (*scraper.ScrapedScene, error) {
|
||||
func marshalScrapedScene(content scraper.ScrapedContent) (*models.ScrapedScene, error) {
|
||||
s, err := marshalScrapedScenes([]scraper.ScrapedContent{content})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -120,7 +141,7 @@ func marshalScrapedScene(content scraper.ScrapedContent) (*scraper.ScrapedScene,
|
||||
}
|
||||
|
||||
// marshalScrapedGallery will marshal a single scraped gallery
|
||||
func marshalScrapedGallery(content scraper.ScrapedContent) (*scraper.ScrapedGallery, error) {
|
||||
func marshalScrapedGallery(content scraper.ScrapedContent) (*models.ScrapedGallery, error) {
|
||||
g, err := marshalScrapedGalleries([]scraper.ScrapedContent{content})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -129,6 +150,16 @@ func marshalScrapedGallery(content scraper.ScrapedContent) (*scraper.ScrapedGall
|
||||
return g[0], nil
|
||||
}
|
||||
|
||||
// marshalScrapedImage will marshal a single scraped image
|
||||
func marshalScrapedImage(content scraper.ScrapedContent) (*models.ScrapedImage, error) {
|
||||
g, err := marshalScrapedImages([]scraper.ScrapedContent{content})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return g[0], nil
|
||||
}
|
||||
|
||||
// marshalScrapedMovie will marshal a single scraped movie
|
||||
func marshalScrapedMovie(content scraper.ScrapedContent) (*models.ScrapedMovie, error) {
|
||||
m, err := marshalScrapedMovies([]scraper.ScrapedContent{content})
|
||||
|
||||
@@ -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"
|
||||
@@ -40,10 +41,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
loginEndpoint = "/login"
|
||||
logoutEndpoint = "/logout"
|
||||
gqlEndpoint = "/graphql"
|
||||
playgroundEndpoint = "/playground"
|
||||
loginEndpoint = "/login"
|
||||
loginLocaleEndpoint = loginEndpoint + "/locale"
|
||||
logoutEndpoint = "/logout"
|
||||
gqlEndpoint = "/graphql"
|
||||
playgroundEndpoint = "/playground"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
@@ -185,7 +187,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)
|
||||
@@ -227,6 +229,7 @@ func Initialize() (*Server, error) {
|
||||
r.Get(loginEndpoint, handleLogin())
|
||||
r.Post(loginEndpoint, handleLoginPost())
|
||||
r.Get(logoutEndpoint, handleLogout())
|
||||
r.Get(loginLocaleEndpoint, handleLoginLocale(cfg))
|
||||
r.HandleFunc(loginEndpoint+"/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, loginEndpoint)
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
|
||||
@@ -17,7 +17,11 @@ import (
|
||||
"github.com/stashapp/stash/ui"
|
||||
)
|
||||
|
||||
const returnURLParam = "returnURL"
|
||||
const (
|
||||
returnURLParam = "returnURL"
|
||||
|
||||
defaultLocale = "en-GB"
|
||||
)
|
||||
|
||||
func getLoginPage() []byte {
|
||||
data, err := fs.ReadFile(ui.LoginUIBox, "login.html")
|
||||
@@ -58,6 +62,47 @@ func serveLoginPage(w http.ResponseWriter, r *http.Request, returnURL string, lo
|
||||
utils.ServeStaticContent(w, r, buffer.Bytes())
|
||||
}
|
||||
|
||||
func handleLoginLocale(cfg *config.Config) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// get the locale from the config
|
||||
lang := cfg.GetLanguage()
|
||||
if lang == "" {
|
||||
lang = defaultLocale
|
||||
}
|
||||
|
||||
data, err := getLoginLocale(lang)
|
||||
if err != nil {
|
||||
logger.Debugf("Failed to load login locale file for language %s: %v", lang, err)
|
||||
// try again with the default language
|
||||
if lang != defaultLocale {
|
||||
data, err = getLoginLocale(defaultLocale)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to load login locale file for default language %s: %v", defaultLocale, err)
|
||||
}
|
||||
}
|
||||
|
||||
// if there's still an error, response with an internal server error
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to load login locale file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// write a script to set the locale string map as a global variable
|
||||
localeScript := fmt.Sprintf("var localeStrings = %s;", data)
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
_, _ = w.Write([]byte(localeScript))
|
||||
}
|
||||
}
|
||||
|
||||
func getLoginLocale(lang string) ([]byte, error) {
|
||||
data, err := fs.ReadFile(ui.LoginUIBox, "locales/"+lang+".json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func handleLogin() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
returnURL := r.URL.Query().Get(returnURLParam)
|
||||
@@ -78,31 +123,26 @@ func handleLogin() http.HandlerFunc {
|
||||
|
||||
func handleLoginPost() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.FormValue(returnURLParam)
|
||||
if url == "" {
|
||||
url = getProxyPrefix(r) + "/"
|
||||
}
|
||||
|
||||
err := manager.GetInstance().SessionStore.Login(w, r)
|
||||
if err != nil {
|
||||
// always log the error
|
||||
logger.Errorf("Error logging in: %v", err)
|
||||
logger.Errorf("Error logging in: %v from IP: %s", err, r.RemoteAddr)
|
||||
}
|
||||
|
||||
var invalidCredentialsError *session.InvalidCredentialsError
|
||||
|
||||
if errors.As(err, &invalidCredentialsError) {
|
||||
// serve login page with an error
|
||||
serveLoginPage(w, r, url, "Username or password is invalid")
|
||||
http.Error(w, "Username or password is invalid", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
// don't expose the error to the user
|
||||
http.Error(w, "An unexpected error occurred. See logs", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
"github.com/stashapp/stash/pkg/stashbox"
|
||||
)
|
||||
|
||||
func (r *Resolver) newStashBoxClient(box models.StashBox) *stashbox.Client {
|
||||
return stashbox.NewClient(box, r.stashboxRepository())
|
||||
return stashbox.NewClient(box, stashbox.ExcludeTagPatterns(manager.GetInstance().Config.GetScraperExcludeTagPatterns()))
|
||||
}
|
||||
|
||||
func resolveStashBoxFn(indexField, endpointField string) func(index *int, endpoint *string) (*models.StashBox, error) {
|
||||
|
||||
@@ -2,11 +2,11 @@ package autotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
type GalleryFinderUpdater interface {
|
||||
@@ -53,7 +53,7 @@ func GalleryPerformers(ctx context.Context, s *models.Gallery, rw GalleryPerform
|
||||
}
|
||||
existing := s.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func GalleryTags(ctx context.Context, s *models.Gallery, rw GalleryTagUpdater, t
|
||||
}
|
||||
existing := s.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ package autotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
type ImageFinderUpdater interface {
|
||||
@@ -44,7 +44,7 @@ func ImagePerformers(ctx context.Context, s *models.Image, rw ImagePerformerUpda
|
||||
}
|
||||
existing := s.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func ImageTags(ctx context.Context, s *models.Image, rw ImageTagUpdater, tagRead
|
||||
}
|
||||
existing := s.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ func createPerformer(ctx context.Context, pqb models.PerformerWriter) error {
|
||||
Name: testName,
|
||||
}
|
||||
|
||||
err := pqb.Create(ctx, &performer)
|
||||
err := pqb.Create(ctx, &models.CreatePerformerInput{Performer: &performer})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ package autotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
@@ -63,7 +63,7 @@ func (tagger *Tagger) PerformerScenes(ctx context.Context, p *models.Performer,
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (tagger *Tagger) PerformerImages(ctx context.Context, p *models.Performer,
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func (tagger *Tagger) PerformerGalleries(ctx context.Context, p *models.Performe
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ package autotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
type SceneFinderUpdater interface {
|
||||
@@ -44,7 +44,7 @@ func ScenePerformers(ctx context.Context, s *models.Scene, rw ScenePerformerUpda
|
||||
}
|
||||
existing := s.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func SceneTags(ctx context.Context, s *models.Scene, rw SceneTagUpdater, tagRead
|
||||
}
|
||||
existing := s.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ package autotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ func (tagger *Tagger) TagScenes(ctx context.Context, p *models.Tag, paths []stri
|
||||
}
|
||||
existing := o.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func (tagger *Tagger) TagImages(ctx context.Context, p *models.Tag, paths []stri
|
||||
}
|
||||
existing := o.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func (tagger *Tagger) TagGalleries(ctx context.Context, p *models.Tag, paths []s
|
||||
}
|
||||
existing := o.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -40,7 +41,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
var pageSize = 100
|
||||
@@ -521,7 +521,7 @@ func (me *contentDirectoryService) getPageVideos(sceneFilter *models.SceneFilter
|
||||
}
|
||||
|
||||
func getPageFromID(paths []string) *int {
|
||||
i := sliceutil.Index(paths, "page")
|
||||
i := slices.Index(paths, "page")
|
||||
if i == -1 || i+1 >= len(paths) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package dlna
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
// only keep the 10 most recent IP addresses
|
||||
@@ -30,7 +29,7 @@ func (m *ipWhitelistManager) addRecent(addr string) bool {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
i := sliceutil.Index(m.recentIPAddresses, addr)
|
||||
i := slices.Index(m.recentIPAddresses, addr)
|
||||
if i != -1 {
|
||||
if i == 0 {
|
||||
// don't do anything if it's already at the start
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -31,7 +31,7 @@ func (e *MultipleMatchesFoundError) Error() string {
|
||||
}
|
||||
|
||||
type SceneScraper interface {
|
||||
ScrapeScenes(ctx context.Context, sceneID int) ([]*scraper.ScrapedScene, error)
|
||||
ScrapeScenes(ctx context.Context, sceneID int) ([]*models.ScrapedScene, error)
|
||||
}
|
||||
|
||||
type SceneUpdatePostHookExecutor interface {
|
||||
@@ -95,7 +95,7 @@ func (t *SceneIdentifier) Identify(ctx context.Context, scene *models.Scene) err
|
||||
}
|
||||
|
||||
type scrapeResult struct {
|
||||
result *scraper.ScrapedScene
|
||||
result *models.ScrapedScene
|
||||
source ScraperSource
|
||||
}
|
||||
|
||||
@@ -244,7 +244,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 +266,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 +336,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
|
||||
}
|
||||
@@ -370,7 +373,7 @@ func getFieldOptions(options []MetadataOptions) map[string]*FieldOptions {
|
||||
return ret
|
||||
}
|
||||
|
||||
func getScenePartial(scene *models.Scene, scraped *scraper.ScrapedScene, fieldOptions map[string]*FieldOptions, setOrganized bool) models.ScenePartial {
|
||||
func getScenePartial(scene *models.Scene, scraped *models.ScrapedScene, fieldOptions map[string]*FieldOptions, setOrganized bool) models.ScenePartial {
|
||||
partial := models.ScenePartial{}
|
||||
|
||||
if scraped.Title != nil && (scene.Title != *scraped.Title) {
|
||||
|
||||
@@ -4,13 +4,12 @@ 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"
|
||||
)
|
||||
@@ -19,11 +18,11 @@ var testCtx = context.Background()
|
||||
|
||||
type mockSceneScraper struct {
|
||||
errIDs []int
|
||||
results map[int][]*scraper.ScrapedScene
|
||||
results map[int][]*models.ScrapedScene
|
||||
}
|
||||
|
||||
func (s mockSceneScraper) ScrapeScenes(ctx context.Context, sceneID int) ([]*scraper.ScrapedScene, error) {
|
||||
if sliceutil.Contains(s.errIDs, sceneID) {
|
||||
func (s mockSceneScraper) ScrapeScenes(ctx context.Context, sceneID int) ([]*models.ScrapedScene, error) {
|
||||
if slices.Contains(s.errIDs, sceneID) {
|
||||
return nil, errors.New("scrape scene error")
|
||||
}
|
||||
return s.results[sceneID], nil
|
||||
@@ -70,7 +69,7 @@ func TestSceneIdentifier_Identify(t *testing.T) {
|
||||
{
|
||||
Scraper: mockSceneScraper{
|
||||
errIDs: []int{errID1},
|
||||
results: map[int][]*scraper.ScrapedScene{
|
||||
results: map[int][]*models.ScrapedScene{
|
||||
found1ID: {{
|
||||
Title: &scrapedTitle,
|
||||
}},
|
||||
@@ -80,7 +79,7 @@ func TestSceneIdentifier_Identify(t *testing.T) {
|
||||
{
|
||||
Scraper: mockSceneScraper{
|
||||
errIDs: []int{errID2},
|
||||
results: map[int][]*scraper.ScrapedScene{
|
||||
results: map[int][]*models.ScrapedScene{
|
||||
found2ID: {{
|
||||
Title: &scrapedTitle,
|
||||
}},
|
||||
@@ -250,7 +249,7 @@ func TestSceneIdentifier_modifyScene(t *testing.T) {
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
||||
},
|
||||
&scrapeResult{
|
||||
result: &scraper.ScrapedScene{},
|
||||
result: &models.ScrapedScene{},
|
||||
source: ScraperSource{
|
||||
Options: defaultOptions,
|
||||
},
|
||||
@@ -386,14 +385,14 @@ func Test_getScenePartial(t *testing.T) {
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
|
||||
scrapedScene := &scraper.ScrapedScene{
|
||||
scrapedScene := &models.ScrapedScene{
|
||||
Title: &scrapedTitle,
|
||||
Date: &scrapedDate,
|
||||
Details: &scrapedDetails,
|
||||
URLs: []string{scrapedURL},
|
||||
}
|
||||
|
||||
scrapedUnchangedScene := &scraper.ScrapedScene{
|
||||
scrapedUnchangedScene := &models.ScrapedScene{
|
||||
Title: &originalTitle,
|
||||
Date: &originalDate,
|
||||
Details: &originalDetails,
|
||||
@@ -423,7 +422,7 @@ func Test_getScenePartial(t *testing.T) {
|
||||
|
||||
type args struct {
|
||||
scene *models.Scene
|
||||
scraped *scraper.ScrapedScene
|
||||
scraped *models.ScrapedScene
|
||||
fieldOptions map[string]*FieldOptions
|
||||
setOrganized bool
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func createMissingPerformer(ctx context.Context, endpoint string, w PerformerCre
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = w.Create(ctx, newPerformer)
|
||||
err = w.Create(ctx, &models.CreatePerformerInput{Performer: newPerformer})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating performer: %w", err)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ func Test_getPerformerID(t *testing.T) {
|
||||
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
db.Performer.On("Create", testCtx, mock.Anything).Run(func(args mock.Arguments) {
|
||||
p := args.Get(1).(*models.Performer)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Run(func(args mock.Arguments) {
|
||||
p := args.Get(1).(*models.CreatePerformerInput)
|
||||
p.ID = validStoredID
|
||||
}).Return(nil)
|
||||
|
||||
@@ -154,14 +154,14 @@ func Test_createMissingPerformer(t *testing.T) {
|
||||
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
db.Performer.On("Create", testCtx, mock.MatchedBy(func(p *models.Performer) bool {
|
||||
db.Performer.On("Create", testCtx, mock.MatchedBy(func(p *models.CreatePerformerInput) bool {
|
||||
return p.Name == validName
|
||||
})).Run(func(args mock.Arguments) {
|
||||
p := args.Get(1).(*models.Performer)
|
||||
p := args.Get(1).(*models.CreatePerformerInput)
|
||||
p.ID = performerID
|
||||
}).Return(nil)
|
||||
|
||||
db.Performer.On("Create", testCtx, mock.MatchedBy(func(p *models.Performer) bool {
|
||||
db.Performer.On("Create", testCtx, mock.MatchedBy(func(p *models.CreatePerformerInput) bool {
|
||||
return p.Name == invalidName
|
||||
})).Return(errors.New("error creating performer"))
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -182,7 +183,13 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) {
|
||||
return tagIDs, nil
|
||||
}
|
||||
|
||||
func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, error) {
|
||||
// stashIDs returns the updated stash IDs for the scene
|
||||
// returns nil if not applicable or no changes were made
|
||||
// if setUpdateTime is true, then the updated_at field will be set to the current time
|
||||
// for the applicable matching stash ID
|
||||
func (g sceneRelationships) stashIDs(ctx context.Context, setUpdateTime bool) ([]models.StashID, error) {
|
||||
updateTime := time.Now()
|
||||
|
||||
remoteSiteID := g.result.result.RemoteSiteID
|
||||
fieldStrategy := g.fieldOptions["stash_ids"]
|
||||
target := g.scene
|
||||
@@ -199,7 +206,7 @@ func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, err
|
||||
strategy = fieldStrategy.Strategy
|
||||
}
|
||||
|
||||
var stashIDs []models.StashID
|
||||
var stashIDs models.StashIDs
|
||||
originalStashIDs := target.StashIDs.List()
|
||||
|
||||
if strategy == FieldStrategyMerge {
|
||||
@@ -208,15 +215,17 @@ func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, err
|
||||
stashIDs = append(stashIDs, originalStashIDs...)
|
||||
}
|
||||
|
||||
// find and update the stash id if it exists
|
||||
for i, stashID := range stashIDs {
|
||||
if endpoint == stashID.Endpoint {
|
||||
// if stashID is the same, then don't set
|
||||
if stashID.StashID == *remoteSiteID {
|
||||
if !setUpdateTime && stashID.StashID == *remoteSiteID {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// replace the stash id and return
|
||||
stashID.StashID = *remoteSiteID
|
||||
stashID.UpdatedAt = updateTime
|
||||
stashIDs[i] = stashID
|
||||
return stashIDs, nil
|
||||
}
|
||||
@@ -224,11 +233,14 @@ func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, err
|
||||
|
||||
// not found, create new entry
|
||||
stashIDs = append(stashIDs, models.StashID{
|
||||
StashID: *remoteSiteID,
|
||||
Endpoint: endpoint,
|
||||
StashID: *remoteSiteID,
|
||||
Endpoint: endpoint,
|
||||
UpdatedAt: updateTime,
|
||||
})
|
||||
|
||||
if sliceutil.SliceSame(originalStashIDs, stashIDs) {
|
||||
// don't return if nothing was changed
|
||||
// if we're setting update time, then we always return
|
||||
if !setUpdateTime && stashIDs.HasSameStashIDs(originalStashIDs) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
@@ -124,7 +124,7 @@ func Test_sceneRelationships_studio(t *testing.T) {
|
||||
source: ScraperSource{
|
||||
RemoteSite: "endpoint",
|
||||
},
|
||||
result: &scraper.ScrapedScene{
|
||||
result: &models.ScrapedScene{
|
||||
Studio: tt.result,
|
||||
},
|
||||
}
|
||||
@@ -314,7 +314,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
tr.scene = tt.scene
|
||||
tr.fieldOptions["performers"] = tt.fieldOptions
|
||||
tr.result = &scrapeResult{
|
||||
result: &scraper.ScrapedScene{
|
||||
result: &models.ScrapedScene{
|
||||
Performers: tt.scraped,
|
||||
},
|
||||
}
|
||||
@@ -506,7 +506,7 @@ func Test_sceneRelationships_tags(t *testing.T) {
|
||||
tr.scene = tt.scene
|
||||
tr.fieldOptions["tags"] = tt.fieldOptions
|
||||
tr.result = &scrapeResult{
|
||||
result: &scraper.ScrapedScene{
|
||||
result: &models.ScrapedScene{
|
||||
Tags: tt.scraped,
|
||||
},
|
||||
}
|
||||
@@ -548,8 +548,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 +562,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 +582,7 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
},
|
||||
newEndpoint,
|
||||
&remoteSiteID,
|
||||
false,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
@@ -587,6 +592,7 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
defaultOptions,
|
||||
"",
|
||||
&remoteSiteID,
|
||||
false,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
@@ -596,6 +602,7 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
defaultOptions,
|
||||
newEndpoint,
|
||||
nil,
|
||||
false,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
@@ -605,19 +612,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 +654,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 +677,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 +695,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) {
|
||||
@@ -676,16 +726,25 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
source: ScraperSource{
|
||||
RemoteSite: tt.endpoint,
|
||||
},
|
||||
result: &scraper.ScrapedScene{
|
||||
result: &models.ScrapedScene{
|
||||
RemoteSiteID: tt.remoteSiteID,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -767,7 +826,7 @@ func Test_sceneRelationships_cover(t *testing.T) {
|
||||
ID: tt.sceneID,
|
||||
}
|
||||
tr.result = &scrapeResult{
|
||||
result: &scraper.ScrapedScene{
|
||||
result: &models.ScrapedScene{
|
||||
Image: tt.image,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ const (
|
||||
|
||||
// slice default values
|
||||
var (
|
||||
defaultVideoExtensions = []string{"m4v", "mp4", "mov", "wmv", "avi", "mpg", "mpeg", "rmvb", "rm", "flv", "asf", "mkv", "webm"}
|
||||
defaultVideoExtensions = []string{"m4v", "mp4", "mov", "wmv", "avi", "mpg", "mpeg", "rmvb", "rm", "flv", "asf", "mkv", "webm", "f4v"}
|
||||
defaultImageExtensions = []string{"png", "jpg", "jpeg", "gif", "webp"}
|
||||
defaultGalleryExtensions = []string{"zip", "cbz"}
|
||||
defaultMenuItems = []string{"scenes", "images", "movies", "markers", "galleries", "performers", "studios", "tags"}
|
||||
@@ -1105,9 +1105,10 @@ func stashBoxValidate(str string) bool {
|
||||
}
|
||||
|
||||
type StashBoxInput struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
APIKey string `json:"api_key"`
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
APIKey string `json:"api_key"`
|
||||
Name string `json:"name"`
|
||||
MaxRequestsPerMinute int `json:"max_requests_per_minute"`
|
||||
}
|
||||
|
||||
func (i *Config) ValidateStashBoxes(boxes []*StashBoxInput) error {
|
||||
@@ -1533,7 +1534,7 @@ func (i *Config) GetDefaultGenerateSettings() *models.GenerateMetadataOptions {
|
||||
}
|
||||
|
||||
// GetDangerousAllowPublicWithoutAuth determines if the security feature is enabled.
|
||||
// See https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet
|
||||
// See https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet
|
||||
func (i *Config) GetDangerousAllowPublicWithoutAuth() bool {
|
||||
return i.getBool(dangerousAllowPublicWithoutAuth)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ var (
|
||||
"external_host": ExternalHost,
|
||||
"generated": Generated,
|
||||
"metadata": Metadata,
|
||||
"blobs": BlobsPath,
|
||||
"cache": Cache,
|
||||
"stash": Stash,
|
||||
"ui": UILocation,
|
||||
|
||||
@@ -42,3 +42,7 @@ func (jp *jsonUtils) saveGallery(fn string, gallery *jsonschema.Gallery) error {
|
||||
func (jp *jsonUtils) saveFile(fn string, file jsonschema.DirEntry) error {
|
||||
return jsonschema.SaveFileFile(filepath.Join(jp.json.Files, fn), file)
|
||||
}
|
||||
|
||||
func (jp *jsonUtils) saveSavedFilter(fn string, savedFilter *jsonschema.SavedFilter) error {
|
||||
return jsonschema.SaveSavedFilterFile(filepath.Join(jp.json.SavedFilters, fn), savedFilter)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ type SceneService interface {
|
||||
AssignFile(ctx context.Context, sceneID int, fileID models.FileID) error
|
||||
Merge(ctx context.Context, sourceIDs []int, destinationID int, fileDeleter *scene.FileDeleter, options scene.MergeOptions) error
|
||||
Destroy(ctx context.Context, scene *models.Scene, fileDeleter *scene.FileDeleter, deleteGenerated, deleteFile bool) error
|
||||
|
||||
FindMany(ctx context.Context, ids []int, load ...scene.LoadRelationshipOption) ([]*models.Scene, error)
|
||||
sceneFingerprintGetter
|
||||
}
|
||||
|
||||
type ImageService interface {
|
||||
|
||||
@@ -42,8 +42,8 @@ func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error
|
||||
|
||||
logger.Infof("Migrating database from %d to %d", schemaInfo.CurrentSchemaVersion, schemaInfo.RequiredSchemaVersion)
|
||||
|
||||
// set the number of tasks = required steps + optimise
|
||||
progress.SetTotal(int(schemaInfo.StepsRequired + 1))
|
||||
// set the number of tasks = backup + required steps + optimise
|
||||
progress.SetTotal(int(schemaInfo.StepsRequired + 2))
|
||||
|
||||
database := s.Database
|
||||
|
||||
@@ -61,12 +61,20 @@ func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error
|
||||
}
|
||||
}
|
||||
|
||||
// perform database backup
|
||||
if err := database.Backup(backupPath); err != nil {
|
||||
progress.ExecuteTask("Backing up database", func() {
|
||||
defer progress.Increment()
|
||||
|
||||
// perform database backup
|
||||
err = database.Backup(backupPath)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error backing up database: %s", err)
|
||||
}
|
||||
|
||||
if err := s.runMigrations(ctx, progress); err != nil {
|
||||
err = s.runMigrations(ctx, progress)
|
||||
|
||||
if err != nil {
|
||||
errStr := fmt.Sprintf("error performing migration: %s", err)
|
||||
|
||||
// roll back to the backed up version
|
||||
@@ -87,6 +95,11 @@ func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error
|
||||
}
|
||||
}
|
||||
|
||||
// reinitialise the database
|
||||
if err := database.ReInitialise(); err != nil {
|
||||
return fmt.Errorf("error reinitialising database: %s", err)
|
||||
}
|
||||
|
||||
logger.Infof("Database migration complete")
|
||||
|
||||
return nil
|
||||
@@ -124,6 +137,8 @@ func (s *MigrateJob) runMigrations(ctx context.Context, progress *job.Progress)
|
||||
|
||||
defer m.Close()
|
||||
|
||||
logger.Info("Running migrations")
|
||||
|
||||
for {
|
||||
currentSchemaVersion := m.CurrentSchemaVersion()
|
||||
targetSchemaVersion := m.RequiredSchemaVersion()
|
||||
@@ -144,21 +159,15 @@ func (s *MigrateJob) runMigrations(ctx context.Context, progress *job.Progress)
|
||||
progress.Increment()
|
||||
}
|
||||
|
||||
// reinitialise the database
|
||||
if err := database.ReInitialise(); err != nil {
|
||||
return fmt.Errorf("error reinitialising database: %s", err)
|
||||
}
|
||||
|
||||
// optimise the database
|
||||
// perform post-migrate analyze using the migrator connection
|
||||
progress.ExecuteTask("Optimising database", func() {
|
||||
err = database.Optimise(ctx)
|
||||
err = m.PostMigrate(ctx)
|
||||
progress.Increment()
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error optimising database: %s", err)
|
||||
}
|
||||
|
||||
progress.Increment()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/savedfilter"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
@@ -176,6 +177,7 @@ func (t *ExportTask) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
t.ExportPerformers(ctx, workerCount)
|
||||
t.ExportStudios(ctx, workerCount)
|
||||
t.ExportTags(ctx, workerCount)
|
||||
t.ExportSavedFilters(ctx, workerCount)
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -1186,3 +1188,62 @@ func (t *ExportTask) exportGroup(ctx context.Context, wg *sync.WaitGroup, jobCha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ExportTask) ExportSavedFilters(ctx context.Context, workers int) {
|
||||
// don't export saved filters unless we're doing a full export
|
||||
if !t.full {
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
reader := t.repository.SavedFilter
|
||||
var filters []*models.SavedFilter
|
||||
var err error
|
||||
filters, err = reader.All(ctx)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("[saved filters] failed to fetch saved filters: %v", err)
|
||||
}
|
||||
|
||||
logger.Info("[saved filters] exporting")
|
||||
startTime := time.Now()
|
||||
|
||||
jobCh := make(chan *models.SavedFilter, workers*2) // make a buffered channel to feed workers
|
||||
|
||||
for w := 0; w < workers; w++ { // create export Saved Filter workers
|
||||
wg.Add(1)
|
||||
go t.exportSavedFilter(ctx, &wg, jobCh)
|
||||
}
|
||||
|
||||
for i, savedFilter := range filters {
|
||||
index := i + 1
|
||||
logger.Progressf("[saved filters] %d of %d", index, len(filters))
|
||||
|
||||
jobCh <- savedFilter // feed workers
|
||||
}
|
||||
|
||||
close(jobCh)
|
||||
wg.Wait()
|
||||
|
||||
logger.Infof("[saved filters] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
||||
}
|
||||
|
||||
func (t *ExportTask) exportSavedFilter(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.SavedFilter) {
|
||||
defer wg.Done()
|
||||
|
||||
for thisFilter := range jobChan {
|
||||
newJSON, err := savedfilter.ToJSON(ctx, thisFilter)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("[saved filter] <%s> error getting saved filter JSON: %v", thisFilter.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fn := newJSON.Filename()
|
||||
|
||||
if err := t.json.saveSavedFilter(fn, newJSON); err != nil {
|
||||
logger.Errorf("[saved filter] <%s> failed to save json: %v", fn, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (t *GenerateInteractiveHeatmapSpeedTask) Start(ctx context.Context) {
|
||||
err := generator.Generate(funscriptPath, heatmapPath, t.Scene.Files.Primary().Duration)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("error generating heatmap: %s", err.Error())
|
||||
logger.Errorf("error generating heatmap for %s: %s", t.Scene.Path, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,8 +46,16 @@ func (t *GenerateInteractiveHeatmapSpeedTask) Start(ctx context.Context) {
|
||||
if err := r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
primaryFile := t.Scene.Files.Primary()
|
||||
primaryFile.InteractiveSpeed = &median
|
||||
qb := r.File
|
||||
return qb.Update(ctx, primaryFile)
|
||||
if err := r.File.Update(ctx, primaryFile); err != nil {
|
||||
return fmt.Errorf("updating interactive speed for %s: %w", primaryFile.Path, err)
|
||||
}
|
||||
|
||||
// update the scene UpdatedAt field
|
||||
// NewScenePartial sets the UpdatedAt field to the current time
|
||||
if _, err := r.Scene.UpdatePartial(ctx, t.Scene.ID, models.NewScenePartial()); err != nil {
|
||||
return fmt.Errorf("updating UpdatedAt field for scene %d: %w", t.Scene.ID, err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil && ctx.Err() == nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
|
||||
@@ -105,11 +105,11 @@ func (t *GenerateMarkersTask) generateSceneMarkers(ctx context.Context) {
|
||||
|
||||
func (t *GenerateMarkersTask) generateMarker(videoFile *models.VideoFile, scene *models.Scene, sceneMarker *models.SceneMarker) {
|
||||
sceneHash := scene.GetHash(t.fileNamingAlgorithm)
|
||||
seconds := int(sceneMarker.Seconds)
|
||||
seconds := float64(sceneMarker.Seconds)
|
||||
|
||||
g := t.generator
|
||||
|
||||
if err := g.MarkerPreviewVideo(context.TODO(), videoFile.Path, sceneHash, seconds, instance.Config.GetPreviewAudio()); err != nil {
|
||||
if err := g.MarkerPreviewVideo(context.TODO(), videoFile.Path, sceneHash, seconds, sceneMarker.EndSeconds, instance.Config.GetPreviewAudio()); err != nil {
|
||||
logger.Errorf("[generator] failed to generate marker video: %v", err)
|
||||
logErrorOutput(err)
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
"github.com/stashapp/stash/internal/identify"
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/stashbox"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
var ErrInput = errors.New("invalid request input")
|
||||
@@ -169,12 +171,20 @@ func (j *IdentifyJob) getSources() ([]identify.ScraperSource, error) {
|
||||
|
||||
var src identify.ScraperSource
|
||||
if stashBox != nil {
|
||||
stashboxRepository := stashbox.NewRepository(instance.Repository)
|
||||
matcher := match.SceneRelationships{
|
||||
PerformerFinder: instance.Repository.Performer,
|
||||
TagFinder: instance.Repository.Tag,
|
||||
StudioFinder: instance.Repository.Studio,
|
||||
}
|
||||
|
||||
src = identify.ScraperSource{
|
||||
Name: "stash-box: " + stashBox.Endpoint,
|
||||
Scraper: stashboxSource{
|
||||
stashbox.NewClient(*stashBox, stashboxRepository),
|
||||
stashBox.Endpoint,
|
||||
Client: stashbox.NewClient(*stashBox, stashbox.ExcludeTagPatterns(instance.Config.GetScraperExcludeTagPatterns())),
|
||||
endpoint: stashBox.Endpoint,
|
||||
txnManager: instance.Repository.TxnManager,
|
||||
sceneFingerprintGetter: instance.SceneService,
|
||||
matcher: matcher,
|
||||
},
|
||||
RemoteSite: stashBox.Endpoint,
|
||||
}
|
||||
@@ -247,14 +257,42 @@ func resolveStashBox(sb []*models.StashBox, source scraper.Source) (*models.Stas
|
||||
type stashboxSource struct {
|
||||
*stashbox.Client
|
||||
endpoint string
|
||||
|
||||
txnManager models.TxnManager
|
||||
sceneFingerprintGetter sceneFingerprintGetter
|
||||
matcher match.SceneRelationships
|
||||
}
|
||||
|
||||
func (s stashboxSource) ScrapeScenes(ctx context.Context, sceneID int) ([]*scraper.ScrapedScene, error) {
|
||||
results, err := s.FindStashBoxSceneByFingerprints(ctx, sceneID)
|
||||
type sceneFingerprintGetter interface {
|
||||
GetScenesFingerprints(ctx context.Context, ids []int) ([]models.Fingerprints, error)
|
||||
}
|
||||
|
||||
func (s stashboxSource) ScrapeScenes(ctx context.Context, sceneID int) ([]*models.ScrapedScene, error) {
|
||||
var fps []models.Fingerprints
|
||||
if err := txn.WithReadTxn(ctx, s.txnManager, func(ctx context.Context) error {
|
||||
var err error
|
||||
fps, err = s.sceneFingerprintGetter.GetScenesFingerprints(ctx, []int{sceneID})
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("error getting scene fingerprints: %w", err)
|
||||
}
|
||||
|
||||
results, err := s.FindSceneByFingerprints(ctx, fps[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error querying stash-box using scene ID %d: %w", sceneID, err)
|
||||
}
|
||||
|
||||
if err := txn.WithReadTxn(ctx, s.txnManager, func(ctx context.Context) error {
|
||||
for _, ret := range results {
|
||||
if err := s.matcher.MatchRelationships(ctx, ret, s.endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("error matching scene relationships: %w", err)
|
||||
}
|
||||
|
||||
if len(results) > 0 {
|
||||
return results, nil
|
||||
}
|
||||
@@ -271,7 +309,7 @@ type scraperSource struct {
|
||||
scraperID string
|
||||
}
|
||||
|
||||
func (s scraperSource) ScrapeScenes(ctx context.Context, sceneID int) ([]*scraper.ScrapedScene, error) {
|
||||
func (s scraperSource) ScrapeScenes(ctx context.Context, sceneID int) ([]*models.ScrapedScene, error) {
|
||||
content, err := s.cache.ScrapeID(ctx, s.scraperID, sceneID, scraper.ScrapeContentTypeScene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -282,8 +320,8 @@ func (s scraperSource) ScrapeScenes(ctx context.Context, sceneID int) ([]*scrape
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if scene, ok := content.(scraper.ScrapedScene); ok {
|
||||
return []*scraper.ScrapedScene{&scene}, nil
|
||||
if scene, ok := content.(models.ScrapedScene); ok {
|
||||
return []*models.ScrapedScene{&scene}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("could not convert content to scene")
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/savedfilter"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/studio"
|
||||
"github.com/stashapp/stash/pkg/tag"
|
||||
@@ -124,6 +125,7 @@ func (t *ImportTask) Start(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
t.ImportSavedFilters(ctx)
|
||||
t.ImportTags(ctx)
|
||||
t.ImportPerformers(ctx)
|
||||
t.ImportStudios(ctx)
|
||||
@@ -707,6 +709,11 @@ func (t *ImportTask) ImportScenes(ctx context.Context) {
|
||||
return err
|
||||
}
|
||||
|
||||
// skip importing markers if the scene was not created
|
||||
if sceneImporter.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// import the scene markers
|
||||
for _, m := range sceneJSON.Markers {
|
||||
markerImporter := &scene.MarkerImporter{
|
||||
@@ -779,3 +786,53 @@ func (t *ImportTask) ImportImages(ctx context.Context) {
|
||||
|
||||
logger.Info("[images] import complete")
|
||||
}
|
||||
|
||||
func (t *ImportTask) ImportSavedFilters(ctx context.Context) {
|
||||
logger.Info("[saved filters] importing")
|
||||
|
||||
path := t.json.json.SavedFilters
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
logger.Errorf("[saved filters] failed to read saved filters directory: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
r := t.repository
|
||||
|
||||
for i, fi := range files {
|
||||
index := i + 1
|
||||
savedFilterJSON, err := jsonschema.LoadSavedFilterFile(filepath.Join(path, fi.Name()))
|
||||
if err != nil {
|
||||
logger.Errorf("[saved filters] failed to read json: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Progressf("[saved filters] %d of %d", index, len(files))
|
||||
|
||||
if err := r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
return t.importSavedFilter(ctx, savedFilterJSON)
|
||||
}); err != nil {
|
||||
logger.Errorf("[saved filters] <%s> failed to import: %v", fi.Name(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("[saved filters] import complete")
|
||||
}
|
||||
|
||||
func (t *ImportTask) importSavedFilter(ctx context.Context, savedFilterJSON *jsonschema.SavedFilter) error {
|
||||
importer := &savedfilter.Importer{
|
||||
ReaderWriter: t.repository.SavedFilter,
|
||||
Input: *savedFilterJSON,
|
||||
MissingRefBehaviour: t.MissingRefBehaviour,
|
||||
}
|
||||
|
||||
if err := performImport(ctx, importer, t.DuplicateBehaviour); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ type handlerRequiredFilter struct {
|
||||
GalleryFinder galleryFinder
|
||||
CaptionUpdater video.CaptionUpdater
|
||||
|
||||
FolderCache *lru.LRU
|
||||
FolderCache *lru.LRU[bool]
|
||||
|
||||
videoFileNamingAlgorithm models.HashAlgorithm
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func newHandlerRequiredFilter(c *config.Config, repo models.Repository) *handler
|
||||
ImageFinder: repo.Image,
|
||||
GalleryFinder: repo.Gallery,
|
||||
CaptionUpdater: repo.File,
|
||||
FolderCache: lru.New(processes * 2),
|
||||
FolderCache: lru.New[bool](processes * 2),
|
||||
videoFileNamingAlgorithm: c.GetVideoFileNamingAlgorithm(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/stashbox"
|
||||
"github.com/stashapp/stash/pkg/studio"
|
||||
)
|
||||
|
||||
@@ -94,8 +96,7 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode
|
||||
|
||||
r := instance.Repository
|
||||
|
||||
stashboxRepository := stashbox.NewRepository(r)
|
||||
client := stashbox.NewClient(*t.box, stashboxRepository)
|
||||
client := stashbox.NewClient(*t.box, stashbox.ExcludeTagPatterns(instance.Config.GetScraperExcludeTagPatterns()))
|
||||
|
||||
if t.refresh {
|
||||
var remoteID string
|
||||
@@ -118,7 +119,19 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode
|
||||
return nil, err
|
||||
}
|
||||
if remoteID != "" {
|
||||
performer, err = client.FindStashBoxPerformerByID(ctx, remoteID)
|
||||
performer, err = client.FindPerformerByID(ctx, remoteID)
|
||||
|
||||
if performer != nil && performer.RemoteMergedIntoId != nil {
|
||||
mergedPerformer, err := t.handleMergedPerformer(ctx, performer, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mergedPerformer != nil {
|
||||
logger.Infof("Performer id %s merged into %s, updating local performer", remoteID, *performer.RemoteMergedIntoId)
|
||||
performer = mergedPerformer
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var name string
|
||||
@@ -127,12 +140,35 @@ func (t *StashBoxBatchTagTask) findStashBoxPerformer(ctx context.Context) (*mode
|
||||
} else {
|
||||
name = t.performer.Name
|
||||
}
|
||||
performer, err = client.FindStashBoxPerformerByName(ctx, name)
|
||||
performer, err = client.FindPerformerByName(ctx, name)
|
||||
}
|
||||
|
||||
if performer != nil {
|
||||
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
return match.ScrapedPerformer(ctx, r.Performer, performer, t.box.Endpoint)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return performer, err
|
||||
}
|
||||
|
||||
func (t *StashBoxBatchTagTask) handleMergedPerformer(ctx context.Context, performer *models.ScrapedPerformer, client *stashbox.Client) (mergedPerformer *models.ScrapedPerformer, err error) {
|
||||
mergedPerformer, err = client.FindPerformerByID(ctx, *performer.RemoteMergedIntoId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading merged performer %s from stashbox", *performer.RemoteMergedIntoId)
|
||||
}
|
||||
|
||||
if mergedPerformer.StoredID != nil && *mergedPerformer.StoredID != *performer.StoredID {
|
||||
logger.Warnf("Performer %s merged into %s, but both exist locally, not merging", *performer.StoredID, *mergedPerformer.StoredID)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
mergedPerformer.StoredID = performer.StoredID
|
||||
return mergedPerformer, nil
|
||||
}
|
||||
|
||||
func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *models.ScrapedPerformer, excluded map[string]bool) {
|
||||
// Refreshing an existing performer
|
||||
if t.performer != nil {
|
||||
@@ -156,6 +192,19 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m
|
||||
|
||||
partial := p.ToPartial(t.box.Endpoint, excluded, existingStashIDs)
|
||||
|
||||
// if we're setting the performer's aliases, and not the name, then filter out the name
|
||||
// from the aliases to avoid duplicates
|
||||
// add the name to the aliases if it's not already there
|
||||
if partial.Aliases != nil && !partial.Name.Set {
|
||||
partial.Aliases.Values = sliceutil.Filter(partial.Aliases.Values, func(s string) bool {
|
||||
return s != t.performer.Name
|
||||
})
|
||||
|
||||
if p.Name != nil && t.performer.Name != *p.Name {
|
||||
partial.Aliases.Values = sliceutil.AppendUnique(partial.Aliases.Values, *p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if err := performer.ValidateUpdate(ctx, t.performer.ID, partial, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -194,7 +243,7 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.Create(ctx, newPerformer); err != nil {
|
||||
if err := qb.Create(ctx, &models.CreatePerformerInput{Performer: newPerformer}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -246,8 +295,7 @@ func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models.
|
||||
|
||||
r := instance.Repository
|
||||
|
||||
stashboxRepository := stashbox.NewRepository(r)
|
||||
client := stashbox.NewClient(*t.box, stashboxRepository)
|
||||
client := stashbox.NewClient(*t.box, stashbox.ExcludeTagPatterns(instance.Config.GetScraperExcludeTagPatterns()))
|
||||
|
||||
if t.refresh {
|
||||
var remoteID string
|
||||
@@ -268,7 +316,7 @@ func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models.
|
||||
return nil, err
|
||||
}
|
||||
if remoteID != "" {
|
||||
studio, err = client.FindStashBoxStudio(ctx, remoteID)
|
||||
studio, err = client.FindStudio(ctx, remoteID)
|
||||
}
|
||||
} else {
|
||||
var name string
|
||||
@@ -277,7 +325,19 @@ func (t *StashBoxBatchTagTask) findStashBoxStudio(ctx context.Context) (*models.
|
||||
} else {
|
||||
name = t.studio.Name
|
||||
}
|
||||
studio, err = client.FindStashBoxStudio(ctx, name)
|
||||
studio, err = client.FindStudio(ctx, name)
|
||||
}
|
||||
|
||||
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
if studio != nil {
|
||||
if err := match.ScrapedStudioHierarchy(ctx, r.Studio, studio, t.box.Endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return studio, err
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1100,7 +1100,8 @@ func (s *scanJob) removeOutdatedFingerprints(existing models.File, fp models.Fin
|
||||
|
||||
// oshash has changed, MD5 is missing - remove MD5 from the existing fingerprints
|
||||
logger.Infof("Removing outdated checksum from %s", existing.Base().Path)
|
||||
existing.Base().Fingerprints.Remove(models.FingerprintTypeMD5)
|
||||
b := existing.Base()
|
||||
b.Fingerprints = b.Fingerprints.Remove(models.FingerprintTypeMD5)
|
||||
}
|
||||
|
||||
// returns a file only if it was updated
|
||||
|
||||
@@ -3,6 +3,7 @@ package gallery
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -153,7 +154,7 @@ func (i *Importer) populatePerformers(ctx context.Context) error {
|
||||
}
|
||||
|
||||
missingPerformers := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingPerformers) > 0 {
|
||||
@@ -187,7 +188,9 @@ func (i *Importer) createPerformers(ctx context.Context, names []string) ([]*mod
|
||||
newPerformer := models.NewPerformer()
|
||||
newPerformer.Name = name
|
||||
|
||||
err := i.PerformerWriter.Create(ctx, &newPerformer)
|
||||
err := i.PerformerWriter.Create(ctx, &models.CreatePerformerInput{
|
||||
Performer: &newPerformer,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -212,7 +215,7 @@ func (i *Importer) populateTags(ctx context.Context) error {
|
||||
}
|
||||
|
||||
missingTags := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingTags) > 0 {
|
||||
|
||||
@@ -201,8 +201,8 @@ func TestImporterPreImportWithMissingPerformer(t *testing.T) {
|
||||
}
|
||||
|
||||
db.Performer.On("FindByNames", testCtx, []string{missingPerformerName}, false).Return(nil, nil).Times(3)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.Performer")).Run(func(args mock.Arguments) {
|
||||
performer := args.Get(1).(*models.Performer)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Run(func(args mock.Arguments) {
|
||||
performer := args.Get(1).(*models.CreatePerformerInput)
|
||||
performer.ID = existingPerformerID
|
||||
}).Return(nil)
|
||||
|
||||
@@ -235,7 +235,7 @@ func TestImporterPreImportWithMissingPerformerCreateErr(t *testing.T) {
|
||||
}
|
||||
|
||||
db.Performer.On("FindByNames", testCtx, []string{missingPerformerName}, false).Return(nil, nil).Once()
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.Performer")).Return(errors.New("Create error"))
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Return(errors.New("Create error"))
|
||||
|
||||
err := i.PreImport(testCtx)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
@@ -3,6 +3,7 @@ package group
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -96,7 +97,7 @@ func importTags(ctx context.Context, tagWriter models.TagFinderCreator, names []
|
||||
}
|
||||
|
||||
missingTags := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingTags) > 0 {
|
||||
|
||||
@@ -2,6 +2,7 @@ package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -105,7 +106,7 @@ func (s *Service) validateUpdateGroupHierarchy(ctx context.Context, existing *mo
|
||||
subIDs := idsFromGroupDescriptions(effectiveSubGroups)
|
||||
|
||||
// ensure we haven't set the group as a subgroup of itself
|
||||
if sliceutil.Contains(containingIDs, existing.ID) || sliceutil.Contains(subIDs, existing.ID) {
|
||||
if slices.Contains(containingIDs, existing.ID) || slices.Contains(subIDs, existing.ID) {
|
||||
return ErrHierarchyLoop
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package image
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -239,7 +240,7 @@ func (i *Importer) populatePerformers(ctx context.Context) error {
|
||||
}
|
||||
|
||||
missingPerformers := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingPerformers) > 0 {
|
||||
@@ -273,7 +274,9 @@ func (i *Importer) createPerformers(ctx context.Context, names []string) ([]*mod
|
||||
newPerformer := models.NewPerformer()
|
||||
newPerformer.Name = name
|
||||
|
||||
err := i.PerformerWriter.Create(ctx, &newPerformer)
|
||||
err := i.PerformerWriter.Create(ctx, &models.CreatePerformerInput{
|
||||
Performer: &newPerformer,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -375,7 +378,7 @@ func importTags(ctx context.Context, tagWriter models.TagFinderCreator, names []
|
||||
}
|
||||
|
||||
missingTags := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingTags) > 0 {
|
||||
|
||||
@@ -163,8 +163,8 @@ func TestImporterPreImportWithMissingPerformer(t *testing.T) {
|
||||
}
|
||||
|
||||
db.Performer.On("FindByNames", testCtx, []string{missingPerformerName}, false).Return(nil, nil).Times(3)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.Performer")).Run(func(args mock.Arguments) {
|
||||
performer := args.Get(1).(*models.Performer)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Run(func(args mock.Arguments) {
|
||||
performer := args.Get(1).(*models.CreatePerformerInput)
|
||||
performer.ID = existingPerformerID
|
||||
}).Return(nil)
|
||||
|
||||
@@ -197,7 +197,7 @@ func TestImporterPreImportWithMissingPerformerCreateErr(t *testing.T) {
|
||||
}
|
||||
|
||||
db.Performer.On("FindByNames", testCtx, []string{missingPerformerName}, false).Return(nil, nil).Once()
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.Performer")).Return(errors.New("Create error"))
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Return(errors.New("Create error"))
|
||||
|
||||
err := i.PreImport(testCtx)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
@@ -356,7 +356,7 @@ func (h *ScanHandler) getGalleryToAssociate(ctx context.Context, newImage *model
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if g != nil && !sliceutil.Contains(newImage.GalleryIDs.List(), g.ID) {
|
||||
if g != nil && !slices.Contains(newImage.GalleryIDs.List(), g.ID) {
|
||||
return g, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -20,18 +20,52 @@ type GroupNamesFinder interface {
|
||||
FindByNames(ctx context.Context, names []string, nocase bool) ([]*models.Group, error)
|
||||
}
|
||||
|
||||
type SceneRelationships struct {
|
||||
PerformerFinder PerformerFinder
|
||||
TagFinder models.TagQueryer
|
||||
StudioFinder StudioFinder
|
||||
}
|
||||
|
||||
// MatchRelationships accepts a scraped scene and attempts to match its relationships to existing stash models.
|
||||
func (r SceneRelationships) MatchRelationships(ctx context.Context, s *models.ScrapedScene, endpoint string) error {
|
||||
thisStudio := s.Studio
|
||||
for thisStudio != nil {
|
||||
if err := ScrapedStudio(ctx, r.StudioFinder, s.Studio, endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
thisStudio = thisStudio.Parent
|
||||
}
|
||||
|
||||
for _, p := range s.Performers {
|
||||
err := ScrapedPerformer(ctx, r.PerformerFinder, p, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range s.Tags {
|
||||
err := ScrapedTag(ctx, r.TagFinder, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ScrapedPerformer matches the provided performer with the
|
||||
// performers in the database and sets the ID field if one is found.
|
||||
func ScrapedPerformer(ctx context.Context, qb PerformerFinder, p *models.ScrapedPerformer, stashBoxEndpoint *string) error {
|
||||
func ScrapedPerformer(ctx context.Context, qb PerformerFinder, p *models.ScrapedPerformer, stashBoxEndpoint string) error {
|
||||
if p.StoredID != nil || p.Name == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if a performer with the StashID already exists
|
||||
if stashBoxEndpoint != nil && p.RemoteSiteID != nil {
|
||||
if stashBoxEndpoint != "" && p.RemoteSiteID != nil {
|
||||
performers, err := qb.FindByStashID(ctx, models.StashID{
|
||||
StashID: *p.RemoteSiteID,
|
||||
Endpoint: *stashBoxEndpoint,
|
||||
Endpoint: stashBoxEndpoint,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -73,16 +107,16 @@ type StudioFinder interface {
|
||||
|
||||
// ScrapedStudio matches the provided studio with the studios
|
||||
// in the database and sets the ID field if one is found.
|
||||
func ScrapedStudio(ctx context.Context, qb StudioFinder, s *models.ScrapedStudio, stashBoxEndpoint *string) error {
|
||||
func ScrapedStudio(ctx context.Context, qb StudioFinder, s *models.ScrapedStudio, stashBoxEndpoint string) error {
|
||||
if s.StoredID != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if a studio with the StashID already exists
|
||||
if stashBoxEndpoint != nil && s.RemoteSiteID != nil {
|
||||
if stashBoxEndpoint != "" && s.RemoteSiteID != nil {
|
||||
studios, err := qb.FindByStashID(ctx, models.StashID{
|
||||
StashID: *s.RemoteSiteID,
|
||||
Endpoint: *stashBoxEndpoint,
|
||||
Endpoint: stashBoxEndpoint,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -118,6 +152,19 @@ func ScrapedStudio(ctx context.Context, qb StudioFinder, s *models.ScrapedStudio
|
||||
return nil
|
||||
}
|
||||
|
||||
// ScrapedStudioHierarchy executes ScrapedStudio for the provided studio and its parents recursively.
|
||||
func ScrapedStudioHierarchy(ctx context.Context, qb StudioFinder, s *models.ScrapedStudio, stashBoxEndpoint string) error {
|
||||
if err := ScrapedStudio(ctx, qb, s, stashBoxEndpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Parent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ScrapedStudioHierarchy(ctx, qb, s.Parent, stashBoxEndpoint)
|
||||
}
|
||||
|
||||
// ScrapedGroup matches the provided movie with the movies
|
||||
// in the database and returns the ID field if one is found.
|
||||
func ScrapedGroup(ctx context.Context, qb GroupNamesFinder, storedID *string, name *string) (matchedID *string, err error) {
|
||||
|
||||
17
pkg/models/custom_fields.go
Normal file
17
pkg/models/custom_fields.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import "context"
|
||||
|
||||
type CustomFieldMap map[string]interface{}
|
||||
|
||||
type CustomFieldsInput struct {
|
||||
// If populated, the entire custom fields map will be replaced with this value
|
||||
Full map[string]interface{} `json:"full"`
|
||||
// If populated, only the keys in this map will be updated
|
||||
Partial map[string]interface{} `json:"partial"`
|
||||
}
|
||||
|
||||
type CustomFieldsReader interface {
|
||||
GetCustomFields(ctx context.Context, id int) (map[string]interface{}, error)
|
||||
GetCustomFieldsBulk(ctx context.Context, ids []int) ([]CustomFieldMap, error)
|
||||
}
|
||||
@@ -194,3 +194,9 @@ type PhashDistanceCriterionInput struct {
|
||||
type OrientationCriterionInput struct {
|
||||
Value []OrientationEnum `json:"value"`
|
||||
}
|
||||
|
||||
type CustomFieldCriterionInput struct {
|
||||
Field string `json:"field"`
|
||||
Value []any `json:"value"`
|
||||
Modifier CriterionModifier `json:"modifier"`
|
||||
}
|
||||
|
||||
@@ -26,18 +26,47 @@ func (f *Fingerprint) Value() string {
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the string representation of the Fingerprint.
|
||||
// It will return an empty string if the Fingerprint is not a string.
|
||||
func (f Fingerprint) String() string {
|
||||
s, _ := f.Fingerprint.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
// Int64 returns the int64 representation of the Fingerprint.
|
||||
// It will return 0 if the Fingerprint is not an int64.
|
||||
func (f Fingerprint) Int64() int64 {
|
||||
v, _ := f.Fingerprint.(int64)
|
||||
return v
|
||||
}
|
||||
|
||||
type Fingerprints []Fingerprint
|
||||
|
||||
func (f *Fingerprints) Remove(type_ string) {
|
||||
func (f Fingerprints) Remove(type_ string) Fingerprints {
|
||||
var ret Fingerprints
|
||||
|
||||
for _, ff := range *f {
|
||||
for _, ff := range f {
|
||||
if ff.Type != type_ {
|
||||
ret = append(ret, ff)
|
||||
}
|
||||
}
|
||||
|
||||
*f = ret
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f Fingerprints) Filter(types ...string) Fingerprints {
|
||||
var ret Fingerprints
|
||||
|
||||
for _, ff := range f {
|
||||
for _, t := range types {
|
||||
if ff.Type == t {
|
||||
ret = append(ret, ff)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// Equals returns true if the contents of this slice are equal to those in the other slice.
|
||||
@@ -87,33 +116,27 @@ func (f Fingerprints) For(type_ string) *Fingerprint {
|
||||
}
|
||||
|
||||
func (f Fingerprints) Get(type_ string) interface{} {
|
||||
for _, fp := range f {
|
||||
if fp.Type == type_ {
|
||||
return fp.Fingerprint
|
||||
}
|
||||
fp := f.For(type_)
|
||||
if fp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return fp.Fingerprint
|
||||
}
|
||||
|
||||
func (f Fingerprints) GetString(type_ string) string {
|
||||
fp := f.Get(type_)
|
||||
if fp != nil {
|
||||
s, _ := fp.(string)
|
||||
return s
|
||||
fp := f.For(type_)
|
||||
if fp == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return ""
|
||||
return fp.String()
|
||||
}
|
||||
|
||||
func (f Fingerprints) GetInt64(type_ string) int64 {
|
||||
fp := f.Get(type_)
|
||||
fp := f.For(type_)
|
||||
if fp != nil {
|
||||
v, _ := fp.(int64)
|
||||
return v
|
||||
return 0
|
||||
}
|
||||
|
||||
return 0
|
||||
return fp.Int64()
|
||||
}
|
||||
|
||||
// AppendUnique appends a fingerprint to the list if a Fingerprint of the same type does not already exist in the list. If one does, then it is updated with o's Fingerprint value.
|
||||
|
||||
@@ -63,6 +63,28 @@ type ImageFilterType struct {
|
||||
UpdatedAt *TimestampCriterionInput `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ImageUpdateInput struct {
|
||||
ClientMutationID *string `json:"clientMutationId"`
|
||||
ID string `json:"id"`
|
||||
Title *string `json:"title"`
|
||||
Code *string `json:"code"`
|
||||
Urls []string `json:"urls"`
|
||||
Date *string `json:"date"`
|
||||
Details *string `json:"details"`
|
||||
Photographer *string `json:"photographer"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Organized *bool `json:"organized"`
|
||||
SceneIds []string `json:"scene_ids"`
|
||||
StudioID *string `json:"studio_id"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
PerformerIds []string `json:"performer_ids"`
|
||||
GalleryIds []string `json:"gallery_ids"`
|
||||
PrimaryFileID *string `json:"primary_file_id"`
|
||||
|
||||
// deprecated
|
||||
URL *string `json:"url"`
|
||||
}
|
||||
|
||||
type ImageDestroyInput struct {
|
||||
ID string `json:"id"`
|
||||
DeleteFile *bool `json:"delete_file"`
|
||||
|
||||
31
pkg/models/jsonschema/load.go
Normal file
31
pkg/models/jsonschema/load.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func loadFile[T any](filePath string) (*T, error) {
|
||||
var ret T
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
jsonParser := json.NewDecoder(file)
|
||||
err = jsonParser.Decode(&ret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func saveFile[T any](filePath string, obj *T) error {
|
||||
if obj == nil {
|
||||
return fmt.Errorf("object must not be nil")
|
||||
}
|
||||
return marshalToFile(filePath, obj)
|
||||
}
|
||||
@@ -65,6 +65,8 @@ type Performer struct {
|
||||
StashIDs []models.StashID `json:"stash_ids,omitempty"`
|
||||
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
|
||||
|
||||
CustomFields map[string]interface{} `json:"custom_fields,omitempty"`
|
||||
|
||||
// deprecated - for import only
|
||||
URL string `json:"url,omitempty"`
|
||||
Twitter string `json:"twitter,omitempty"`
|
||||
|
||||
27
pkg/models/jsonschema/saved_filter.go
Normal file
27
pkg/models/jsonschema/saved_filter.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type SavedFilter struct {
|
||||
Mode models.FilterMode `db:"mode" json:"mode"`
|
||||
Name string `db:"name" json:"name"`
|
||||
FindFilter *models.FindFilterType `json:"find_filter"`
|
||||
ObjectFilter map[string]interface{} `json:"object_filter"`
|
||||
UIOptions map[string]interface{} `json:"ui_options"`
|
||||
}
|
||||
|
||||
func (s SavedFilter) Filename() string {
|
||||
ret := fsutil.SanitiseBasename(s.Name + "_" + s.Mode.String())
|
||||
return ret + ".json"
|
||||
}
|
||||
|
||||
func LoadSavedFilterFile(filePath string) (*SavedFilter, error) {
|
||||
return loadFile[SavedFilter](filePath)
|
||||
}
|
||||
|
||||
func SaveSavedFilterFile(filePath string, image *SavedFilter) error {
|
||||
return saveFile[SavedFilter](filePath, image)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
type SceneMarker struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Seconds string `json:"seconds,omitempty"`
|
||||
EndSeconds string `json:"end_seconds,omitempty"`
|
||||
PrimaryTag string `json:"primary_tag,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
CreatedAt json.JSONTime `json:"created_at,omitempty"`
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
type Tag struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
SortName string `json:"sort_name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Favorite bool `json:"favorite,omitempty"`
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
|
||||
@@ -80,11 +80,11 @@ func (_m *PerformerReaderWriter) CountByTagID(ctx context.Context, tagID int) (i
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, newPerformer
|
||||
func (_m *PerformerReaderWriter) Create(ctx context.Context, newPerformer *models.Performer) error {
|
||||
func (_m *PerformerReaderWriter) Create(ctx context.Context, newPerformer *models.CreatePerformerInput) error {
|
||||
ret := _m.Called(ctx, newPerformer)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Performer) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.CreatePerformerInput) error); ok {
|
||||
r0 = rf(ctx, newPerformer)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
@@ -314,6 +314,52 @@ func (_m *PerformerReaderWriter) GetAliases(ctx context.Context, relatedID int)
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetCustomFields provides a mock function with given fields: ctx, id
|
||||
func (_m *PerformerReaderWriter) GetCustomFields(ctx context.Context, id int) (map[string]interface{}, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 map[string]interface{}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int) map[string]interface{}); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetCustomFieldsBulk provides a mock function with given fields: ctx, ids
|
||||
func (_m *PerformerReaderWriter) GetCustomFieldsBulk(ctx context.Context, ids []int) ([]models.CustomFieldMap, error) {
|
||||
ret := _m.Called(ctx, ids)
|
||||
|
||||
var r0 []models.CustomFieldMap
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []int) []models.CustomFieldMap); ok {
|
||||
r0 = rf(ctx, ids)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]models.CustomFieldMap)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok {
|
||||
r1 = rf(ctx, ids)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetImage provides a mock function with given fields: ctx, performerID
|
||||
func (_m *PerformerReaderWriter) GetImage(ctx context.Context, performerID int) ([]byte, error) {
|
||||
ret := _m.Called(ctx, performerID)
|
||||
@@ -502,11 +548,11 @@ func (_m *PerformerReaderWriter) QueryForAutoTag(ctx context.Context, words []st
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, updatedPerformer
|
||||
func (_m *PerformerReaderWriter) Update(ctx context.Context, updatedPerformer *models.Performer) error {
|
||||
func (_m *PerformerReaderWriter) Update(ctx context.Context, updatedPerformer *models.UpdatePerformerInput) error {
|
||||
ret := _m.Called(ctx, updatedPerformer)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Performer) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.UpdatePerformerInput) error); ok {
|
||||
r0 = rf(ctx, updatedPerformer)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user