Compare commits

..

2 Commits

Author SHA1 Message Date
AdultSun
ac5dfdece2 Expands quickstart language based on DogmaDragon's suggestions 2024-03-11 15:02:14 -07:00
AdultSun
eb658834c3 Update StashDB details in README.md
- Directs users to new guide in the StashDB docs instead of Discord
- No longer necessary to join Discord/Matrix for new users of StashDB now that invite codes are multi-use
- Updates formatting of the same "Quickstart Guide" section a little
2024-03-10 17:32:46 -07:00
908 changed files with 26714 additions and 61373 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: stashapp
# patreon: # Replace with a single Patreon username
open_collective: stashapp
# ko_fi: # Replace with a single Ko-fi username
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
# liberapay: StashApp
# issuehunt: # Replace with a single IssueHunt username
# otechie: # Replace with a single Otechie username
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -12,22 +12,17 @@ concurrency:
cancel-in-progress: true
env:
COMPILER_IMAGE: stashapp/compiler:10
COMPILER_IMAGE: stashapp/compiler:8
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Checkout
run: git fetch --prune --unshallow --tags
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Pull compiler image
run: docker pull $COMPILER_IMAGE
@@ -97,23 +92,20 @@ jobs:
docker exec -t build /bin/bash -c "make build-cc-linux-arm32v6"
docker exec -t build /bin/bash -c "make build-cc-freebsd"
- name: Zip UI
run: docker exec -t build /bin/bash -c "make zip-ui"
- name: Cleanup build container
run: docker rm -f -v build
- name: Generate checksums
run: |
git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
sha1sum dist/Stash.app.zip dist/stash-* dist/stash-ui.zip | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
sha1sum dist/Stash.app.zip dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
echo "STASH_VERSION=$(git describe --tags --exclude latest_develop)" >> $GITHUB_ENV
echo "RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_ENV
- name: Upload Windows binary
# only upload binaries for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: stash-win.exe
path: dist/stash-win.exe
@@ -121,7 +113,7 @@ jobs:
- name: Upload macOS binary
# only upload binaries for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: stash-macos
path: dist/stash-macos
@@ -129,19 +121,11 @@ jobs:
- name: Upload Linux binary
# only upload binaries for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: stash-linux
path: dist/stash-linux
- name: Upload UI
# only upload for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v4
with:
name: stash-ui.zip
path: dist/stash-ui.zip
- name: Update latest_develop tag
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
run : git tag -f latest_develop; git push -f --tags
@@ -163,7 +147,6 @@ jobs:
dist/stash-linux-arm32v7
dist/stash-linux-arm32v6
dist/stash-freebsd
dist/stash-ui.zip
CHECKSUMS_SHA1
- name: Master release
@@ -183,7 +166,6 @@ jobs:
dist/stash-linux-arm32v7
dist/stash-linux-arm32v6
dist/stash-freebsd
dist/stash-ui.zip
CHECKSUMS_SHA1
gzip: false

View File

@@ -9,7 +9,7 @@ on:
pull_request:
env:
COMPILER_IMAGE: stashapp/compiler:10
COMPILER_IMAGE: stashapp/compiler:8
jobs:
golangci:
@@ -21,11 +21,6 @@ jobs:
- name: Checkout
run: git fetch --prune --unshallow --tags
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Pull compiler image
run: docker pull $COMPILER_IMAGE
@@ -38,7 +33,7 @@ jobs:
run: docker exec -t build /bin/bash -c "make generate-backend"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v3
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
View File

@@ -21,9 +21,6 @@ vendor
# GraphQL generated output
internal/api/generated_*.go
# Generated locale files
ui/login/locales/*
####
# Visual Studio
####

View File

@@ -15,11 +15,11 @@ linters:
- unused
# Linters added by the stash project.
# - contextcheck
- copyloopvar
- dogsled
- errchkjson
- errorlint
# - exhaustive
- exportloopref
- gocritic
# - goerr113
- gofmt
@@ -48,6 +48,8 @@ linters-settings:
ignore-generated-header: true
severity: error
confidence: 0.8
error-code: 1
warning-code: 1
rules:
- name: blank-imports
disabled: true

View File

@@ -281,10 +281,6 @@ 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
@@ -311,8 +307,7 @@ test:
# runs all tests - including integration tests
.PHONY: it
it:
$(eval GO_BUILD_TAGS += integration)
go test -tags "$(GO_BUILD_TAGS)" ./...
go test -tags=integration ./...
# generates test mocks
.PHONY: generate-test-mocks
@@ -355,17 +350,9 @@ ifdef STASH_SOURCEMAPS
endif
.PHONY: ui
ui: ui-only generate-login-locale
.PHONY: ui-only
ui-only: ui-env
ui: ui-env
cd ui/v2.5 && yarn build
.PHONY: zip-ui
zip-ui:
rm -f dist/stash-ui.zip
cd ui/v2.5/build && zip -r ../../../dist/stash-ui.zip .
.PHONY: ui-start
ui-start: ui-env
cd ui/v2.5 && yarn start --host
@@ -379,20 +366,6 @@ fmt-ui:
validate-ui:
cd ui/v2.5 && yarn run validate
# these targets run the same steps as fmt-ui and validate-ui, but only on files that have changed
fmt-ui-quick:
cd ui/v2.5 && yarn run prettier --write $$(git diff --name-only --relative --diff-filter d . ../../graphql)
# does not run tsc checks, as they are slow
validate-ui-quick:
cd ui/v2.5 && \
tsfiles=$$(git diff --name-only --relative --diff-filter d src | grep -e "\.tsx\?\$$"); \
scssfiles=$$(git diff --name-only --relative --diff-filter d src | grep "\.scss"); \
prettyfiles=$$(git diff --name-only --relative --diff-filter d . ../../graphql); \
if [ -n "$$tsfiles" ]; then yarn run eslint $$tsfiles; fi && \
if [ -n "$$scssfiles" ]; then yarn run stylelint $$scssfiles; fi && \
if [ -n "$$prettyfiles" ]; then yarn run prettier --check $$prettyfiles; fi
# runs all of the backend PR-acceptance steps
.PHONY: validate-backend
validate-backend: lint it

View File

@@ -5,6 +5,7 @@
[![GitHub Sponsors](https://img.shields.io/github/sponsors/stashapp?logo=github)](https://github.com/sponsors/stashapp)
[![Open Collective backers](https://img.shields.io/opencollective/backers/stashapp?logo=opencollective)](https://opencollective.com/stashapp)
[![Go Report Card](https://goreportcard.com/badge/github.com/stashapp/stash)](https://goreportcard.com/report/github.com/stashapp/stash)
[![Matrix](https://img.shields.io/matrix/stashapp:unredacted.org?logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#stashapp:unredacted.org)
[![Discord](https://img.shields.io/discord/559159668438728723.svg?logo=discord)](https://discord.gg/2TsNFKt)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/stashapp/stash?logo=github)](https://github.com/stashapp/stash/releases/latest)
[![GitHub issues by-label](https://img.shields.io/github/issues-raw/stashapp/stash/bounty)](https://github.com/stashapp/stash/labels/bounty)
@@ -23,11 +24,6 @@ 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>
@@ -52,7 +48,7 @@ Stash is a web-based application. Once the application is running, the interface
On first run, Stash will prompt you for some configuration options and media directories to index, called "Scanning" in Stash. After scanning, your media will be available for browsing, curating, editing, and tagging.
Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/docs/en/Manual/Scraping.md), which integrate directly into Stash. Identifying an entire collection will typically require a mix of multiple sources:
Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en/Scraping.md), which integrate directly into Stash. Identifying an entire collection will typically require a mix of multiple sources:
- The project maintains [StashDB](https://stashdb.org/), a crowd-sourced repository of scene, studio, and performer information. Connecting it to Stash will allow you to automatically identify much of a typical media collection. It runs on our stash-box software and is primarily focused on mainstream digital scenes and studios. Instructions, invite codes, and more can be found in this guide to [Accessing StashDB](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stashdb/).
- Several community-managed stash-box databases can also be connected to Stash in a similar manner. Each one serves a slightly different niche and follows their own methodology. A rundown of each stash-box, their differences, and the information you need to sign up can be found in this guide to [Accessing Stash-Boxes](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stash-boxes/).
- Many community-maintained scrapers can also be downloaded, installed, and updated from within Stash, allowing you to pull data from a wide range of other websites and databases. They can be found by navigating to Settings -> Metadata Providers -> Available Scrapers -> Community (stable). These can be trickier to use than a stash-box because every scraper works a little differently. For more information, please visit the [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers).
@@ -61,15 +57,10 @@ Stash can pull metadata (performers, tags, descriptions, studios, and more) dire
<sub>[StashDB](http://stashdb.org) is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box).</sub>
# Translation
[![Translate](https://translate.codeberg.org/widget/stash/stash/svg-badge.svg)](https://translate.codeberg.org/engage/stash/)
[![Translate](https://hosted.weblate.org/widget/stashapp/stash/svg-badge.svg)](https://hosted.weblate.org/engage/stashapp/)
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇪🇪 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇷🇺 🇪🇸 🇸🇪 🇹🇼 🇹🇷
Stash is available in 32 languages (so far!) and it could be in your language too. We use Weblate to coordinate community translations. If you want to help us translate Stash into your language, you can make an account at [Codeberg's Weblate](https://translate.codeberg.org/projects/stash/stash/) to get started contributing new languages or improving existing ones. Thanks!
[![Translation status](https://translate.codeberg.org/widget/stash/stash/multi-auto.svg)](https://translate.codeberg.org/engage/stash/)
## Join Our Community
We are excited to announce that we have a new home for support, feature requests, and discussions related to Stash and its associated projects. Join our community on the [Discourse forum](https://discourse.stashapp.cc) to connect with other users, share your ideas, and get help from fellow enthusiasts.
Stash is available in 25 languages (so far!) and it could be in your language too. We use Weblate to coordinate community translations. If you want to help us translate Stash into your language, you can make an account at [Stash's Weblate](https://hosted.weblate.org/projects/stashapp/stash/) to get started contributing new languages or improving existing ones. Thanks!
# Support (FAQ)
@@ -77,17 +68,16 @@ Check out our documentation on [Stash-Docs](https://docs.stashapp.cc) for inform
For more help you can:
* Check the in-app documentation, in the top right corner of the app (it's also mirrored on [Stash-Docs](https://docs.stashapp.cc/in-app-manual))
* Join our [community forum](https://discourse.stashapp.cc)
* Join the [Discord server](https://discord.gg/2TsNFKt)
* Join the [Matrix space](https://matrix.to/#/#stashapp:unredacted.org)
* Join the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support.
* Start a [discussion on GitHub](https://github.com/stashapp/stash/discussions)
# 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/themes/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/user-interface-ui/custom-css-snippets).
# For Developers

View File

@@ -4,7 +4,6 @@ package main
import (
"fmt"
"os"
"os/exec"
flag "github.com/spf13/pflag"
"github.com/stashapp/stash/pkg/ffmpeg"
@@ -18,7 +17,7 @@ func customUsage() {
flag.PrintDefaults()
}
func printPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, quiet *bool) error {
func printPhash(ff *ffmpeg.FFMpeg, ffp ffmpeg.FFProbe, inputfile string, quiet *bool) error {
ffvideoFile, err := ffp.NewVideoFile(inputfile)
if err != nil {
return err
@@ -46,13 +45,6 @@ func printPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, quiet
return nil
}
func getPaths() (string, string) {
ffmpegPath, _ := exec.LookPath("ffmpeg")
ffprobePath, _ := exec.LookPath("ffprobe")
return ffmpegPath, ffprobePath
}
func main() {
flag.Usage = customUsage
quiet := flag.BoolP("quiet", "q", false, "print only the phash")
@@ -77,10 +69,10 @@ func main() {
fmt.Fprintf(os.Stderr, "Example: parallel %v ::: *.mp4\n", os.Args[0])
}
ffmpegPath, ffprobePath := getPaths()
ffmpegPath, ffprobePath := ffmpeg.GetPaths(nil)
encoder := ffmpeg.NewEncoder(ffmpegPath)
// don't need to InitHWSupport, phashing doesn't use hw acceleration
ffprobe := ffmpeg.NewFFProbe(ffprobePath)
ffprobe := ffmpeg.FFProbe(ffprobePath)
for _, item := range args {
if err := printPhash(encoder, ffprobe, item, quiet); err != nil {

View File

@@ -37,8 +37,6 @@ func main() {
defer recoverPanic()
initLogTemp()
helpFlag := false
pflag.BoolVarP(&helpFlag, "help", "h", false, "show this help text and exit")
@@ -106,16 +104,6 @@ func main() {
exitCode = <-exit
}
// initLogTemp initializes a temporary logger for use before the config is loaded.
// Logs only error level message to stderr.
func initLogTemp() *log.Logger {
l := log.NewLogger()
l.Init("", true, "Error")
logger.Logger = l
return l
}
func initLog(cfg *config.Config) *log.Logger {
l := log.NewLogger()
l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel())
@@ -152,9 +140,6 @@ func recoverPanic() {
func exitError(err error) {
exitCode = 1
logger.Error(err)
// #5784 - log to stdout as well as the logger
// this does mean that it will log twice if the logger is set to stdout
fmt.Println(err)
if desktop.IsDesktop() {
desktop.FatalError(err)
}

View File

@@ -1,7 +1,7 @@
# This dockerfile should be built with `make docker-build` from the stash root.
# Build Frontend
FROM node:20-alpine AS frontend
FROM node: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,22 +13,19 @@ 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-only
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
# Build Backend
FROM golang:1.22.8-alpine AS backend
FROM golang:1.19-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/
# needed for generate-login-locale
COPY ./ui /stash/ui/
RUN make generate-backend generate-login-locale
COPY ./cmd /stash/cmd
COPY ./internal /stash/internal
COPY --from=frontend /stash /stash/
RUN make generate-backend
ARG GITHASH
ARG STASH_VERSION
RUN make flags-release flags-pie stash

View File

@@ -1,8 +1,7 @@
# This dockerfile should be built with `make docker-cuda-build` from the stash root.
ARG CUDA_VERSION=12.8.0
# Build Frontend
FROM node:20-alpine AS frontend
FROM node: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/
@@ -14,47 +13,37 @@ 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-only
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
# Build Backend
FROM golang:1.22.8-bullseye AS backend
FROM golang:1.19-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
# Final Runnable Image
FROM nvidia/cuda:${CUDA_VERSION}-base-ubuntu24.04
RUN apt update && apt upgrade -y && apt install -y \
# stash dependencies
ca-certificates libvips-tools ffmpeg \
# intel dependencies
intel-media-va-driver-non-free vainfo \
# python tools
python3 python3-pip && \
# cleanup
apt autoremove -y && apt clean && \
rm -rf /var/lib/apt/lists/*
COPY --from=backend --chmod=555 /stash/stash /usr/bin/
FROM nvidia/cuda:12.0.1-base-ubuntu22.04
RUN apt update && apt upgrade -y && apt install -y ca-certificates libvips-tools ffmpeg wget intel-media-va-driver-non-free vainfo
RUN rm -rf /var/lib/apt/lists/*
COPY --from=backend /stash/stash /usr/bin/
# NVENC Patch
RUN mkdir -p /usr/local/bin /patched-lib
ADD --chmod=555 https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh /usr/local/bin/patch.sh
ADD --chmod=555 https://raw.githubusercontent.com/keylase/nvidia-patch/master/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh -O /usr/local/bin/patch.sh
RUN wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/docker-entrypoint.sh -O /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/patch.sh /usr/local/bin/docker-entrypoint.sh /usr/bin/stash
ENV LANG=C.UTF-8
ENV NVIDIA_VISIBLE_DEVICES=all
ENV LANG C.UTF-8
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES=video,utility
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999

View File

@@ -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 `tools.go` file):
From the top-level directory (should contain `main.go` file):
```
make docker-build

View File

@@ -12,18 +12,16 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-linux-arm32v6; \
FROM --platform=$TARGETPLATFORM alpine:latest AS app
COPY --from=binary /stash /usr/bin/
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
# vips version 8.15.0-r0 breaks thumbnail generation on arm32v6
# need to use 8.14.3-r0 from alpine 3.18 instead
# 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"
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 \
&& gem install faraday \
&& apk del .build-deps
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999
CMD ["stash"]

View File

@@ -1,4 +1,4 @@
FROM golang:1.22.8
FROM golang:1.19
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 13.4
ENV FREEBSD_VERSION 12.4
ENV FREEBSD_DOWNLOAD_URL http://ftp.plusline.de/FreeBSD/releases/amd64/${FREEBSD_VERSION}-RELEASE/base.txz
ENV FREEBSD_SHA 8e13b0a93daba349b8d28ad246d7beb327659b2ef4fe44d89f447392daec5a7c
ENV FREEBSD_SHA 581c7edacfd2fca2bdf5791f667402d22fccd8a5e184635e0cac075564d57aa8
RUN cd /tmp && \
curl -o base.txz $FREEBSD_DOWNLOAD_URL && \

View File

@@ -1,6 +1,6 @@
user=stashapp
repo=compiler
version=10
version=8
latest:
docker build -t ${user}/${repo}:latest .

View File

@@ -1,14 +1,12 @@
# Docker Installation (for most 64-bit GNU/Linux systems)
StashApp is supported on most systems that support Docker. Your OS likely ships with or makes available the necessary packages.
StashApp is supported on most systems that support Docker and docker-compose. Your OS likely ships with or makes available the necessary packages.
## Dependencies
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.
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.
Installation instructions are available below, and if your distributions's repository ships a current version of docker, you may use that.
Installation instructions are available below, and if your distrobution'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:
@@ -21,7 +19,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
@@ -31,9 +29,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 run stash, including ffmpeg.
The StashApp docker container ships with everything you need to automatically build and 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](https://docs.stashapp.cc/guides/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 (such as NGINX or Traefik) is recommended, but not required.
The latest version is always recommended.

View File

@@ -1,5 +1,6 @@
# APPNICENAME=Stash
# APPDESCRIPTION=An organizer for your porn, written in Go
version: '3.4'
services:
stash:
image: stashapp/stash:latest
@@ -26,12 +27,10 @@ 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

View File

@@ -1,24 +1,3 @@
## Goals and design vision
The goal of stash is to be:
- an application for organising and viewing adult content - currently this is videos and images, in future this will be extended to include audio and text content
- organising includes scraping of metadata from websites and metadata repositories
- free and open-source
- portable and offline - can be run on a USB stick without needing to install dependencies (with the exception of ffmpeg)
- minimal, but highly extensible. The core feature set should be the minimum required to achieve the primary goal, while being extensible enough to extend via plugins
- easy to learn and use, with minimal technical knowledge required
The core stash system is not intended for:
- managing downloading of content
- managing content on external websites
- publically sharing content
Other requirements:
- support as many video and image formats as possible
- interfaces with external systems (for example stash-box) should be made as generic as possible.
Design considerations:
- features are easy to add and difficult to remove. Large superfluous features should be scrutinised and avoided where possible (eg DLNA, filename parser). Such features should be considered for third-party plugins instead.
## Technical Debt
Please be sure to consider how heavily your contribution impacts the maintainability of the project long term, sometimes less is more. We don't want to merge collossal pull requests with hundreds of dependencies by a driveby contributor.

View File

@@ -4,7 +4,7 @@
* [Go](https://golang.org/dl/)
* [GolangCI](https://golangci-lint.run/) - A meta-linter which runs several linters in parallel
* To install, follow the [local installation instructions](https://golangci-lint.run/welcome/install/#local-installation)
* To install, follow the [local installation instructions](https://golangci-lint.run/usage/install/#local-installation)
* [Yarn](https://yarnpkg.com/en/docs/install) - Yarn package manager
## Environment
@@ -69,9 +69,6 @@ NOTE: The `make` command in OpenBSD will be `gmake`. For example, `make pre-ui`
* `make it` - Runs all unit and integration tests
* `make fmt` - Formats the Go source code
* `make fmt-ui` - Formats the UI source code
* `make validate-ui` - Runs tests and checks for the UI only
* `make fmt-ui-quick` - (experimental) Formats only changed UI source code
* `make validate-ui-quick` - (experimental) Runs tests and checks of changed UI code
* `make server-start` - Runs a development stash server in the `.local` directory
* `make server-clean` - Removes the `.local` directory and all of its contents
* `make ui-start` - Runs the UI in development mode. Requires a running Stash server to connect to - the server URL can be changed from the default of `http://localhost:9999` using the environment variable `VITE_APP_PLATFORM_URL`, but keep in mind that authentication cannot be used since the session authorization cookie cannot be sent cross-origin. The UI runs on port `3000` or the next available port.

76
go.mod
View File

@@ -1,96 +1,90 @@
module github.com/stashapp/stash
go 1.22.8
go 1.19
require (
github.com/99designs/gqlgen v0.17.55
github.com/99designs/gqlgen v0.17.2
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552
github.com/Yamashou/gqlgenc v0.25.3
github.com/Yamashou/gqlgenc v0.0.6
github.com/anacrolix/dms v1.2.2
github.com/antchfx/htmlquery v1.3.0
github.com/asticode/go-astisub v0.25.1
github.com/asticode/go-astisub v0.26.0
github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617
github.com/chromedp/chromedp v0.9.2
github.com/corona10/goimagehash v1.1.0
github.com/disintegration/imaging v1.6.2
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
github.com/doug-martin/goqu/v9 v9.18.0
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/chi/v5 v5.0.10
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/golang-jwt/jwt/v4 v4.5.2
github.com/gofrs/uuid/v5 v5.0.0
github.com/golang-jwt/jwt/v4 v4.5.0
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/hashicorp/golang-lru/v2 v2.0.6
github.com/jinzhu/copier v0.4.0
github.com/jmoiron/sqlx v1.4.0
github.com/jmoiron/sqlx v1.3.5
github.com/json-iterator/go v1.1.12
github.com/kermieisinthehouse/gosx-notifier v0.1.2
github.com/kermieisinthehouse/systray v1.2.4
github.com/knadh/koanf v1.5.0
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-sqlite3 v1.14.22
github.com/mitchellh/mapstructure v1.5.0
github.com/mattn/go-sqlite3 v1.14.17
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
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/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
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/cast v1.5.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4
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.18
github.com/vektah/gqlparser/v2 v2.4.2
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.31.0
golang.org/x/image v0.18.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
golang.org/x/crypto v0.17.0
golang.org/x/image v0.12.0
golang.org/x/net v0.17.0
golang.org/x/sys v0.15.0
golang.org/x/term v0.15.0
golang.org/x/text v0.14.0
gopkg.in/guregu/null.v4 v4.0.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/agnivade/levenshtein v1.2.0 // indirect
github.com/agnivade/levenshtein v1.1.1 // 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/coder/websocket v1.8.12 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // 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
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
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/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matryer/moq v0.2.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
@@ -100,21 +94,19 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/zerolog v1.30.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/objx v0.5.0 // indirect
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.5 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/urfave/cli/v2 v2.8.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/atomic v1.11.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
golang.org/x/mod v0.12.0 // indirect
golang.org/x/tools v0.13.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

294
go.sum
View File

@@ -49,30 +49,27 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
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.55 h1:3vzrNWYyzSZjGDFo68e5j9sSauLxfKvLp+6ioRokVtM=
github.com/99designs/gqlgen v0.17.55/go.mod h1:3Bq768f8hgVPGZxL8aY9MaYmbxa6llPM/qu1IGH1EJo=
github.com/99designs/gqlgen v0.17.2 h1:yczvlwMsfcVu/JtejqfrLwXuSP0yZFhmcss3caEvHw8=
github.com/99designs/gqlgen v0.17.2/go.mod h1:K5fzLKwtph+FFgh9j7nFbRUdBKvTcGnsta51fsMTn3o=
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.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.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/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/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=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/anacrolix/dms v1.2.2 h1:0mk2/DXNqa5KDDbaLgFPf3oMV6VCGdFNh3d/gt4oafM=
github.com/anacrolix/dms v1.2.2/go.mod h1:msPKAoppoNRfrYplJqx63FZ+VipDZ4Xsj3KzIQxyU7k=
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
@@ -83,9 +80,6 @@ github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes=
@@ -100,26 +94,15 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asticode/go-astikit v0.20.0 h1:+7N+J4E4lWx2QOkRdOf6DafWJMv6O4RRfgClwQokrH8=
github.com/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astisub v0.25.1 h1:RZMGfZPp7CXOkI6g+zCU7DRLuciGPGup921uKZnMXPI=
github.com/asticode/go-astisub v0.25.1/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
github.com/asticode/go-astisub v0.26.0 h1:Ka1oUyWzo/lIx7RX97GI1QdbClqYVxI0ExKuZRN/cDk=
github.com/asticode/go-astisub v0.26.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
github.com/asticode/go-astits v1.8.0 h1:rf6aiiGn/QhlFjNON1n5plqF3Fs025XLUwiQ0NB6oZg=
github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ=
github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bool64/dev v0.2.28 h1:6ayDfrB/jnNr2iQAZHI+uT3Qi6rErSbJYQs1y8rSrwM=
github.com/bool64/dev v0.2.28/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -135,11 +118,8 @@ github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmt
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -153,39 +133,27 @@ 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 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/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-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
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/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=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY=
github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -200,10 +168,7 @@ github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
@@ -211,8 +176,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/httplog v0.3.1 h1:uC3IUWCZagtbCinb3ypFh36SEcgd6StWw2Bu0XSXRtg=
@@ -222,18 +187,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -244,10 +202,12 @@ 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.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.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.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
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-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=
@@ -284,7 +244,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -301,9 +260,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -324,12 +281,8 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@@ -340,13 +293,12 @@ 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=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
@@ -354,8 +306,6 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -365,22 +315,18 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/golang-lru/v2 v2.0.6 h1:3xi/Cafd1NaoEnS/yDssIiuVeDVywU0QdFGl3aQaQHM=
github.com/hashicorp/golang-lru/v2 v2.0.6/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
@@ -390,36 +336,21 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
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=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -427,34 +358,28 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kermieisinthehouse/gosx-notifier v0.1.2 h1:KV0KBeKK2B24kIHY7iK0jgS64Q05f4oB+hUZmsPodxQ=
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=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
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/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
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=
@@ -463,6 +388,8 @@ 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 h1:Q06vEqnBYjjfx5KKgHfYRKE/lvlRu+Nj+xodG4YdHnU=
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=
@@ -477,34 +404,26 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
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=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -514,26 +433,20 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007 h1:Ohgj9L0EYOgXxkDp+bczlMBiulwmqYzQpvQNUdtt3oc=
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007/go.mod h1:wKCOWMb6iNlvKiOToY2cNuaovSXvIiv1zDi9QDR7aGQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -550,29 +463,22 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac h1:kYPjbEN6YPYWWHI6ky1J813KzIq/8+Wg4TO4xU7A/KU=
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -580,28 +486,25 @@ 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=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
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.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
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=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
@@ -609,8 +512,8 @@ github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfA
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
@@ -625,9 +528,8 @@ github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1Fof
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -637,9 +539,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
@@ -652,21 +553,24 @@ 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.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
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.5.18 h1:zSND3GtutylAQ1JpWnTHcqtaRZjl+y3NROeW8vuNo6Y=
github.com/vektah/gqlparser/v2 v2.5.18/go.mod h1:6HLzf7JKv9Fi3APymudztFQNmLXR5qJeEo6BOFcXVfc=
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.4.2 h1:29TGc6QmhEUq5fll+2FPoTmhUhR65WEKN4VK/jo0OlM=
github.com/vektah/gqlparser/v2 v2.4.2/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
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-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
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=
@@ -678,11 +582,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
github.com/zencoder/go-dash/v3 v3.0.2 h1:oP1+dOh+Gp57PkvdCyMfbHtrHaxfl3w4kR3KBBbuqQE=
github.com/zencoder/go-dash/v3 v3.0.2/go.mod h1:30R5bKy1aUYY45yesjtZ9l8trNc2TwNqbS17WVQmCzk=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -713,8 +614,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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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=
@@ -728,8 +629,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -755,9 +656,11 @@ 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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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=
@@ -807,8 +710,9 @@ 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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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=
@@ -838,18 +742,16 @@ 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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
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=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -861,12 +763,10 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -881,8 +781,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -890,7 +788,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -903,7 +800,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -921,41 +817,41 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.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.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
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=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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=
@@ -998,6 +894,7 @@ 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=
@@ -1014,9 +911,12 @@ 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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
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=
@@ -1063,7 +963,6 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -1127,11 +1026,9 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
@@ -1172,12 +1069,10 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg=
gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI=
@@ -1185,14 +1080,14 @@ gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.3/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -1209,4 +1104,3 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

View File

@@ -7,6 +7,9 @@ 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
@@ -17,7 +20,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/stashbox
- github.com/stashapp/stash/pkg/scraper/stashbox
models:
# Scalars
@@ -35,6 +38,12 @@ 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
@@ -42,11 +51,6 @@ models:
fieldName: DurationFinite
frame_rate:
fieldName: FrameRateFinite
# movie is group under the hood
Movie:
model: github.com/stashapp/stash/pkg/models.Group
MovieFilterType:
model: github.com/stashapp/stash/pkg/models.GroupFilterType
# autobind on config causes generation issues
BlobsStorageType:
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
@@ -123,6 +127,9 @@ 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:

View File

@@ -4,7 +4,6 @@ type Query {
findSavedFilter(id: ID!): SavedFilter
findSavedFilters(mode: FilterMode): [SavedFilter!]!
findDefaultFilter(mode: FilterMode!): SavedFilter
@deprecated(reason: "default filter now stored in UI config")
"Find a scene by ID or Checksum"
findScene(id: ID, checksum: String): Scene
@@ -45,7 +44,6 @@ type Query {
findSceneMarkers(
scene_marker_filter: SceneMarkerFilterType
filter: FindFilterType
ids: [ID!]
): FindSceneMarkersResultType!
findImage(id: ID, checksum: String): Image
@@ -78,22 +76,13 @@ type Query {
): FindStudiosResultType!
"Find a movie by ID"
findMovie(id: ID!): Movie @deprecated(reason: "Use findGroup instead")
findMovie(id: ID!): Movie
"A function which queries Movie objects"
findMovies(
movie_filter: MovieFilterType
filter: FindFilterType
ids: [ID!]
): FindMoviesResultType! @deprecated(reason: "Use findGroups instead")
"Find a group by ID"
findGroup(id: ID!): Group
"A function which queries Group objects"
findGroups(
group_filter: GroupFilterType
filter: FindFilterType
ids: [ID!]
): FindGroupsResultType!
): FindMoviesResultType!
findGallery(id: ID!): Gallery
findGalleries(
@@ -166,19 +155,7 @@ type Query {
scrapeSingleMovie(
source: ScraperSourceInput!
input: ScrapeSingleMovieInput!
): [ScrapedMovie!]! @deprecated(reason: "Use scrapeSingleGroup instead")
"Scrape for a single group"
scrapeSingleGroup(
source: ScraperSourceInput!
input: ScrapeSingleGroupInput!
): [ScrapedGroup!]!
"Scrape for a single image"
scrapeSingleImage(
source: ScraperSourceInput!
input: ScrapeSingleImageInput!
): [ScrapedImage!]!
): [ScrapedMovie!]!
"Scrapes content based on a URL"
scrapeURL(url: String!, ty: ScrapeContentType!): ScrapedContent
@@ -189,13 +166,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")
"Scrapes a complete group record based on a URL"
scrapeGroupURL(url: String!): ScrapedGroup
# Plugins
"List loaded plugins"
@@ -241,7 +213,7 @@ type Query {
allPerformers: [Performer!]!
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
allMovies: [Movie!]! @deprecated(reason: "Use findGroups instead")
allMovies: [Movie!]! @deprecated(reason: "Use findMovies instead")
# Get everything with minimal metadata
@@ -254,12 +226,7 @@ type Query {
type Mutation {
setup(input: SetupInput!): Boolean!
"Migrates the schema to the required version. Returns the job ID"
migrate(input: MigrateInput!): ID!
"Downloads and installs ffmpeg and ffprobe binaries into the configuration directory. Returns the job ID."
downloadFFMpeg: ID!
migrate(input: MigrateInput!): Boolean!
sceneCreate(input: SceneCreateInput!): Scene
sceneUpdate(input: SceneUpdateInput!): Scene
@@ -285,13 +252,6 @@ type Mutation {
"Sets the resume time point (if provided) and adds the provided duration to the scene's play duration"
sceneSaveActivity(id: ID!, resume_time: Float, playDuration: Float): Boolean!
"Resets the resume time point and play duration"
sceneResetActivity(
id: ID!
reset_resume: Boolean
reset_duration: Boolean
): Boolean!
"Increments the play count for the scene. Returns the new play count value."
sceneIncrementPlayCount(id: ID!): Int!
@deprecated(reason: "Use sceneAddPlay instead")
@@ -309,7 +269,6 @@ type Mutation {
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean!
sceneMarkersDestroy(ids: [ID!]!): Boolean!
sceneAssignFile(input: AssignSceneFileInput!): Boolean!
@@ -334,8 +293,6 @@ type Mutation {
addGalleryImages(input: GalleryAddInput!): Boolean!
removeGalleryImages(input: GalleryRemoveInput!): Boolean!
setGalleryCover(input: GallerySetCoverInput!): Boolean!
resetGalleryCover(input: GalleryResetCoverInput!): Boolean!
galleryChapterCreate(input: GalleryChapterCreateInput!): GalleryChapter
galleryChapterUpdate(input: GalleryChapterUpdateInput!): GalleryChapter
@@ -353,34 +310,16 @@ type Mutation {
studiosDestroy(ids: [ID!]!): Boolean!
movieCreate(input: MovieCreateInput!): Movie
@deprecated(reason: "Use groupCreate instead")
movieUpdate(input: MovieUpdateInput!): Movie
@deprecated(reason: "Use groupUpdate instead")
movieDestroy(input: MovieDestroyInput!): Boolean!
@deprecated(reason: "Use groupDestroy instead")
moviesDestroy(ids: [ID!]!): Boolean!
@deprecated(reason: "Use groupsDestroy instead")
bulkMovieUpdate(input: BulkMovieUpdateInput!): [Movie!]
@deprecated(reason: "Use bulkGroupUpdate instead")
groupCreate(input: GroupCreateInput!): Group
groupUpdate(input: GroupUpdateInput!): Group
groupDestroy(input: GroupDestroyInput!): Boolean!
groupsDestroy(ids: [ID!]!): Boolean!
bulkGroupUpdate(input: BulkGroupUpdateInput!): [Group!]
addGroupSubGroups(input: GroupSubGroupAddInput!): Boolean!
removeGroupSubGroups(input: GroupSubGroupRemoveInput!): Boolean!
"Reorder sub groups within a group. Returns true if successful."
reorderSubGroups(input: ReorderSubGroupsInput!): Boolean!
tagCreate(input: TagCreateInput!): Tag
tagUpdate(input: TagUpdateInput!): Tag
tagDestroy(input: TagDestroyInput!): Boolean!
tagsDestroy(ids: [ID!]!): Boolean!
tagsMerge(input: TagsMergeInput!): Tag
bulkTagUpdate(input: BulkTagUpdateInput!): [Tag!]
"""
Moves the given files to the given destination. Returns true if successful.
@@ -400,7 +339,6 @@ type Mutation {
saveFilter(input: SaveFilterInput!): SavedFilter!
destroySavedFilter(input: DestroyFilterInput!): Boolean!
setDefaultFilter(input: SetDefaultFilterInput!): Boolean!
@deprecated(reason: "now uses UI config")
"Change general configuration options"
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
@@ -411,19 +349,12 @@ type Mutation {
input: ConfigDefaultSettingsInput!
): ConfigDefaultSettingsResult!
"overwrites the entire plugin configuration for the given plugin"
# overwrites the entire plugin configuration for the given plugin
configurePlugin(plugin_id: ID!, input: Map!): Map!
"""
overwrites the UI configuration
if input is provided, then the entire UI configuration is replaced
if partial is provided, then the partial UI configuration is merged into the existing UI configuration
"""
configureUI(input: Map, partial: Map): Map!
"""
sets a single UI key value
key is a dot separated path to the value
"""
# overwrites the entire UI configuration
configureUI(input: Map!): Map!
# sets a single UI key value
configureUISetting(key: String!, value: Any): Map!
"Generate and set (or clear) API key"

View File

@@ -81,10 +81,6 @@ input ConfigGeneralInput {
blobsPath: String
"Where to store blobs"
blobsStorage: BlobsStorageType
"Path to the ffmpeg binary. If empty, stash will attempt to find it in the path or config directory"
ffmpegPath: String
"Path to the ffprobe binary. If empty, stash will attempt to find it in the path or config directory"
ffprobePath: String
"Whether to calculate MD5 checksums for scene video files"
calculateMD5: Boolean
"Hash algorithm to use for generated file naming"
@@ -203,10 +199,6 @@ type ConfigGeneralResult {
blobsPath: String!
"Where to store blobs"
blobsStorage: BlobsStorageType!
"Path to the ffmpeg binary. If empty, stash will attempt to find it in the path or config directory"
ffmpegPath: String!
"Path to the ffprobe binary. If empty, stash will attempt to find it in the path or config directory"
ffprobePath: String!
"Whether to calculate MD5 checksums for scene video files"
calculateMD5: Boolean!
"Hash algorithm to use for generated file naming"
@@ -469,8 +461,6 @@ input ConfigDLNAInput {
serverName: String
"True if DLNA service should be enabled by default"
enabled: Boolean
"Defaults to 1338"
port: Int
"List of IPs whitelisted for DLNA service"
whitelistedIPs: [String!]
"List of interfaces to run DLNA on. Empty for all"
@@ -483,8 +473,6 @@ type ConfigDLNAResult {
serverName: String!
"True if DLNA service should be enabled by default"
enabled: Boolean!
"Defaults to 1338"
port: Int!
"List of IPs whitelisted for DLNA service"
whitelistedIPs: [String!]!
"List of interfaces to run DLNA on. Empty for all"

View File

@@ -8,7 +8,6 @@ input FindFilterType {
page: Int
"use per_page = -1 to indicate all results. Defaults to 25."
per_page: Int
# TODO - this should be refactored to not use a string
sort: String
direction: SortDirectionEnum
}
@@ -91,12 +90,6 @@ input StashIDCriterionInput {
modifier: CriterionModifier!
}
input CustomFieldCriterionInput {
field: String!
value: [Any!]
modifier: CriterionModifier!
}
input PerformerFilterType {
AND: PerformerFilterType
OR: PerformerFilterType
@@ -150,8 +143,6 @@ input PerformerFilterType {
image_count: IntCriterionInput
"Filter by gallery count"
gallery_count: IntCriterionInput
"Filter by play count"
play_count: IntCriterionInput
"Filter by o count"
o_counter: IntCriterionInput
"Filter by StashID"
@@ -176,20 +167,10 @@ input PerformerFilterType {
birthdate: DateCriterionInput
"Filter by death date"
death_date: DateCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
custom_fields: [CustomFieldCriterionInput!]
}
input SceneMarkerFilterType {
@@ -199,10 +180,6 @@ input SceneMarkerFilterType {
scene_tags: HierarchicalMultiCriterionInput
"Filter to only include scene markers with these performers"
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"
@@ -213,8 +190,6 @@ input SceneMarkerFilterType {
scene_created_at: TimestampCriterionInput
"Filter by lscene ast update time"
scene_updated_at: TimestampCriterionInput
"Filter by related scenes that meet this criteria"
scene_filter: SceneFilterType
}
input SceneFilterType {
@@ -254,8 +229,6 @@ input SceneFilterType {
orientation: OrientationCriterionInput
"Filter by frame rate"
framerate: IntCriterionInput
"Filter by bit rate"
bitrate: IntCriterionInput
"Filter by video codec"
video_codec: StringCriterionInput
"Filter by audio codec"
@@ -269,11 +242,7 @@ input SceneFilterType {
"Filter to only include scenes with this studio"
studios: HierarchicalMultiCriterionInput
"Filter to only include scenes with this movie"
movies: MultiCriterionInput @deprecated(reason: "use groups instead")
"Filter to only include scenes with this group"
groups: HierarchicalMultiCriterionInput
"Filter to only include scenes with this gallery"
galleries: MultiCriterionInput
movies: MultiCriterionInput
"Filter to only include scenes with these tags"
tags: HierarchicalMultiCriterionInput
"Filter by tag count"
@@ -304,37 +273,15 @@ input SceneFilterType {
play_count: IntCriterionInput
"Filter by play duration (in seconds)"
play_duration: IntCriterionInput
"Filter by scene last played time"
last_played_at: TimestampCriterionInput
"Filter by date"
date: DateCriterionInput
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by related performers that meet this criteria"
performers_filter: PerformerFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
"Filter by related movies that meet this criteria"
movies_filter: MovieFilterType
@deprecated(reason: "use groups_filter instead")
"Filter by related groups that meet this criteria"
groups_filter: GroupFilterType
"Filter by related markers that meet this criteria"
markers_filter: SceneMarkerFilterType
}
input MovieFilterType {
AND: MovieFilterType
OR: MovieFilterType
NOT: MovieFilterType
name: StringCriterionInput
director: StringCriterionInput
synopsis: StringCriterionInput
@@ -351,68 +298,12 @@ input MovieFilterType {
url: StringCriterionInput
"Filter to only include movies where performer appears in a scene"
performers: MultiCriterionInput
"Filter to only include movies with these tags"
tags: HierarchicalMultiCriterionInput
"Filter by tag count"
tag_count: IntCriterionInput
"Filter by date"
date: DateCriterionInput
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
}
input GroupFilterType {
AND: GroupFilterType
OR: GroupFilterType
NOT: GroupFilterType
name: StringCriterionInput
director: StringCriterionInput
synopsis: StringCriterionInput
"Filter by duration (in seconds)"
duration: IntCriterionInput
# rating expressed as 1-100
rating100: IntCriterionInput
"Filter to only include groups with this studio"
studios: HierarchicalMultiCriterionInput
"Filter to only include groups missing this property"
is_missing: String
"Filter by url"
url: StringCriterionInput
"Filter to only include groups where performer appears in a scene"
performers: MultiCriterionInput
"Filter to only include groups with these tags"
tags: HierarchicalMultiCriterionInput
"Filter by tag count"
tag_count: IntCriterionInput
"Filter by date"
date: DateCriterionInput
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
"Filter by containing groups"
containing_groups: HierarchicalMultiCriterionInput
"Filter by sub groups"
sub_groups: HierarchicalMultiCriterionInput
"Filter by number of containing groups the group has"
containing_group_count: IntCriterionInput
"Filter by number of sub-groups the group has"
sub_group_count: IntCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
}
input StudioFilterType {
@@ -426,22 +317,16 @@ input StudioFilterType {
parents: MultiCriterionInput
"Filter by StashID"
stash_id_endpoint: StashIDCriterionInput
"Filter to only include studios with these tags"
tags: HierarchicalMultiCriterionInput
"Filter to only include studios missing this property"
is_missing: String
# rating expressed as 1-100
rating100: IntCriterionInput
"Filter by favorite"
favorite: Boolean
"Filter by scene count"
scene_count: IntCriterionInput
"Filter by image count"
image_count: IntCriterionInput
"Filter by gallery count"
gallery_count: IntCriterionInput
"Filter by tag count"
tag_count: IntCriterionInput
"Filter by url"
url: StringCriterionInput
"Filter by studio aliases"
@@ -450,12 +335,6 @@ input StudioFilterType {
child_count: IntCriterionInput
"Filter by autotag ignore value"
ignore_auto_tag: Boolean
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
@@ -489,8 +368,6 @@ input GalleryFilterType {
average_resolution: ResolutionCriterionInput
"Filter to only include galleries that have chapters. `true` or `false`"
has_chapters: String
"Filter to only include galleries with these scenes"
scenes: MultiCriterionInput
"Filter to only include galleries with this studio"
studios: HierarchicalMultiCriterionInput
"Filter to only include galleries with these tags"
@@ -521,17 +398,6 @@ input GalleryFilterType {
code: StringCriterionInput
"Filter by photographer"
photographer: StringCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related performers that meet this criteria"
performers_filter: PerformerFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
}
input TagFilterType {
@@ -542,15 +408,9 @@ input TagFilterType {
"Filter by tag name"
name: StringCriterionInput
"Filter by tag sort_name"
sort_name: StringCriterionInput
"Filter by tag aliases"
aliases: StringCriterionInput
"Filter by favorite"
favorite: Boolean
"Filter by tag description"
description: StringCriterionInput
@@ -569,15 +429,6 @@ input TagFilterType {
"Filter by number of performers with this tag"
performer_count: IntCriterionInput
"Filter by number of studios with this tag"
studio_count: IntCriterionInput
"Filter by number of movies with this tag"
movie_count: IntCriterionInput
"Filter by number of group with this tag"
group_count: IntCriterionInput
"Filter by number of markers with this tag"
marker_count: IntCriterionInput
@@ -596,13 +447,6 @@ input TagFilterType {
"Filter by autotag ignore value"
ignore_auto_tag: Boolean
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by creation time"
created_at: TimestampCriterionInput
@@ -668,15 +512,6 @@ input ImageFilterType {
code: StringCriterionInput
"Filter by photographer"
photographer: StringCriterionInput
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by related performers that meet this criteria"
performers_filter: PerformerFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
}
enum CriterionModifier {
@@ -772,7 +607,6 @@ enum FilterMode {
GALLERIES
SCENE_MARKERS
MOVIES
GROUPS
TAGS
IMAGES
}

View File

@@ -1,8 +1,3 @@
type GalleryPathsType {
cover: String!
preview: String! # Resolver
}
"Gallery type"
type Gallery {
id: ID!
@@ -30,9 +25,6 @@ type Gallery {
performers: [Performer!]!
cover: Image
paths: GalleryPathsType! # Resolver
image(index: Int!): Image!
}
input GalleryCreateInput {
@@ -116,12 +108,3 @@ input GalleryRemoveInput {
gallery_id: ID!
image_ids: [ID!]!
}
input GallerySetCoverInput {
gallery_id: ID!
cover_image_id: ID!
}
input GalleryResetCoverInput {
gallery_id: ID!
}

View File

@@ -1,137 +0,0 @@
"GroupDescription represents a relationship to a group with a description of the relationship"
type GroupDescription {
group: Group!
description: String
}
type Group {
id: ID!
name: String!
aliases: String
"Duration in seconds"
duration: Int
date: String
# rating expressed as 1-100
rating100: Int
studio: Studio
director: String
synopsis: String
urls: [String!]!
tags: [Tag!]!
created_at: Time!
updated_at: Time!
containing_groups: [GroupDescription!]!
sub_groups: [GroupDescription!]!
front_image_path: String # Resolver
back_image_path: String # Resolver
scene_count(depth: Int): Int! # Resolver
sub_group_count(depth: Int): Int! # Resolver
scenes: [Scene!]!
}
input GroupDescriptionInput {
group_id: ID!
description: String
}
input GroupCreateInput {
name: String!
aliases: String
"Duration in seconds"
duration: Int
date: String
# rating expressed as 1-100
rating100: Int
studio_id: ID
director: String
synopsis: String
urls: [String!]
tag_ids: [ID!]
containing_groups: [GroupDescriptionInput!]
sub_groups: [GroupDescriptionInput!]
"This should be a URL or a base64 encoded data URL"
front_image: String
"This should be a URL or a base64 encoded data URL"
back_image: String
}
input GroupUpdateInput {
id: ID!
name: String
aliases: String
duration: Int
date: String
# rating expressed as 1-100
rating100: Int
studio_id: ID
director: String
synopsis: String
urls: [String!]
tag_ids: [ID!]
containing_groups: [GroupDescriptionInput!]
sub_groups: [GroupDescriptionInput!]
"This should be a URL or a base64 encoded data URL"
front_image: String
"This should be a URL or a base64 encoded data URL"
back_image: String
}
input BulkUpdateGroupDescriptionsInput {
groups: [GroupDescriptionInput!]!
mode: BulkUpdateIdMode!
}
input BulkGroupUpdateInput {
clientMutationId: String
ids: [ID!]
# rating expressed as 1-100
rating100: Int
studio_id: ID
director: String
urls: BulkUpdateStrings
tag_ids: BulkUpdateIds
containing_groups: BulkUpdateGroupDescriptionsInput
sub_groups: BulkUpdateGroupDescriptionsInput
}
input GroupDestroyInput {
id: ID!
}
input ReorderSubGroupsInput {
"ID of the group to reorder sub groups for"
group_id: ID!
"""
IDs of the sub groups to reorder. These must be a subset of the current sub groups.
Sub groups will be inserted in this order at the insert_index
"""
sub_group_ids: [ID!]!
"The sub-group ID at which to insert the sub groups"
insert_at_id: ID!
"If true, the sub groups will be inserted after the insert_index, otherwise they will be inserted before"
insert_after: Boolean
}
type FindGroupsResultType {
count: Int!
groups: [Group!]!
}
input GroupSubGroupAddInput {
containing_group_id: ID!
sub_groups: [GroupDescriptionInput!]!
"The index at which to insert the sub groups. If not provided, the sub groups will be appended to the end"
insert_index: Int
}
input GroupSubGroupRemoveInput {
containing_group_id: ID!
sub_group_ids: [ID!]!
}

View File

@@ -4,7 +4,6 @@ enum JobStatus {
FINISHED
STOPPING
CANCELLED
FAILED
}
type Job {
@@ -16,7 +15,6 @@ type Job {
startTime: Time
endTime: Time
addTime: Time!
error: String
}
input FindJobInput {

View File

@@ -75,8 +75,6 @@ input ScanMetaDataFilterInput {
input ScanMetadataInput {
paths: [String!]
"Forces a rescan on files even if modification time is unchanged"
rescan: Boolean
"Generate covers during scan"
scanGenerateCovers: Boolean
"Generate previews during scan"
@@ -97,8 +95,6 @@ input ScanMetadataInput {
}
type ScanMetadataOptions {
"Forces a rescan on files even if modification time is unchanged"
rescan: Boolean!
"Generate covers during scan"
scanGenerateCovers: Boolean!
"Generate previews during scan"
@@ -284,8 +280,7 @@ input ExportObjectsInput {
studios: ExportObjectTypeInput
performers: ExportObjectTypeInput
tags: ExportObjectTypeInput
groups: ExportObjectTypeInput
movies: ExportObjectTypeInput @deprecated(reason: "Use groups instead")
movies: ExportObjectTypeInput
galleries: ExportObjectTypeInput
includeDependencies: Boolean
}
@@ -331,17 +326,8 @@ type SystemStatus {
os: String!
workingDir: String!
homeDir: String!
ffmpegPath: String
ffprobePath: String
}
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
}

View File

@@ -10,15 +10,13 @@ type Movie {
studio: Studio
director: String
synopsis: String
url: String @deprecated(reason: "Use urls")
urls: [String!]!
tags: [Tag!]!
url: String
created_at: Time!
updated_at: Time!
front_image_path: String # Resolver
back_image_path: String # Resolver
scene_count(depth: Int): Int! # Resolver
scene_count: Int! # Resolver
scenes: [Scene!]!
}
@@ -33,9 +31,7 @@ input MovieCreateInput {
studio_id: ID
director: String
synopsis: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
tag_ids: [ID!]
url: String
"This should be a URL or a base64 encoded data URL"
front_image: String
"This should be a URL or a base64 encoded data URL"
@@ -53,9 +49,7 @@ input MovieUpdateInput {
studio_id: ID
director: String
synopsis: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
tag_ids: [ID!]
url: String
"This should be a URL or a base64 encoded data URL"
front_image: String
"This should be a URL or a base64 encoded data URL"
@@ -69,8 +63,6 @@ input BulkMovieUpdateInput {
rating100: Int
studio_id: ID
director: String
urls: BulkUpdateStrings
tag_ids: BulkUpdateIds
}
input MovieDestroyInput {

View File

@@ -16,11 +16,10 @@ type Performer {
id: ID!
name: String!
disambiguation: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
url: String
gender: GenderEnum
twitter: String @deprecated(reason: "Use urls")
instagram: String @deprecated(reason: "Use urls")
twitter: String
instagram: String
birthdate: String
ethnicity: String
country: String
@@ -42,8 +41,7 @@ type Performer {
scene_count: Int! # Resolver
image_count: Int! # Resolver
gallery_count: Int! # Resolver
group_count: Int! # Resolver
movie_count: Int! @deprecated(reason: "use group_count instead") # Resolver
movie_count: Int! # Resolver
performer_count: Int! # Resolver
o_counter: Int # Resolver
scenes: [Scene!]!
@@ -56,17 +54,13 @@ type Performer {
weight: Int
created_at: Time!
updated_at: Time!
groups: [Group!]!
movies: [Movie!]! @deprecated(reason: "use groups instead")
custom_fields: Map!
movies: [Movie!]!
}
input PerformerCreateInput {
name: String!
disambiguation: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
url: String
gender: GenderEnum
birthdate: String
ethnicity: String
@@ -81,8 +75,8 @@ input PerformerCreateInput {
tattoos: String
piercings: String
alias_list: [String!]
twitter: String @deprecated(reason: "Use urls")
instagram: String @deprecated(reason: "Use urls")
twitter: String
instagram: String
favorite: Boolean
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
@@ -95,16 +89,13 @@ input PerformerCreateInput {
hair_color: String
weight: Int
ignore_auto_tag: Boolean
custom_fields: Map
}
input PerformerUpdateInput {
id: ID!
name: String
disambiguation: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
url: String
gender: GenderEnum
birthdate: String
ethnicity: String
@@ -119,8 +110,8 @@ input PerformerUpdateInput {
tattoos: String
piercings: String
alias_list: [String!]
twitter: String @deprecated(reason: "Use urls")
instagram: String @deprecated(reason: "Use urls")
twitter: String
instagram: String
favorite: Boolean
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
@@ -133,8 +124,6 @@ input PerformerUpdateInput {
hair_color: String
weight: Int
ignore_auto_tag: Boolean
custom_fields: CustomFieldsInput
}
input BulkUpdateStrings {
@@ -146,8 +135,7 @@ input BulkPerformerUpdateInput {
clientMutationId: String
ids: [ID!]
disambiguation: String
url: String @deprecated(reason: "Use urls")
urls: BulkUpdateStrings
url: String
gender: GenderEnum
birthdate: String
ethnicity: String
@@ -162,8 +150,8 @@ input BulkPerformerUpdateInput {
tattoos: String
piercings: String
alias_list: BulkUpdateStrings
twitter: String @deprecated(reason: "Use urls")
instagram: String @deprecated(reason: "Use urls")
twitter: String
instagram: String
favorite: Boolean
tag_ids: BulkUpdateIds
# rating expressed as 1-100
@@ -173,8 +161,6 @@ input BulkPerformerUpdateInput {
hair_color: String
weight: Int
ignore_auto_tag: Boolean
custom_fields: CustomFieldsInput
}
input PerformerDestroyInput {

View File

@@ -2,10 +2,7 @@ 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!
@@ -21,10 +18,7 @@ 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!]
@@ -33,10 +27,7 @@ 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!]

View File

@@ -26,11 +26,6 @@ type SceneMovie {
scene_index: Int
}
type SceneGroup {
group: Group!
scene_index: Int
}
type VideoCaption {
language_code: String!
caption_type: String!
@@ -73,8 +68,7 @@ type Scene {
scene_markers: [SceneMarker!]!
galleries: [Gallery!]!
studio: Studio
groups: [SceneGroup!]!
movies: [SceneMovie!]! @deprecated(reason: "Use groups")
movies: [SceneMovie!]!
tags: [Tag!]!
performers: [Performer!]!
stash_ids: [StashID!]!
@@ -88,11 +82,6 @@ input SceneMovieInput {
scene_index: Int
}
input SceneGroupInput {
group_id: ID!
scene_index: Int
}
input SceneCreateInput {
title: String
code: String
@@ -107,8 +96,7 @@ input SceneCreateInput {
studio_id: ID
gallery_ids: [ID!]
performer_ids: [ID!]
groups: [SceneGroupInput!]
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
movies: [SceneMovieInput!]
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
cover_image: String
@@ -140,8 +128,7 @@ input SceneUpdateInput {
studio_id: ID
gallery_ids: [ID!]
performer_ids: [ID!]
groups: [SceneGroupInput!]
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
movies: [SceneMovieInput!]
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
cover_image: String
@@ -188,8 +175,7 @@ input BulkSceneUpdateInput {
gallery_ids: BulkUpdateIds
performer_ids: BulkUpdateIds
tag_ids: BulkUpdateIds
group_ids: BulkUpdateIds
movie_ids: BulkUpdateIds @deprecated(reason: "Use group_ids")
movie_ids: BulkUpdateIds
}
input SceneDestroyInput {

View File

@@ -1,65 +0,0 @@
"A movie from a scraping operation..."
type ScrapedMovie {
stored_id: ID
name: String
aliases: String
duration: String
date: String
rating: String
director: String
url: String @deprecated(reason: "use urls")
urls: [String!]
synopsis: String
studio: ScrapedStudio
tags: [ScrapedTag!]
"This should be a base64 encoded data URL"
front_image: String
"This should be a base64 encoded data URL"
back_image: String
}
input ScrapedMovieInput {
name: String
aliases: String
duration: String
date: String
rating: String
director: String
url: String @deprecated(reason: "use urls")
urls: [String!]
synopsis: String
# not including tags for the input
}
"A group from a scraping operation..."
type ScrapedGroup {
stored_id: ID
name: String
aliases: String
duration: String
date: String
rating: String
director: String
urls: [String!]
synopsis: String
studio: ScrapedStudio
tags: [ScrapedTag!]
"This should be a base64 encoded data URL"
front_image: String
"This should be a base64 encoded data URL"
back_image: String
}
input ScrapedGroupInput {
name: String
aliases: String
duration: String
date: String
rating: String
director: String
urls: [String!]
synopsis: String
# not including tags for the input
}

View File

@@ -0,0 +1,29 @@
"A movie from a scraping operation..."
type ScrapedMovie {
stored_id: ID
name: String
aliases: String
duration: String
date: String
rating: String
director: String
url: String
synopsis: String
studio: ScrapedStudio
"This should be a base64 encoded data URL"
front_image: String
"This should be a base64 encoded data URL"
back_image: String
}
input ScrapedMovieInput {
name: String
aliases: String
duration: String
date: String
rating: String
director: String
url: String
synopsis: String
}

View File

@@ -5,10 +5,9 @@ type ScrapedPerformer {
name: String
disambiguation: String
gender: String
url: String @deprecated(reason: "use urls")
urls: [String!]
twitter: String @deprecated(reason: "use urls")
instagram: String @deprecated(reason: "use urls")
url: String
twitter: String
instagram: String
birthdate: String
ethnicity: String
country: String
@@ -41,10 +40,9 @@ input ScrapedPerformerInput {
name: String
disambiguation: String
gender: String
url: String @deprecated(reason: "use urls")
urls: [String!]
twitter: String @deprecated(reason: "use urls")
instagram: String @deprecated(reason: "use urls")
url: String
twitter: String
instagram: String
birthdate: String
ethnicity: String
country: String

View File

@@ -10,9 +10,7 @@ enum ScrapeType {
"Type of the content a scraper generates"
enum ScrapeContentType {
GALLERY
IMAGE
MOVIE
GROUP
PERFORMER
SCENE
}
@@ -23,9 +21,7 @@ union ScrapedContent =
| ScrapedTag
| ScrapedScene
| ScrapedGallery
| ScrapedImage
| ScrapedMovie
| ScrapedGroup
| ScrapedPerformer
type ScraperSpec {
@@ -43,12 +39,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"
group: ScraperSpec
movie: ScraperSpec
}
type ScrapedStudio {
@@ -84,8 +76,7 @@ type ScrapedScene {
studio: ScrapedStudio
tags: [ScrapedTag!]
performers: [ScrapedPerformer!]
movies: [ScrapedMovie!] @deprecated(reason: "use groups")
groups: [ScrapedGroup!]
movies: [ScrapedMovie!]
remote_site_id: String
duration: Int
@@ -132,32 +123,12 @@ 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")
"Stash-box endpoint"
stash_box_endpoint: String
"Scraper ID to scrape with. Should be unset if stash_box_endpoint/stash_box_index is set"
"Scraper ID to scrape with. Should be unset if stash_box_index is set"
scraper_id: ID
}
@@ -166,7 +137,7 @@ type ScraperSource {
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
"Stash-box endpoint"
stash_box_endpoint: String
"Scraper ID to scrape with. Should be unset if stash_box_endpoint/stash_box_index is set"
"Scraper ID to scrape with. Should be unset if stash_box_index is set"
scraper_id: ID
}
@@ -214,38 +185,18 @@ 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
"Instructs to query by movie id"
movie_id: ID
"Instructs to query by movie fragment"
"Instructs to query by gallery fragment"
movie_input: ScrapedMovieInput
}
input ScrapeSingleGroupInput {
"Instructs to query by string"
query: String
"Instructs to query by group id"
group_id: ID
"Instructs to query by group fragment"
group_input: ScrapedGroupInput
}
input StashBoxSceneQueryInput {
"Index of the configured stash-box instance to use"
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
"Endpoint of the stash-box instance to use"
stash_box_endpoint: String
stash_box_index: Int!
"Instructs query by scene fingerprints"
scene_ids: [ID!]
"Query by query string"
@@ -254,9 +205,7 @@ input StashBoxSceneQueryInput {
input StashBoxPerformerQueryInput {
"Index of the configured stash-box instance to use"
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
"Endpoint of the stash-box instance to use"
stash_box_endpoint: String
stash_box_index: Int!
"Instructs query by scene fingerprints"
performer_ids: [ID!]
"Query by query string"
@@ -277,9 +226,7 @@ type StashBoxFingerprint {
"If neither ids nor names are set, tag all items"
input StashBoxBatchTagInput {
"Stash endpoint to use for the tagging"
endpoint: Int @deprecated(reason: "use stash_box_endpoint")
"Endpoint of the stash-box instance to use"
stash_box_endpoint: String
endpoint: Int!
"Fields to exclude when executing the tagging"
exclude_fields: [String!]
"Refresh items already tagged by StashBox if true. Only tag items with no StashBox tagging if false"

View File

@@ -2,37 +2,30 @@ 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 {
scene_ids: [String!]!
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
stash_box_endpoint: String
stash_box_index: Int!
}
input StashBoxDraftSubmissionInput {
id: String!
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
stash_box_endpoint: String
stash_box_index: Int!
}

View File

@@ -7,8 +7,7 @@ type StatsResultType {
gallery_count: Int!
performer_count: Int!
studio_count: Int!
group_count: Int!
movie_count: Int! @deprecated(reason: "use group_count instead")
movie_count: Int!
tag_count: Int!
total_o_count: Int!
total_play_duration: Float!

View File

@@ -5,7 +5,6 @@ type Studio {
parent_studio: Studio
child_studios: [Studio!]!
aliases: [String!]!
tags: [Tag!]!
ignore_auto_tag: Boolean!
image_path: String # Resolver
@@ -13,17 +12,14 @@ type Studio {
image_count(depth: Int): Int! # Resolver
gallery_count(depth: Int): Int! # Resolver
performer_count(depth: Int): Int! # Resolver
group_count(depth: Int): Int! # Resolver
movie_count(depth: Int): Int! @deprecated(reason: "use group_count instead") # Resolver
movie_count(depth: Int): Int! # Resolver
stash_ids: [StashID!]!
# rating expressed as 1-100
rating100: Int
favorite: Boolean!
details: String
created_at: Time!
updated_at: Time!
groups: [Group!]!
movies: [Movie!]! @deprecated(reason: "use groups instead")
movies: [Movie!]!
}
input StudioCreateInput {
@@ -35,10 +31,8 @@ input StudioCreateInput {
stash_ids: [StashIDInput!]
# rating expressed as 1-100
rating100: Int
favorite: Boolean
details: String
aliases: [String!]
tag_ids: [ID!]
ignore_auto_tag: Boolean
}
@@ -52,10 +46,8 @@ input StudioUpdateInput {
stash_ids: [StashIDInput!]
# rating expressed as 1-100
rating100: Int
favorite: Boolean
details: String
aliases: [String!]
tag_ids: [ID!]
ignore_auto_tag: Boolean
}

View File

@@ -1,23 +1,18 @@
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!
created_at: Time!
updated_at: Time!
favorite: Boolean!
image_path: String # Resolver
scene_count(depth: Int): Int! # Resolver
scene_marker_count(depth: Int): Int! # Resolver
image_count(depth: Int): Int! # Resolver
gallery_count(depth: Int): Int! # Resolver
performer_count(depth: Int): Int! # Resolver
studio_count(depth: Int): Int! # Resolver
group_count(depth: Int): Int! # Resolver
movie_count(depth: Int): Int! @deprecated(reason: "use group_count instead") # Resolver
parents: [Tag!]!
children: [Tag!]!
@@ -27,12 +22,10 @@ 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
favorite: Boolean
"This should be a URL or a base64 encoded data URL"
image: String
@@ -43,12 +36,10 @@ 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
favorite: Boolean
"This should be a URL or a base64 encoded data URL"
image: String
@@ -69,14 +60,3 @@ input TagsMergeInput {
source: [ID!]!
destination: ID!
}
input BulkTagUpdateInput {
ids: [ID!]
description: String
aliases: BulkUpdateStrings
ignore_auto_tag: Boolean
favorite: Boolean
parent_ids: BulkUpdateIds
child_ids: BulkUpdateIds
}

View File

@@ -30,6 +30,11 @@ fragment TagFragment on Tag {
id
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
@@ -49,16 +54,15 @@ fragment PerformerFragment on Performer {
aliases
gender
merged_ids
deleted
merged_into_id
urls {
...URLFragment
}
images {
...ImageFragment
}
birth_date
death_date
birthdate {
...FuzzyDateFragment
}
ethnicity
country
eye_color

View File

@@ -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/faq/setup/#protecting-against-accidental-exposure-to-the-internet"
"More information and fixes are available at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-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/faq/setup/#protecting-against-accidental-exposure-to-the-internet"
"Please read the log entry or visit https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
)
func allowUnauthenticated(r *http.Request) bool {
@@ -42,7 +42,7 @@ func authenticateHandler() func(http.Handler) http.Handler {
userID, err := manager.GetInstance().SessionStore.Authenticate(w, r)
if err != nil {
if !errors.Is(err, session.ErrUnauthorized) {
if errors.Is(err, session.ErrUnauthorized) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

@@ -335,86 +335,43 @@ func (t changesetTranslator) updateStringsBulk(value *BulkUpdateStrings, field s
}
}
func (t changesetTranslator) updateStashIDs(value models.StashIDInputs, field string) *models.UpdateStashIDs {
func (t changesetTranslator) updateStashIDs(value []models.StashID, field string) *models.UpdateStashIDs {
if !t.hasField(field) {
return nil
}
return &models.UpdateStashIDs{
StashIDs: value.ToStashIDs(),
StashIDs: value,
Mode: models.RelationshipUpdateModeSet,
}
}
func (t changesetTranslator) relatedGroupsFromMovies(value []models.SceneMovieInput) (models.RelatedGroups, error) {
groupsScenes, err := models.GroupsScenesFromInput(value)
func (t changesetTranslator) relatedMovies(value []models.SceneMovieInput) (models.RelatedMovies, error) {
moviesScenes, err := models.MoviesScenesFromInput(value)
if err != nil {
return models.RelatedGroups{}, err
return models.RelatedMovies{}, err
}
return models.NewRelatedGroups(groupsScenes), nil
return models.NewRelatedMovies(moviesScenes), nil
}
func groupsScenesFromGroupInput(input []models.SceneGroupInput) ([]models.GroupsScenes, error) {
ret := make([]models.GroupsScenes, len(input))
for i, v := range input {
mID, err := strconv.Atoi(v.GroupID)
if err != nil {
return nil, fmt.Errorf("invalid group ID: %s", v.GroupID)
}
ret[i] = models.GroupsScenes{
GroupID: mID,
SceneIndex: v.SceneIndex,
}
}
return ret, nil
}
func (t changesetTranslator) relatedGroups(value []models.SceneGroupInput) (models.RelatedGroups, error) {
groupsScenes, err := groupsScenesFromGroupInput(value)
if err != nil {
return models.RelatedGroups{}, err
}
return models.NewRelatedGroups(groupsScenes), nil
}
func (t changesetTranslator) updateGroupIDsFromMovies(value []models.SceneMovieInput, field string) (*models.UpdateGroupIDs, error) {
func (t changesetTranslator) updateMovieIDs(value []models.SceneMovieInput, field string) (*models.UpdateMovieIDs, error) {
if !t.hasField(field) {
return nil, nil
}
groupsScenes, err := models.GroupsScenesFromInput(value)
moviesScenes, err := models.MoviesScenesFromInput(value)
if err != nil {
return nil, err
}
return &models.UpdateGroupIDs{
Groups: groupsScenes,
return &models.UpdateMovieIDs{
Movies: moviesScenes,
Mode: models.RelationshipUpdateModeSet,
}, nil
}
func (t changesetTranslator) updateGroupIDs(value []models.SceneGroupInput, field string) (*models.UpdateGroupIDs, error) {
if !t.hasField(field) {
return nil, nil
}
groupsScenes, err := groupsScenesFromGroupInput(value)
if err != nil {
return nil, err
}
return &models.UpdateGroupIDs{
Groups: groupsScenes,
Mode: models.RelationshipUpdateModeSet,
}, nil
}
func (t changesetTranslator) updateGroupIDsBulk(value *BulkUpdateIds, field string) (*models.UpdateGroupIDs, error) {
func (t changesetTranslator) updateMovieIDsBulk(value *BulkUpdateIds, field string) (*models.UpdateMovieIDs, error) {
if !t.hasField(field) || value == nil {
return nil, nil
}
@@ -424,74 +381,13 @@ func (t changesetTranslator) updateGroupIDsBulk(value *BulkUpdateIds, field stri
return nil, fmt.Errorf("converting ids [%v]: %w", value.Ids, err)
}
groups := make([]models.GroupsScenes, len(ids))
movies := make([]models.MoviesScenes, len(ids))
for i, id := range ids {
groups[i] = models.GroupsScenes{GroupID: id}
movies[i] = models.MoviesScenes{MovieID: id}
}
return &models.UpdateGroupIDs{
Groups: groups,
Mode: value.Mode,
}, nil
}
func groupsDescriptionsFromGroupInput(input []*GroupDescriptionInput) ([]models.GroupIDDescription, error) {
ret := make([]models.GroupIDDescription, len(input))
for i, v := range input {
gID, err := strconv.Atoi(v.GroupID)
if err != nil {
return nil, fmt.Errorf("invalid group ID: %s", v.GroupID)
}
ret[i] = models.GroupIDDescription{
GroupID: gID,
}
if v.Description != nil {
ret[i].Description = *v.Description
}
}
return ret, nil
}
func (t changesetTranslator) groupIDDescriptions(value []*GroupDescriptionInput) (models.RelatedGroupDescriptions, error) {
groupsScenes, err := groupsDescriptionsFromGroupInput(value)
if err != nil {
return models.RelatedGroupDescriptions{}, err
}
return models.NewRelatedGroupDescriptions(groupsScenes), nil
}
func (t changesetTranslator) updateGroupIDDescriptions(value []*GroupDescriptionInput, field string) (*models.UpdateGroupDescriptions, error) {
if !t.hasField(field) {
return nil, nil
}
groupsScenes, err := groupsDescriptionsFromGroupInput(value)
if err != nil {
return nil, err
}
return &models.UpdateGroupDescriptions{
Groups: groupsScenes,
Mode: models.RelationshipUpdateModeSet,
}, nil
}
func (t changesetTranslator) updateGroupIDDescriptionsBulk(value *BulkUpdateGroupDescriptionsInput, field string) (*models.UpdateGroupDescriptions, error) {
if !t.hasField(field) || value == nil {
return nil, nil
}
groups, err := groupsDescriptionsFromGroupInput(value.Groups)
if err != nil {
return nil, err
}
return &models.UpdateGroupDescriptions{
Groups: groups,
return &models.UpdateMovieIDs{
Movies: movies,
Mode: value.Mode,
}, nil
}

View File

@@ -5,11 +5,11 @@ package api
type key int
const (
galleryKey key = 0
performerKey
// galleryKey key = 0
performerKey key = iota + 1
sceneKey
studioKey
groupKey
movieKey
tagKey
downloadKey
imageKey

View File

@@ -1,2 +0,0 @@
// Package api provides the HTTP and Graphql API for the application.
package api

View File

@@ -1,65 +0,0 @@
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
}

View File

@@ -1,60 +0,0 @@
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)
})
}
}

View File

@@ -1,221 +0,0 @@
// 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)
}

View File

@@ -1,19 +1,14 @@
// Package loaders contains the dataloaders used by the resolver in [api].
// They are generated with `make generate-dataloaders`.
// The dataloaders are used to batch requests to the database.
//go:generate go run github.com/vektah/dataloaden SceneLoader int *github.com/stashapp/stash/pkg/models.Scene
//go:generate go run github.com/vektah/dataloaden GalleryLoader int *github.com/stashapp/stash/pkg/models.Gallery
//go:generate go run github.com/vektah/dataloaden ImageLoader int *github.com/stashapp/stash/pkg/models.Image
//go:generate go run github.com/vektah/dataloaden PerformerLoader int *github.com/stashapp/stash/pkg/models.Performer
//go:generate go run github.com/vektah/dataloaden StudioLoader int *github.com/stashapp/stash/pkg/models.Studio
//go:generate go run github.com/vektah/dataloaden TagLoader int *github.com/stashapp/stash/pkg/models.Tag
//go:generate go run github.com/vektah/dataloaden GroupLoader int *github.com/stashapp/stash/pkg/models.Group
//go:generate go run github.com/vektah/dataloaden MovieLoader int *github.com/stashapp/stash/pkg/models.Movie
//go:generate go run github.com/vektah/dataloaden FileLoader github.com/stashapp/stash/pkg/models.FileID github.com/stashapp/stash/pkg/models.File
//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
@@ -52,16 +47,13 @@ type Loaders struct {
ImageFiles *ImageFileIDsLoader
GalleryFiles *GalleryFileIDsLoader
GalleryByID *GalleryLoader
ImageByID *ImageLoader
PerformerByID *PerformerLoader
PerformerCustomFields *CustomFieldsLoader
StudioByID *StudioLoader
TagByID *TagLoader
GroupByID *GroupLoader
FileByID *FileLoader
GalleryByID *GalleryLoader
ImageByID *ImageLoader
PerformerByID *PerformerLoader
StudioByID *StudioLoader
TagByID *TagLoader
MovieByID *MovieLoader
FileByID *FileLoader
}
type Middleware struct {
@@ -92,11 +84,6 @@ 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,
@@ -107,10 +94,10 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
maxBatch: maxBatch,
fetch: m.fetchTags(ctx),
},
GroupByID: &GroupLoader{
MovieByID: &MovieLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchGroups(ctx),
fetch: m.fetchMovies(ctx),
},
FileByID: &FileLoader{
wait: wait,
@@ -223,18 +210,6 @@ 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 {
@@ -257,11 +232,11 @@ func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.T
}
}
func (m Middleware) fetchGroups(ctx context.Context) func(keys []int) ([]*models.Group, []error) {
return func(keys []int) (ret []*models.Group, errs []error) {
func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models.Movie, []error) {
return func(keys []int) (ret []*models.Movie, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Group.FindMany(ctx, keys)
ret, err = m.Repository.Movie.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)

View File

@@ -9,10 +9,10 @@ import (
"github.com/stashapp/stash/pkg/models"
)
// GroupLoaderConfig captures the config to create a new GroupLoader
type GroupLoaderConfig struct {
// MovieLoaderConfig captures the config to create a new MovieLoader
type MovieLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Group, []error)
Fetch func(keys []int) ([]*models.Movie, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
@@ -21,19 +21,19 @@ type GroupLoaderConfig struct {
MaxBatch int
}
// NewGroupLoader creates a new GroupLoader given a fetch, wait, and maxBatch
func NewGroupLoader(config GroupLoaderConfig) *GroupLoader {
return &GroupLoader{
// NewMovieLoader creates a new MovieLoader given a fetch, wait, and maxBatch
func NewMovieLoader(config MovieLoaderConfig) *MovieLoader {
return &MovieLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// GroupLoader batches and caches requests
type GroupLoader struct {
// MovieLoader batches and caches requests
type MovieLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Group, []error)
fetch func(keys []int) ([]*models.Movie, []error)
// how long to done before sending a batch
wait time.Duration
@@ -44,51 +44,51 @@ type GroupLoader struct {
// INTERNAL
// lazily created cache
cache map[int]*models.Group
cache map[int]*models.Movie
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *groupLoaderBatch
batch *movieLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type groupLoaderBatch struct {
type movieLoaderBatch struct {
keys []int
data []*models.Group
data []*models.Movie
error []error
closing bool
done chan struct{}
}
// Load a Group by key, batching and caching will be applied automatically
func (l *GroupLoader) Load(key int) (*models.Group, error) {
// Load a Movie by key, batching and caching will be applied automatically
func (l *MovieLoader) Load(key int) (*models.Movie, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Group.
// LoadThunk returns a function that when called will block waiting for a Movie.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *GroupLoader) LoadThunk(key int) func() (*models.Group, error) {
func (l *MovieLoader) LoadThunk(key int) func() (*models.Movie, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Group, error) {
return func() (*models.Movie, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &groupLoaderBatch{done: make(chan struct{})}
l.batch = &movieLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Group, error) {
return func() (*models.Movie, error) {
<-batch.done
var data *models.Group
var data *models.Movie
if pos < len(batch.data) {
data = batch.data[pos]
}
@@ -113,43 +113,43 @@ func (l *GroupLoader) LoadThunk(key int) func() (*models.Group, error) {
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *GroupLoader) LoadAll(keys []int) ([]*models.Group, []error) {
results := make([]func() (*models.Group, error), len(keys))
func (l *MovieLoader) LoadAll(keys []int) ([]*models.Movie, []error) {
results := make([]func() (*models.Movie, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
groups := make([]*models.Group, len(keys))
movies := make([]*models.Movie, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
groups[i], errors[i] = thunk()
movies[i], errors[i] = thunk()
}
return groups, errors
return movies, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Groups.
// LoadAllThunk returns a function that when called will block waiting for a Movies.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *GroupLoader) LoadAllThunk(keys []int) func() ([]*models.Group, []error) {
results := make([]func() (*models.Group, error), len(keys))
func (l *MovieLoader) LoadAllThunk(keys []int) func() ([]*models.Movie, []error) {
results := make([]func() (*models.Movie, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Group, []error) {
groups := make([]*models.Group, len(keys))
return func() ([]*models.Movie, []error) {
movies := make([]*models.Movie, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
groups[i], errors[i] = thunk()
movies[i], errors[i] = thunk()
}
return groups, errors
return movies, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *GroupLoader) Prime(key int, value *models.Group) bool {
func (l *MovieLoader) Prime(key int, value *models.Movie) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
@@ -163,22 +163,22 @@ func (l *GroupLoader) Prime(key int, value *models.Group) bool {
}
// Clear the value at key from the cache, if it exists
func (l *GroupLoader) Clear(key int) {
func (l *MovieLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *GroupLoader) unsafeSet(key int, value *models.Group) {
func (l *MovieLoader) unsafeSet(key int, value *models.Movie) {
if l.cache == nil {
l.cache = map[int]*models.Group{}
l.cache = map[int]*models.Movie{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *groupLoaderBatch) keyIndex(l *GroupLoader, key int) int {
func (b *movieLoaderBatch) keyIndex(l *MovieLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
@@ -202,7 +202,7 @@ func (b *groupLoaderBatch) keyIndex(l *GroupLoader, key int) int {
return pos
}
func (b *groupLoaderBatch) startTimer(l *GroupLoader) {
func (b *movieLoaderBatch) startTimer(l *MovieLoader) {
time.Sleep(l.wait)
l.mu.Lock()
@@ -218,7 +218,7 @@ func (b *groupLoaderBatch) startTimer(l *GroupLoader) {
b.end(l)
}
func (b *groupLoaderBatch) end(l *GroupLoader) {
func (b *movieLoaderBatch) end(l *MovieLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -13,6 +13,7 @@ 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 (
@@ -36,7 +37,6 @@ type Resolver struct {
sceneService manager.SceneService
imageService manager.ImageService
galleryService manager.GalleryService
groupService manager.GroupService
hookExecutor hookExecutor
}
@@ -72,14 +72,9 @@ func (r *Resolver) SceneMarker() SceneMarkerResolver {
func (r *Resolver) Studio() StudioResolver {
return &studioResolver{r}
}
func (r *Resolver) Group() GroupResolver {
return &groupResolver{r}
}
func (r *Resolver) Movie() MovieResolver {
return &movieResolver{&groupResolver{r}}
return &movieResolver{r}
}
func (r *Resolver) Subscription() SubscriptionResolver {
return &subscriptionResolver{r}
}
@@ -116,11 +111,7 @@ type sceneResolver struct{ *Resolver }
type sceneMarkerResolver struct{ *Resolver }
type imageResolver struct{ *Resolver }
type studioResolver struct{ *Resolver }
// movie is group under the hood
type groupResolver struct{ *Resolver }
type movieResolver struct{ *groupResolver }
type movieResolver struct{ *Resolver }
type tagResolver struct{ *Resolver }
type galleryFileResolver struct{ *Resolver }
type videoFileResolver struct{ *Resolver }
@@ -137,6 +128,10 @@ 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)
@@ -178,7 +173,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
galleryQB := repo.Gallery
studioQB := repo.Studio
performerQB := repo.Performer
movieQB := repo.Group
movieQB := repo.Movie
tagQB := repo.Tag
// embrace the error
@@ -223,7 +218,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
return err
}
groupsCount, err := movieQB.Count(ctx)
moviesCount, err := movieQB.Count(ctx)
if err != nil {
return err
}
@@ -267,8 +262,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
GalleryCount: galleryCount,
PerformerCount: performersCount,
StudioCount: studiosCount,
GroupCount: groupsCount,
MovieCount: groupsCount,
MovieCount: moviesCount,
TagCount: tagsCount,
TotalOCount: totalOCount,
TotalPlayDuration: totalPlayDuration,

View File

@@ -2,10 +2,8 @@ package api
import (
"context"
"fmt"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/image"
@@ -191,28 +189,3 @@ func (r *galleryResolver) Urls(ctx context.Context, obj *models.Gallery) ([]stri
return obj.URLs.List(), nil
}
func (r *galleryResolver) Paths(ctx context.Context, obj *models.Gallery) (*GalleryPathsType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewGalleryURLBuilder(baseURL, obj)
return &GalleryPathsType{
Cover: builder.GetCoverURL(),
Preview: builder.GetPreviewURL(),
}, nil
}
func (r *galleryResolver) Image(ctx context.Context, obj *models.Gallery, index int) (ret *models.Image, err error) {
if index < 0 {
return nil, fmt.Errorf("index must >= 0")
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Image.FindByGalleryIDIndex(ctx, obj.ID, uint(index))
return err
}); err != nil {
return nil, err
}
return
}

View File

@@ -18,6 +18,11 @@ 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 {

View File

@@ -5,12 +5,10 @@ import (
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/group"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
)
func (r *groupResolver) Date(ctx context.Context, obj *models.Group) (*string, error) {
func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, error) {
if obj.Date != nil {
result := obj.Date.String()
return &result, nil
@@ -18,40 +16,11 @@ func (r *groupResolver) Date(ctx context.Context, obj *models.Group) (*string, e
return nil, nil
}
func (r *groupResolver) Rating100(ctx context.Context, obj *models.Group) (*int, error) {
func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) {
return obj.Rating, nil
}
func (r *groupResolver) URL(ctx context.Context, obj *models.Group) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Group)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
if len(urls) == 0 {
return nil, nil
}
return &urls[0], nil
}
func (r *groupResolver) Urls(ctx context.Context, obj *models.Group) ([]string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Group)
}); err != nil {
return nil, err
}
}
return obj.URLs.List(), nil
}
func (r *groupResolver) Studio(ctx context.Context, obj *models.Group) (ret *models.Studio, err error) {
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *models.Studio, err error) {
if obj.StudioID == nil {
return nil, nil
}
@@ -59,102 +28,26 @@ func (r *groupResolver) Studio(ctx context.Context, obj *models.Group) (ret *mod
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
}
func (r groupResolver) Tags(ctx context.Context, obj *models.Group) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Group)
}); err != nil {
return nil, err
}
}
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
return ret, firstError(errs)
}
func (r groupResolver) relatedGroups(ctx context.Context, rgd models.RelatedGroupDescriptions) (ret []*GroupDescription, err error) {
// rgd must be loaded
gds := rgd.List()
ids := make([]int, len(gds))
for i, gd := range gds {
ids[i] = gd.GroupID
}
groups, errs := loaders.From(ctx).GroupByID.LoadAll(ids)
err = firstError(errs)
if err != nil {
return
}
ret = make([]*GroupDescription, len(groups))
for i, group := range groups {
ret[i] = &GroupDescription{Group: group}
d := gds[i].Description
if d != "" {
ret[i].Description = &d
}
}
return ret, firstError(errs)
}
func (r groupResolver) ContainingGroups(ctx context.Context, obj *models.Group) (ret []*GroupDescription, err error) {
if !obj.ContainingGroups.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadContainingGroupIDs(ctx, r.repository.Group)
}); err != nil {
return nil, err
}
}
return r.relatedGroups(ctx, obj.ContainingGroups)
}
func (r groupResolver) SubGroups(ctx context.Context, obj *models.Group) (ret []*GroupDescription, err error) {
if !obj.SubGroups.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadSubGroupIDs(ctx, r.repository.Group)
}); err != nil {
return nil, err
}
}
return r.relatedGroups(ctx, obj.SubGroups)
}
func (r *groupResolver) SubGroupCount(ctx context.Context, obj *models.Group, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = group.CountByContainingGroupID(ctx, r.repository.Group, obj.ID, depth)
return err
}); err != nil {
return 0, err
}
return ret, nil
}
func (r *groupResolver) FrontImagePath(ctx context.Context, obj *models.Group) (*string, error) {
func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
hasImage, err = r.repository.Group.HasFrontImage(ctx, obj.ID)
hasImage, err = r.repository.Movie.HasFrontImage(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
imagePath := urlbuilders.NewGroupURLBuilder(baseURL, obj).GetGroupFrontImageURL(hasImage)
imagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieFrontImageURL(hasImage)
return &imagePath, nil
}
func (r *groupResolver) BackImagePath(ctx context.Context, obj *models.Group) (*string, error) {
func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
hasImage, err = r.repository.Group.HasBackImage(ctx, obj.ID)
hasImage, err = r.repository.Movie.HasBackImage(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -166,13 +59,13 @@ func (r *groupResolver) BackImagePath(ctx context.Context, obj *models.Group) (*
}
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
imagePath := urlbuilders.NewGroupURLBuilder(baseURL, obj).GetGroupBackImageURL()
imagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieBackImageURL()
return &imagePath, nil
}
func (r *groupResolver) SceneCount(ctx context.Context, obj *models.Group, depth *int) (ret int, err error) {
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = scene.CountByGroupID(ctx, r.repository.Scene, obj.ID, depth)
ret, err = r.repository.Scene.CountByMovieID(ctx, obj.ID)
return err
}); err != nil {
return 0, err
@@ -181,10 +74,10 @@ func (r *groupResolver) SceneCount(ctx context.Context, obj *models.Group, depth
return ret, nil
}
func (r *groupResolver) Scenes(ctx context.Context, obj *models.Group) (ret []*models.Scene, err error) {
func (r *movieResolver) Scenes(ctx context.Context, obj *models.Movie) (ret []*models.Scene, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Scene.FindByGroupID(ctx, obj.ID)
ret, err = r.repository.Scene.FindByMovieID(ctx, obj.ID)
return err
}); err != nil {
return nil, err

View File

@@ -24,79 +24,6 @@ func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer
return obj.Aliases.List(), nil
}
func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
if len(urls) == 0 {
return nil, nil
}
return &urls[0], nil
}
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
// find the first twitter url
for _, url := range urls {
if performer.IsTwitterURL(url) {
u := url
return &u, nil
}
}
return nil, nil
}
func (r *performerResolver) Instagram(ctx context.Context, obj *models.Performer) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
// find the first instagram url
for _, url := range urls {
if performer.IsInstagramURL(url) {
u := url
return &u, nil
}
}
return nil, nil
}
func (r *performerResolver) Urls(ctx context.Context, obj *models.Performer) ([]string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
return obj.URLs.List(), nil
}
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Height != nil {
ret := strconv.Itoa(*obj.Height)
@@ -179,9 +106,9 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
return ret, nil
}
func (r *performerResolver) GroupCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Group.CountByPerformerID(ctx, obj.ID)
ret, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return 0, err
@@ -190,11 +117,6 @@ func (r *performerResolver) GroupCount(ctx context.Context, obj *models.Performe
return ret, nil
}
// deprecated
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
return r.GroupCount(ctx, obj)
}
func (r *performerResolver) PerformerCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = performer.CountByAppearsWith(ctx, r.repository.Performer, obj.ID)
@@ -257,9 +179,9 @@ func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer
return nil, nil
}
func (r *performerResolver) Groups(ctx context.Context, obj *models.Performer) (ret []*models.Group, err error) {
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Group.FindByPerformerID(ctx, obj.ID)
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -267,21 +189,3 @@ 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)
}

View File

@@ -184,20 +184,20 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod
}
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) {
if !obj.Groups.Loaded() {
if !obj.Movies.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
return obj.LoadGroups(ctx, qb)
return obj.LoadMovies(ctx, qb)
}); err != nil {
return nil, err
}
}
loader := loaders.From(ctx).GroupByID
loader := loaders.From(ctx).MovieByID
for _, sm := range obj.Groups.List() {
movie, err := loader.Load(sm.GroupID)
for _, sm := range obj.Movies.List() {
movie, err := loader.Load(sm.MovieID)
if err != nil {
return nil, err
}
@@ -214,37 +214,6 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S
return ret, nil
}
func (r *sceneResolver) Groups(ctx context.Context, obj *models.Scene) (ret []*SceneGroup, err error) {
if !obj.Groups.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
return obj.LoadGroups(ctx, qb)
}); err != nil {
return nil, err
}
}
loader := loaders.From(ctx).GroupByID
for _, sm := range obj.Groups.List() {
group, err := loader.Load(sm.GroupID)
if err != nil {
return nil, err
}
sceneIdx := sm.SceneIndex
sceneGroup := &SceneGroup{
Group: group,
SceneIndex: sceneIdx,
}
ret = append(ret, sceneGroup)
}
return ret, nil
}
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {

View File

@@ -6,9 +6,9 @@ import (
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/group"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/movie"
"github.com/stashapp/stash/pkg/performer"
"github.com/stashapp/stash/pkg/scene"
)
@@ -40,20 +40,6 @@ func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) ([]str
return obj.Aliases.List(), nil
}
func (r *studioResolver) Tags(ctx context.Context, obj *models.Studio) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Studio)
}); err != nil {
return nil, err
}
}
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
return ret, firstError(errs)
}
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = scene.CountByStudioID(ctx, r.repository.Scene, obj.ID, depth)
@@ -98,9 +84,9 @@ func (r *studioResolver) PerformerCount(ctx context.Context, obj *models.Studio,
return ret, nil
}
func (r *studioResolver) GroupCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = group.CountByStudioID(ctx, r.repository.Group, obj.ID, depth)
ret, err = movie.CountByStudioID(ctx, r.repository.Movie, obj.ID, depth)
return err
}); err != nil {
return 0, err
@@ -109,11 +95,6 @@ func (r *studioResolver) GroupCount(ctx context.Context, obj *models.Studio, dep
return ret, nil
}
// deprecated
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
return r.GroupCount(ctx, obj, depth)
}
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
if obj.ParentID == nil {
return nil, nil
@@ -149,9 +130,9 @@ func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*in
return obj.Rating, nil
}
func (r *studioResolver) Groups(ctx context.Context, obj *models.Studio) (ret []*models.Group, err error) {
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Group.FindByStudioID(ctx, obj.ID)
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -159,8 +140,3 @@ func (r *studioResolver) Groups(ctx context.Context, obj *models.Studio) (ret []
return ret, nil
}
// deprecated
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Group, err error) {
return r.Groups(ctx, obj)
}

View File

@@ -3,55 +3,45 @@ package api
import (
"context"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/group"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/performer"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/studio"
)
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if !obj.ParentIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadParentIDs(ctx, r.repository.Tag)
}); err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByChildTagID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.ParentIDs.List())
return ret, firstError(errs)
return ret, nil
}
func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if !obj.ChildIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadChildIDs(ctx, r.repository.Tag)
}); err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByParentTagID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.ChildIDs.List())
return ret, firstError(errs)
return ret, nil
}
func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []string, err error) {
if !obj.Aliases.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadAliases(ctx, r.repository.Tag)
}); err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.GetAliases(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
return obj.Aliases.List(), nil
return ret, err
}
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
@@ -109,32 +99,6 @@ func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag, depth
return ret, nil
}
func (r *tagResolver) StudioCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = studio.CountByTagID(ctx, r.repository.Studio, obj.ID, depth)
return err
}); err != nil {
return 0, err
}
return ret, nil
}
func (r *tagResolver) GroupCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = group.CountByTagID(ctx, r.repository.Group, obj.ID, depth)
return err
}); err != nil {
return 0, err
}
return ret, nil
}
func (r *tagResolver) MovieCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
return r.GroupCount(ctx, obj, depth)
}
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {

View File

@@ -2,21 +2,16 @@ package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"regexp"
"strconv"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/internal/manager/task"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
var ErrOverriddenConfig = errors.New("cannot set overridden value")
@@ -26,60 +21,9 @@ func (r *mutationResolver) Setup(ctx context.Context, input manager.SetupInput)
return err == nil, err
}
func (r *mutationResolver) DownloadFFMpeg(ctx context.Context) (string, error) {
mgr := manager.GetInstance()
configDir := mgr.Config.GetConfigPathAbs()
// don't run if ffmpeg is already installed
ffmpegPath := ffmpeg.FindFFMpeg(configDir)
ffprobePath := ffmpeg.FindFFProbe(configDir)
if ffmpegPath != "" && ffprobePath != "" {
return "", fmt.Errorf("ffmpeg and ffprobe already installed at %s and %s", ffmpegPath, ffprobePath)
}
t := &task.DownloadFFmpegJob{
ConfigDirectory: configDir,
OnComplete: func(ctx context.Context) {
// clear the ffmpeg and ffprobe paths
logger.Infof("Clearing ffmpeg and ffprobe config paths so they are resolved from the config directory")
mgr.Config.SetString(config.FFMpegPath, "")
mgr.Config.SetString(config.FFProbePath, "")
mgr.RefreshFFMpeg(ctx)
mgr.RefreshStreamManager()
},
}
jobID := mgr.JobManager.Add(ctx, "Downloading ffmpeg...", t)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) setConfigString(key string, value *string) {
c := config.GetInstance()
if value != nil {
c.SetString(key, *value)
}
}
func (r *mutationResolver) setConfigBool(key string, value *bool) {
c := config.GetInstance()
if value != nil {
c.SetBool(key, *value)
}
}
func (r *mutationResolver) setConfigInt(key string, value *int) {
c := config.GetInstance()
if value != nil {
c.SetInt(key, *value)
}
}
func (r *mutationResolver) setConfigFloat(key string, value *float64) {
c := config.GetInstance()
if value != nil {
c.SetFloat(key, *value)
}
func (r *mutationResolver) Migrate(ctx context.Context, input manager.MigrateInput) (bool, error) {
err := manager.GetInstance().Migrate(ctx, input)
return err == nil, err
}
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGeneralInput) (*ConfigGeneralResult, error) {
@@ -103,7 +47,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
}
}
c.SetInterface(config.Stash, input.Stashes)
c.Set(config.Stash, input.Stashes)
}
checkConfigOverride := func(key string) error {
@@ -138,7 +82,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
if ext != ".db" && ext != ".sqlite" && ext != ".sqlite3" {
return makeConfigGeneralResult(), fmt.Errorf("invalid database path, use extension db, sqlite, or sqlite3")
}
c.SetString(config.Database, *input.DatabasePath)
c.Set(config.Database, input.DatabasePath)
}
existingBackupDirectoryPath := c.GetBackupDirectoryPath()
@@ -147,7 +91,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
return makeConfigGeneralResult(), err
}
c.SetString(config.BackupDirectoryPath, *input.BackupDirectoryPath)
c.Set(config.BackupDirectoryPath, input.BackupDirectoryPath)
}
existingGeneratedPath := c.GetGeneratedPath()
@@ -156,7 +100,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
return makeConfigGeneralResult(), err
}
c.SetString(config.Generated, *input.GeneratedPath)
c.Set(config.Generated, input.GeneratedPath)
}
refreshScraperCache := false
@@ -169,7 +113,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
refreshScraperCache = true
refreshScraperSource = true
c.SetString(config.ScrapersPath, *input.ScrapersPath)
c.Set(config.ScrapersPath, input.ScrapersPath)
}
refreshPluginCache := false
@@ -182,7 +126,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
refreshPluginCache = true
refreshPluginSource = true
c.SetString(config.PluginsPath, *input.PluginsPath)
c.Set(config.PluginsPath, input.PluginsPath)
}
existingMetadataPath := c.GetMetadataPath()
@@ -191,7 +135,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
return makeConfigGeneralResult(), err
}
c.SetString(config.Metadata, *input.MetadataPath)
c.Set(config.Metadata, input.MetadataPath)
}
refreshStreamManager := false
@@ -201,7 +145,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
return makeConfigGeneralResult(), err
}
c.SetString(config.Cache, *input.CachePath)
c.Set(config.Cache, input.CachePath)
refreshStreamManager = true
}
@@ -212,7 +156,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
return makeConfigGeneralResult(), err
}
c.SetString(config.BlobsPath, *input.BlobsPath)
c.Set(config.BlobsPath, input.BlobsPath)
refreshBlobStorage = true
}
@@ -221,34 +165,12 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
return makeConfigGeneralResult(), fmt.Errorf("blobs path must be set when using filesystem storage")
}
c.SetInterface(config.BlobsStorage, *input.BlobsStorage)
// TODO - migrate between systems
c.Set(config.BlobsStorage, input.BlobsStorage)
refreshBlobStorage = true
}
refreshFfmpeg := false
if input.FfmpegPath != nil && *input.FfmpegPath != c.GetFFMpegPath() {
if *input.FfmpegPath != "" {
if err := ffmpeg.ValidateFFMpeg(*input.FfmpegPath); err != nil {
return makeConfigGeneralResult(), fmt.Errorf("invalid ffmpeg path: %w", err)
}
}
c.SetString(config.FFMpegPath, *input.FfmpegPath)
refreshFfmpeg = true
}
if input.FfprobePath != nil && *input.FfprobePath != c.GetFFProbePath() {
if *input.FfprobePath != "" {
if err := ffmpeg.ValidateFFProbe(*input.FfprobePath); err != nil {
return makeConfigGeneralResult(), fmt.Errorf("invalid ffprobe path: %w", err)
}
}
c.SetString(config.FFProbePath, *input.FfprobePath)
refreshFfmpeg = true
}
if input.VideoFileNamingAlgorithm != nil && *input.VideoFileNamingAlgorithm != c.GetVideoFileNamingAlgorithm() {
calculateMD5 := c.IsCalculateMD5()
if input.CalculateMd5 != nil {
@@ -265,42 +187,68 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
return makeConfigGeneralResult(), err
}
c.SetInterface(config.VideoFileNamingAlgorithm, *input.VideoFileNamingAlgorithm)
c.Set(config.VideoFileNamingAlgorithm, *input.VideoFileNamingAlgorithm)
}
r.setConfigBool(config.CalculateMD5, input.CalculateMd5)
r.setConfigInt(config.ParallelTasks, input.ParallelTasks)
r.setConfigBool(config.PreviewAudio, input.PreviewAudio)
r.setConfigInt(config.PreviewSegments, input.PreviewSegments)
r.setConfigFloat(config.PreviewSegmentDuration, input.PreviewSegmentDuration)
r.setConfigString(config.PreviewExcludeStart, input.PreviewExcludeStart)
r.setConfigString(config.PreviewExcludeEnd, input.PreviewExcludeEnd)
if input.CalculateMd5 != nil {
c.Set(config.CalculateMD5, *input.CalculateMd5)
}
if input.ParallelTasks != nil {
c.Set(config.ParallelTasks, *input.ParallelTasks)
}
if input.PreviewAudio != nil {
c.Set(config.PreviewAudio, *input.PreviewAudio)
}
if input.PreviewSegments != nil {
c.Set(config.PreviewSegments, *input.PreviewSegments)
}
if input.PreviewSegmentDuration != nil {
c.Set(config.PreviewSegmentDuration, *input.PreviewSegmentDuration)
}
if input.PreviewExcludeStart != nil {
c.Set(config.PreviewExcludeStart, *input.PreviewExcludeStart)
}
if input.PreviewExcludeEnd != nil {
c.Set(config.PreviewExcludeEnd, *input.PreviewExcludeEnd)
}
if input.PreviewPreset != nil {
c.SetString(config.PreviewPreset, input.PreviewPreset.String())
c.Set(config.PreviewPreset, input.PreviewPreset.String())
}
r.setConfigBool(config.TranscodeHardwareAcceleration, input.TranscodeHardwareAcceleration)
if input.TranscodeHardwareAcceleration != nil {
c.Set(config.TranscodeHardwareAcceleration, *input.TranscodeHardwareAcceleration)
}
if input.MaxTranscodeSize != nil {
c.SetString(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
c.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
}
if input.MaxStreamingTranscodeSize != nil {
c.SetString(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
c.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
}
if input.WriteImageThumbnails != nil {
c.Set(config.WriteImageThumbnails, *input.WriteImageThumbnails)
}
if input.CreateImageClipsFromVideos != nil {
c.Set(config.CreateImageClipsFromVideos, *input.CreateImageClipsFromVideos)
}
r.setConfigBool(config.WriteImageThumbnails, input.WriteImageThumbnails)
r.setConfigBool(config.CreateImageClipsFromVideos, input.CreateImageClipsFromVideos)
if input.GalleryCoverRegex != nil {
_, err := regexp.Compile(*input.GalleryCoverRegex)
if err != nil {
return makeConfigGeneralResult(), fmt.Errorf("Gallery cover regex '%v' invalid, '%v'", *input.GalleryCoverRegex, err.Error())
}
c.SetString(config.GalleryCoverRegex, *input.GalleryCoverRegex)
c.Set(config.GalleryCoverRegex, *input.GalleryCoverRegex)
}
if input.Username != nil && *input.Username != c.GetUsername() {
c.SetString(config.Username, *input.Username)
c.Set(config.Username, input.Username)
if *input.Password == "" {
logger.Info("Username cleared")
} else {
@@ -323,13 +271,24 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
}
r.setConfigInt(config.MaxSessionAge, input.MaxSessionAge)
r.setConfigString(config.LogFile, input.LogFile)
r.setConfigBool(config.LogOut, input.LogOut)
r.setConfigBool(config.LogAccess, input.LogAccess)
if input.MaxSessionAge != nil {
c.Set(config.MaxSessionAge, *input.MaxSessionAge)
}
if input.LogFile != nil {
c.Set(config.LogFile, input.LogFile)
}
if input.LogOut != nil {
c.Set(config.LogOut, *input.LogOut)
}
if input.LogAccess != nil {
c.Set(config.LogAccess, *input.LogAccess)
}
if input.LogLevel != nil && *input.LogLevel != c.GetLogLevel() {
c.SetString(config.LogLevel, *input.LogLevel)
c.Set(config.LogLevel, input.LogLevel)
logger := manager.GetInstance().Logger
logger.SetLogLevel(*input.LogLevel)
}
@@ -341,7 +300,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
return makeConfigGeneralResult(), fmt.Errorf("video exclusion pattern '%v' invalid: %w", r, err)
}
}
c.SetInterface(config.Exclude, input.Excludes)
c.Set(config.Exclude, input.Excludes)
}
if input.ImageExcludes != nil {
@@ -351,25 +310,27 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
return makeConfigGeneralResult(), fmt.Errorf("image/gallery exclusion pattern '%v' invalid: %w", r, err)
}
}
c.SetInterface(config.ImageExclude, input.ImageExcludes)
c.Set(config.ImageExclude, input.ImageExcludes)
}
if input.VideoExtensions != nil {
c.SetInterface(config.VideoExtensions, input.VideoExtensions)
c.Set(config.VideoExtensions, input.VideoExtensions)
}
if input.ImageExtensions != nil {
c.SetInterface(config.ImageExtensions, input.ImageExtensions)
c.Set(config.ImageExtensions, input.ImageExtensions)
}
if input.GalleryExtensions != nil {
c.SetInterface(config.GalleryExtensions, input.GalleryExtensions)
c.Set(config.GalleryExtensions, input.GalleryExtensions)
}
r.setConfigBool(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
if input.CreateGalleriesFromFolders != nil {
c.Set(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
}
if input.CustomPerformerImageLocation != nil {
c.SetString(config.CustomPerformerImageLocation, *input.CustomPerformerImageLocation)
c.Set(config.CustomPerformerImageLocation, *input.CustomPerformerImageLocation)
initCustomPerformerImages(*input.CustomPerformerImageLocation)
}
@@ -377,35 +338,37 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
if err := c.ValidateStashBoxes(input.StashBoxes); err != nil {
return nil, err
}
c.SetInterface(config.StashBoxes, input.StashBoxes)
c.Set(config.StashBoxes, input.StashBoxes)
}
if input.PythonPath != nil {
r.setConfigString(config.PythonPath, input.PythonPath)
c.Set(config.PythonPath, input.PythonPath)
}
if input.TranscodeInputArgs != nil {
c.SetInterface(config.TranscodeInputArgs, input.TranscodeInputArgs)
c.Set(config.TranscodeInputArgs, input.TranscodeInputArgs)
}
if input.TranscodeOutputArgs != nil {
c.SetInterface(config.TranscodeOutputArgs, input.TranscodeOutputArgs)
c.Set(config.TranscodeOutputArgs, input.TranscodeOutputArgs)
}
if input.LiveTranscodeInputArgs != nil {
c.SetInterface(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs)
c.Set(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs)
}
if input.LiveTranscodeOutputArgs != nil {
c.SetInterface(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs)
c.Set(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs)
}
r.setConfigBool(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
if input.DrawFunscriptHeatmapRange != nil {
c.Set(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
}
if input.ScraperPackageSources != nil {
c.SetInterface(config.ScraperPackageSources, input.ScraperPackageSources)
c.Set(config.ScraperPackageSources, input.ScraperPackageSources)
refreshScraperSource = true
}
if input.PluginPackageSources != nil {
c.SetInterface(config.PluginPackageSources, input.PluginPackageSources)
c.Set(config.PluginPackageSources, input.PluginPackageSources)
refreshPluginSource = true
}
@@ -420,12 +383,6 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
if refreshPluginCache {
manager.GetInstance().RefreshPluginCache()
}
if refreshFfmpeg {
manager.GetInstance().RefreshFFMpeg(ctx)
// refresh stream manager is required since ffmpeg changed
refreshStreamManager = true
}
if refreshStreamManager {
manager.GetInstance().RefreshStreamManager()
}
@@ -445,70 +402,102 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) {
c := config.GetInstance()
if input.MenuItems != nil {
c.SetInterface(config.MenuItems, input.MenuItems)
setBool := func(key string, v *bool) {
if v != nil {
c.Set(key, *v)
}
}
r.setConfigBool(config.SoundOnPreview, input.SoundOnPreview)
r.setConfigBool(config.WallShowTitle, input.WallShowTitle)
setString := func(key string, v *string) {
if v != nil {
c.Set(key, *v)
}
}
r.setConfigBool(config.NoBrowser, input.NoBrowser)
if input.MenuItems != nil {
c.Set(config.MenuItems, input.MenuItems)
}
r.setConfigBool(config.NotificationsEnabled, input.NotificationsEnabled)
setBool(config.SoundOnPreview, input.SoundOnPreview)
setBool(config.WallShowTitle, input.WallShowTitle)
r.setConfigBool(config.ShowScrubber, input.ShowScrubber)
setBool(config.NoBrowser, input.NoBrowser)
r.setConfigString(config.WallPlayback, input.WallPlayback)
r.setConfigInt(config.MaximumLoopDuration, input.MaximumLoopDuration)
r.setConfigBool(config.AutostartVideo, input.AutostartVideo)
r.setConfigBool(config.ShowStudioAsText, input.ShowStudioAsText)
r.setConfigBool(config.AutostartVideoOnPlaySelected, input.AutostartVideoOnPlaySelected)
r.setConfigBool(config.ContinuePlaylistDefault, input.ContinuePlaylistDefault)
setBool(config.NotificationsEnabled, input.NotificationsEnabled)
r.setConfigString(config.Language, input.Language)
setBool(config.ShowScrubber, input.ShowScrubber)
if input.WallPlayback != nil {
c.Set(config.WallPlayback, *input.WallPlayback)
}
if input.MaximumLoopDuration != nil {
c.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
}
setBool(config.AutostartVideo, input.AutostartVideo)
setBool(config.ShowStudioAsText, input.ShowStudioAsText)
setBool(config.AutostartVideoOnPlaySelected, input.AutostartVideoOnPlaySelected)
setBool(config.ContinuePlaylistDefault, input.ContinuePlaylistDefault)
if input.Language != nil {
c.Set(config.Language, *input.Language)
}
if input.ImageLightbox != nil {
options := input.ImageLightbox
r.setConfigInt(config.ImageLightboxSlideshowDelay, options.SlideshowDelay)
if options.SlideshowDelay != nil {
c.Set(config.ImageLightboxSlideshowDelay, *options.SlideshowDelay)
}
r.setConfigString(config.ImageLightboxDisplayModeKey, (*string)(options.DisplayMode))
r.setConfigBool(config.ImageLightboxScaleUp, options.ScaleUp)
r.setConfigBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
r.setConfigString(config.ImageLightboxScrollModeKey, (*string)(options.ScrollMode))
setString(config.ImageLightboxDisplayModeKey, (*string)(options.DisplayMode))
setBool(config.ImageLightboxScaleUp, options.ScaleUp)
setBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
setString(config.ImageLightboxScrollModeKey, (*string)(options.ScrollMode))
r.setConfigInt(config.ImageLightboxScrollAttemptsBeforeChange, options.ScrollAttemptsBeforeChange)
if options.ScrollAttemptsBeforeChange != nil {
c.Set(config.ImageLightboxScrollAttemptsBeforeChange, *options.ScrollAttemptsBeforeChange)
}
}
if input.CSS != nil {
c.SetCSS(*input.CSS)
}
r.setConfigBool(config.CSSEnabled, input.CSSEnabled)
setBool(config.CSSEnabled, input.CSSEnabled)
if input.Javascript != nil {
c.SetJavascript(*input.Javascript)
}
r.setConfigBool(config.JavascriptEnabled, input.JavascriptEnabled)
setBool(config.JavascriptEnabled, input.JavascriptEnabled)
if input.CustomLocales != nil {
c.SetCustomLocales(*input.CustomLocales)
}
r.setConfigBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
setBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
if input.DisableDropdownCreate != nil {
ddc := input.DisableDropdownCreate
r.setConfigBool(config.DisableDropdownCreatePerformer, ddc.Performer)
r.setConfigBool(config.DisableDropdownCreateStudio, ddc.Studio)
r.setConfigBool(config.DisableDropdownCreateTag, ddc.Tag)
r.setConfigBool(config.DisableDropdownCreateMovie, ddc.Movie)
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
setBool(config.DisableDropdownCreateStudio, ddc.Studio)
setBool(config.DisableDropdownCreateTag, ddc.Tag)
setBool(config.DisableDropdownCreateMovie, ddc.Movie)
}
r.setConfigString(config.HandyKey, input.HandyKey)
r.setConfigInt(config.FunscriptOffset, input.FunscriptOffset)
r.setConfigBool(config.UseStashHostedFunscript, input.UseStashHostedFunscript)
if input.HandyKey != nil {
c.Set(config.HandyKey, *input.HandyKey)
}
if input.FunscriptOffset != nil {
c.Set(config.FunscriptOffset, *input.FunscriptOffset)
}
if input.UseStashHostedFunscript != nil {
c.Set(config.UseStashHostedFunscript, *input.UseStashHostedFunscript)
}
if err := c.Write(); err != nil {
return makeConfigInterfaceResult(), err
@@ -520,23 +509,26 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAInput) (*ConfigDLNAResult, error) {
c := config.GetInstance()
r.setConfigString(config.DLNAServerName, input.ServerName)
if input.WhitelistedIPs != nil {
c.SetInterface(config.DLNADefaultIPWhitelist, input.WhitelistedIPs)
if input.ServerName != nil {
c.Set(config.DLNAServerName, *input.ServerName)
}
r.setConfigString(config.DLNAVideoSortOrder, input.VideoSortOrder)
r.setConfigInt(config.DLNAPort, input.Port)
if input.WhitelistedIPs != nil {
c.Set(config.DLNADefaultIPWhitelist, input.WhitelistedIPs)
}
if input.VideoSortOrder != nil {
c.Set(config.DLNAVideoSortOrder, input.VideoSortOrder)
}
refresh := false
if input.Enabled != nil {
c.SetBool(config.DLNADefaultEnabled, *input.Enabled)
c.Set(config.DLNADefaultEnabled, *input.Enabled)
refresh = true
}
if input.Interfaces != nil {
c.SetInterface(config.DLNAInterfaces, input.Interfaces)
c.Set(config.DLNAInterfaces, input.Interfaces)
}
if err := c.Write(); err != nil {
@@ -555,12 +547,12 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigSc
refreshScraperCache := false
if input.ScraperUserAgent != nil {
c.SetString(config.ScraperUserAgent, *input.ScraperUserAgent)
c.Set(config.ScraperUserAgent, input.ScraperUserAgent)
refreshScraperCache = true
}
if input.ScraperCDPPath != nil {
c.SetString(config.ScraperCDPPath, *input.ScraperCDPPath)
c.Set(config.ScraperCDPPath, input.ScraperCDPPath)
refreshScraperCache = true
}
@@ -571,10 +563,12 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigSc
return makeConfigScrapingResult(), fmt.Errorf("tag exclusion pattern '%v' invalid: %w", r, err)
}
}
c.SetInterface(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns)
c.Set(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns)
}
r.setConfigBool(config.ScraperCertCheck, input.ScraperCertCheck)
if input.ScraperCertCheck != nil {
c.Set(config.ScraperCertCheck, input.ScraperCertCheck)
}
if refreshScraperCache {
manager.GetInstance().RefreshScraperCache()
@@ -590,25 +584,30 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input ConfigDe
c := config.GetInstance()
if input.Identify != nil {
c.SetInterface(config.DefaultIdentifySettings, input.Identify)
c.Set(config.DefaultIdentifySettings, input.Identify)
}
if input.Scan != nil {
// if input.Scan is used then ScanMetadataOptions is included in the config file
// this causes the values to not be read correctly
c.SetInterface(config.DefaultScanSettings, input.Scan.ScanMetadataOptions)
c.Set(config.DefaultScanSettings, input.Scan.ScanMetadataOptions)
}
if input.AutoTag != nil {
c.SetInterface(config.DefaultAutoTagSettings, input.AutoTag)
c.Set(config.DefaultAutoTagSettings, input.AutoTag)
}
if input.Generate != nil {
c.SetInterface(config.DefaultGenerateSettings, input.Generate)
c.Set(config.DefaultGenerateSettings, input.Generate)
}
r.setConfigBool(config.DeleteFileDefault, input.DeleteFile)
r.setConfigBool(config.DeleteGeneratedDefault, input.DeleteGenerated)
if input.DeleteFile != nil {
c.Set(config.DeleteFileDefault, *input.DeleteFile)
}
if input.DeleteGenerated != nil {
c.Set(config.DeleteGeneratedDefault, *input.DeleteGenerated)
}
if err := c.Write(); err != nil {
return makeConfigDefaultsResult(), err
@@ -632,7 +631,7 @@ func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input GenerateAPI
}
}
c.SetString(config.ApiKey, newAPIKey)
c.Set(config.ApiKey, newAPIKey)
if err := c.Write(); err != nil {
return newAPIKey, err
}
@@ -640,23 +639,9 @@ func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input GenerateAPI
return newAPIKey, nil
}
func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]interface{}, partial map[string]interface{}) (map[string]interface{}, error) {
func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
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)
c.SetUIConfiguration(existing)
}
c.SetUIConfiguration(input)
if err := c.Write(); err != nil {
return c.GetUIConfiguration(), err
@@ -668,25 +653,14 @@ func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]int
func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, value interface{}) (map[string]interface{}, error) {
c := config.GetInstance()
cfg := utils.NestedMap(c.GetUIConfiguration())
cfg := c.GetUIConfiguration()
cfg[key] = value
// #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)
return r.ConfigureUI(ctx, cfg)
}
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 {

View File

@@ -478,61 +478,6 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input Galler
return true, nil
}
func (r *mutationResolver) SetGalleryCover(ctx context.Context, input GallerySetCoverInput) (bool, error) {
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return false, fmt.Errorf("converting gallery id: %w", err)
}
coverImageID, err := strconv.Atoi(input.CoverImageID)
if err != nil {
return false, fmt.Errorf("converting cover image id: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
gallery, err := qb.Find(ctx, galleryID)
if err != nil {
return err
}
if gallery == nil {
return fmt.Errorf("gallery with id %d not found", galleryID)
}
return r.galleryService.SetCover(ctx, gallery, coverImageID)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) ResetGalleryCover(ctx context.Context, input GalleryResetCoverInput) (bool, error) {
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return false, fmt.Errorf("converting gallery id: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
gallery, err := qb.Find(ctx, galleryID)
if err != nil {
return err
}
if gallery == nil {
return fmt.Errorf("gallery with id %d not found", galleryID)
}
return r.galleryService.ResetCover(ctx, gallery)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret *models.GalleryChapter, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.GalleryChapter.Find(ctx, id)

View File

@@ -1,413 +0,0 @@
package api
import (
"context"
"fmt"
"strconv"
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/group"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/utils"
)
func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Populate a new group from the input
newGroup := models.NewGroup()
newGroup.Name = input.Name
newGroup.Aliases = translator.string(input.Aliases)
newGroup.Duration = input.Duration
newGroup.Rating = input.Rating100
newGroup.Director = translator.string(input.Director)
newGroup.Synopsis = translator.string(input.Synopsis)
var err error
newGroup.Date, err = translator.datePtr(input.Date)
if err != nil {
return nil, fmt.Errorf("converting date: %w", err)
}
newGroup.StudioID, err = translator.intPtrFromString(input.StudioID)
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
newGroup.TagIDs, err = translator.relatedIds(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
newGroup.ContainingGroups, err = translator.groupIDDescriptions(input.ContainingGroups)
if err != nil {
return nil, fmt.Errorf("converting containing group ids: %w", err)
}
newGroup.SubGroups, err = translator.groupIDDescriptions(input.SubGroups)
if err != nil {
return nil, fmt.Errorf("converting containing group ids: %w", err)
}
if input.Urls != nil {
newGroup.URLs = models.NewRelatedStrings(input.Urls)
}
return &newGroup, nil
}
func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
newGroup, err := groupFromGroupCreateInput(ctx, input)
if err != nil {
return nil, err
}
// Process the base 64 encoded image string
var frontimageData []byte
if input.FrontImage != nil {
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
if err != nil {
return nil, fmt.Errorf("processing front image: %w", err)
}
}
// Process the base 64 encoded image string
var backimageData []byte
if input.BackImage != nil {
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
if err != nil {
return nil, fmt.Errorf("processing back image: %w", err)
}
}
// HACK: if back image is being set, set the front image to the default.
// This is because we can't have a null front image with a non-null back image.
if len(frontimageData) == 0 && len(backimageData) != 0 {
frontimageData = static.ReadAll(static.DefaultGroupImage)
}
// Start the transaction and save the group
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err = r.groupService.Create(ctx, newGroup, frontimageData, backimageData); err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.GroupCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.MovieCreatePost, input, nil)
return r.getGroup(ctx, newGroup.ID)
}
func groupPartialFromGroupUpdateInput(translator changesetTranslator, input GroupUpdateInput) (ret models.GroupPartial, err error) {
// Populate group from the input
updatedGroup := models.NewGroupPartial()
updatedGroup.Name = translator.optionalString(input.Name, "name")
updatedGroup.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedGroup.Duration = translator.optionalInt(input.Duration, "duration")
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedGroup.Director = translator.optionalString(input.Director, "director")
updatedGroup.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
updatedGroup.Date, err = translator.optionalDate(input.Date, "date")
if err != nil {
err = fmt.Errorf("converting date: %w", err)
return
}
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
err = fmt.Errorf("converting studio id: %w", err)
return
}
updatedGroup.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
if err != nil {
err = fmt.Errorf("converting tag ids: %w", err)
return
}
updatedGroup.ContainingGroups, err = translator.updateGroupIDDescriptions(input.ContainingGroups, "containing_groups")
if err != nil {
err = fmt.Errorf("converting containing group ids: %w", err)
return
}
updatedGroup.SubGroups, err = translator.updateGroupIDDescriptions(input.SubGroups, "sub_groups")
if err != nil {
err = fmt.Errorf("converting containing group ids: %w", err)
return
}
updatedGroup.URLs = translator.updateStrings(input.Urls, "urls")
return updatedGroup, nil
}
func (r *mutationResolver) GroupUpdate(ctx context.Context, input GroupUpdateInput) (*models.Group, error) {
groupID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, fmt.Errorf("converting id: %w", err)
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedGroup, err := groupPartialFromGroupUpdateInput(translator, input)
if err != nil {
return nil, err
}
var frontimageData []byte
frontImageIncluded := translator.hasField("front_image")
if input.FrontImage != nil {
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
if err != nil {
return nil, fmt.Errorf("processing front image: %w", err)
}
}
var backimageData []byte
backImageIncluded := translator.hasField("back_image")
if input.BackImage != nil {
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
if err != nil {
return nil, fmt.Errorf("processing back image: %w", err)
}
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
frontImage := group.ImageInput{
Image: frontimageData,
Set: frontImageIncluded,
}
backImage := group.ImageInput{
Image: backimageData,
Set: backImageIncluded,
}
_, err = r.groupService.UpdatePartial(ctx, groupID, updatedGroup, frontImage, backImage)
if err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, groupID, hook.GroupUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, groupID, hook.MovieUpdatePost, input, translator.getFields())
return r.getGroup(ctx, groupID)
}
func groupPartialFromBulkGroupUpdateInput(translator changesetTranslator, input BulkGroupUpdateInput) (ret models.GroupPartial, err error) {
updatedGroup := models.NewGroupPartial()
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedGroup.Director = translator.optionalString(input.Director, "director")
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
err = fmt.Errorf("converting studio id: %w", err)
return
}
updatedGroup.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
if err != nil {
err = fmt.Errorf("converting tag ids: %w", err)
return
}
updatedGroup.ContainingGroups, err = translator.updateGroupIDDescriptionsBulk(input.ContainingGroups, "containing_groups")
if err != nil {
err = fmt.Errorf("converting containing group ids: %w", err)
return
}
updatedGroup.SubGroups, err = translator.updateGroupIDDescriptionsBulk(input.SubGroups, "sub_groups")
if err != nil {
err = fmt.Errorf("converting containing group ids: %w", err)
return
}
updatedGroup.URLs = translator.optionalURLsBulk(input.Urls, nil)
return updatedGroup, nil
}
func (r *mutationResolver) BulkGroupUpdate(ctx context.Context, input BulkGroupUpdateInput) ([]*models.Group, error) {
groupIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, fmt.Errorf("converting ids: %w", err)
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Populate group from the input
updatedGroup, err := groupPartialFromBulkGroupUpdateInput(translator, input)
if err != nil {
return nil, err
}
ret := []*models.Group{}
if err := r.withTxn(ctx, func(ctx context.Context) error {
for _, groupID := range groupIDs {
group, err := r.groupService.UpdatePartial(ctx, groupID, updatedGroup, group.ImageInput{}, group.ImageInput{})
if err != nil {
return err
}
ret = append(ret, group)
}
return nil
}); err != nil {
return nil, err
}
var newRet []*models.Group
for _, group := range ret {
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.GroupUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.MovieUpdatePost, input, translator.getFields())
group, err = r.getGroup(ctx, group.ID)
if err != nil {
return nil, err
}
newRet = append(newRet, group)
}
return newRet, nil
}
func (r *mutationResolver) GroupDestroy(ctx context.Context, input GroupDestroyInput) (bool, error) {
id, err := strconv.Atoi(input.ID)
if err != nil {
return false, fmt.Errorf("converting id: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.repository.Group.Destroy(ctx, id)
}); err != nil {
return false, err
}
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
return true, nil
}
func (r *mutationResolver) GroupsDestroy(ctx context.Context, groupIDs []string) (bool, error) {
ids, err := stringslice.StringSliceToIntSlice(groupIDs)
if err != nil {
return false, fmt.Errorf("converting ids: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Group
for _, id := range ids {
if err := qb.Destroy(ctx, id); err != nil {
return err
}
}
return nil
}); err != nil {
return false, err
}
for _, id := range ids {
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, groupIDs, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, groupIDs, nil)
}
return true, nil
}
func (r *mutationResolver) AddGroupSubGroups(ctx context.Context, input GroupSubGroupAddInput) (bool, error) {
groupID, err := strconv.Atoi(input.ContainingGroupID)
if err != nil {
return false, fmt.Errorf("converting group id: %w", err)
}
subGroups, err := groupsDescriptionsFromGroupInput(input.SubGroups)
if err != nil {
return false, fmt.Errorf("converting sub group ids: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.groupService.AddSubGroups(ctx, groupID, subGroups, input.InsertIndex)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) RemoveGroupSubGroups(ctx context.Context, input GroupSubGroupRemoveInput) (bool, error) {
groupID, err := strconv.Atoi(input.ContainingGroupID)
if err != nil {
return false, fmt.Errorf("converting group id: %w", err)
}
subGroupIDs, err := stringslice.StringSliceToIntSlice(input.SubGroupIds)
if err != nil {
return false, fmt.Errorf("converting sub group ids: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.groupService.RemoveSubGroups(ctx, groupID, subGroupIDs)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) ReorderSubGroups(ctx context.Context, input ReorderSubGroupsInput) (bool, error) {
groupID, err := strconv.Atoi(input.GroupID)
if err != nil {
return false, fmt.Errorf("converting group id: %w", err)
}
subGroupIDs, err := stringslice.StringSliceToIntSlice(input.SubGroupIds)
if err != nil {
return false, fmt.Errorf("converting sub group ids: %w", err)
}
insertPointID, err := strconv.Atoi(input.InsertAtID)
if err != nil {
return false, fmt.Errorf("converting insert at id: %w", err)
}
insertAfter := utils.IsTrue(input.InsertAfter)
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.groupService.ReorderSubGroups(ctx, groupID, subGroupIDs, insertPointID, insertAfter)
}); err != nil {
return false, err
}
return true, nil
}

View File

@@ -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 models.ImageUpdateInput) (ret *models.Image, err error) {
func (r *mutationResolver) ImageUpdate(ctx context.Context, input ImageUpdateInput) (ret *models.Image, err error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
@@ -46,7 +46,7 @@ func (r *mutationResolver) ImageUpdate(ctx context.Context, input models.ImageUp
return r.getImage(ctx, ret.ID)
}
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*models.ImageUpdateInput) (ret []*models.Image, err error) {
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*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 []*models.Ima
return newRet, nil
}
func (r *mutationResolver) imageUpdate(ctx context.Context, input models.ImageUpdateInput, translator changesetTranslator) (*models.Image, error) {
func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInput, translator changesetTranslator) (*models.Image, error) {
imageID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, fmt.Errorf("converting id: %w", err)

View File

@@ -38,16 +38,3 @@ func (r *mutationResolver) MigrateBlobs(ctx context.Context, input MigrateBlobsI
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) Migrate(ctx context.Context, input manager.MigrateInput) (string, error) {
mgr := manager.GetInstance()
t := &task.MigrateJob{
BackupPath: input.BackupPath,
Config: mgr.Config,
Database: mgr.Database,
}
jobID := mgr.JobManager.Add(ctx, "Migrating database...", t)
return strconv.Itoa(jobID), nil
}

View File

@@ -12,10 +12,10 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
// used to refetch group after hooks run
func (r *mutationResolver) getGroup(ctx context.Context, id int) (ret *models.Group, err error) {
// used to refetch movie after hooks run
func (r *mutationResolver) getMovie(ctx context.Context, id int) (ret *models.Movie, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Group.Find(ctx, id)
ret, err = r.repository.Movie.Find(ctx, id)
return err
}); err != nil {
return nil, err
@@ -24,43 +24,33 @@ func (r *mutationResolver) getGroup(ctx context.Context, id int) (ret *models.Gr
return ret, nil
}
func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInput) (*models.Group, error) {
func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInput) (*models.Movie, error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Populate a new group from the input
newGroup := models.NewGroup()
// Populate a new movie from the input
newMovie := models.NewMovie()
newGroup.Name = input.Name
newGroup.Aliases = translator.string(input.Aliases)
newGroup.Duration = input.Duration
newGroup.Rating = input.Rating100
newGroup.Director = translator.string(input.Director)
newGroup.Synopsis = translator.string(input.Synopsis)
newMovie.Name = input.Name
newMovie.Aliases = translator.string(input.Aliases)
newMovie.Duration = input.Duration
newMovie.Rating = input.Rating100
newMovie.Director = translator.string(input.Director)
newMovie.Synopsis = translator.string(input.Synopsis)
newMovie.URL = translator.string(input.URL)
var err error
newGroup.Date, err = translator.datePtr(input.Date)
newMovie.Date, err = translator.datePtr(input.Date)
if err != nil {
return nil, fmt.Errorf("converting date: %w", err)
}
newGroup.StudioID, err = translator.intPtrFromString(input.StudioID)
newMovie.StudioID, err = translator.intPtrFromString(input.StudioID)
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
newGroup.TagIDs, err = translator.relatedIds(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
if input.Urls != nil {
newGroup.URLs = models.NewRelatedStrings(input.Urls)
} else if input.URL != nil {
newGroup.URLs = models.NewRelatedStrings([]string{*input.URL})
}
// Process the base 64 encoded image string
var frontimageData []byte
if input.FrontImage != nil {
@@ -82,27 +72,27 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
// HACK: if back image is being set, set the front image to the default.
// This is because we can't have a null front image with a non-null back image.
if len(frontimageData) == 0 && len(backimageData) != 0 {
frontimageData = static.ReadAll(static.DefaultGroupImage)
frontimageData = static.ReadAll(static.DefaultMovieImage)
}
// Start the transaction and save the group
// Start the transaction and save the movie
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Group
qb := r.repository.Movie
err = qb.Create(ctx, &newGroup)
err = qb.Create(ctx, &newMovie)
if err != nil {
return err
}
// update image table
if len(frontimageData) > 0 {
if err := qb.UpdateFrontImage(ctx, newGroup.ID, frontimageData); err != nil {
if err := qb.UpdateFrontImage(ctx, newMovie.ID, frontimageData); err != nil {
return err
}
}
if len(backimageData) > 0 {
if err := qb.UpdateBackImage(ctx, newGroup.ID, backimageData); err != nil {
if err := qb.UpdateBackImage(ctx, newMovie.ID, backimageData); err != nil {
return err
}
}
@@ -112,14 +102,12 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
return nil, err
}
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.GroupCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.MovieCreatePost, input, nil)
return r.getGroup(ctx, newGroup.ID)
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, hook.MovieCreatePost, input, nil)
return r.getMovie(ctx, newMovie.ID)
}
func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInput) (*models.Group, error) {
groupID, err := strconv.Atoi(input.ID)
func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInput) (*models.Movie, error) {
movieID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, fmt.Errorf("converting id: %w", err)
}
@@ -128,32 +116,26 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
inputMap: getUpdateInputMap(ctx),
}
// Populate group from the input
updatedGroup := models.NewGroupPartial()
// Populate movie from the input
updatedMovie := models.NewMoviePartial()
updatedGroup.Name = translator.optionalString(input.Name, "name")
updatedGroup.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedGroup.Duration = translator.optionalInt(input.Duration, "duration")
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedGroup.Director = translator.optionalString(input.Director, "director")
updatedGroup.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
updatedMovie.Name = translator.optionalString(input.Name, "name")
updatedMovie.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedMovie.Duration = translator.optionalInt(input.Duration, "duration")
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedMovie.Director = translator.optionalString(input.Director, "director")
updatedMovie.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
updatedMovie.URL = translator.optionalString(input.URL, "url")
updatedGroup.Date, err = translator.optionalDate(input.Date, "date")
updatedMovie.Date, err = translator.optionalDate(input.Date, "date")
if err != nil {
return nil, fmt.Errorf("converting date: %w", err)
}
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
updatedGroup.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
updatedGroup.URLs = translator.optionalURLs(input.Urls, input.URL)
var frontimageData []byte
frontImageIncluded := translator.hasField("front_image")
if input.FrontImage != nil {
@@ -172,24 +154,24 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
}
}
// Start the transaction and save the group
var group *models.Group
// Start the transaction and save the movie
var movie *models.Movie
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Group
group, err = qb.UpdatePartial(ctx, groupID, updatedGroup)
qb := r.repository.Movie
movie, err = qb.UpdatePartial(ctx, movieID, updatedMovie)
if err != nil {
return err
}
// update image table
if frontImageIncluded {
if err := qb.UpdateFrontImage(ctx, group.ID, frontimageData); err != nil {
if err := qb.UpdateFrontImage(ctx, movie.ID, frontimageData); err != nil {
return err
}
}
if backImageIncluded {
if err := qb.UpdateBackImage(ctx, group.ID, backimageData); err != nil {
if err := qb.UpdateBackImage(ctx, movie.ID, backimageData); err != nil {
return err
}
}
@@ -199,14 +181,12 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
return nil, err
}
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.GroupUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.MovieUpdatePost, input, translator.getFields())
return r.getGroup(ctx, group.ID)
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
return r.getMovie(ctx, movie.ID)
}
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieUpdateInput) ([]*models.Group, error) {
groupIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieUpdateInput) ([]*models.Movie, error) {
movieIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, fmt.Errorf("converting ids: %w", err)
}
@@ -215,36 +195,29 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
inputMap: getUpdateInputMap(ctx),
}
// Populate group from the input
updatedGroup := models.NewGroupPartial()
// Populate movie from the input
updatedMovie := models.NewMoviePartial()
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedGroup.Director = translator.optionalString(input.Director, "director")
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedMovie.Director = translator.optionalString(input.Director, "director")
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
updatedGroup.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
updatedGroup.URLs = translator.optionalURLsBulk(input.Urls, nil)
ret := []*models.Group{}
ret := []*models.Movie{}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Group
qb := r.repository.Movie
for _, groupID := range groupIDs {
group, err := qb.UpdatePartial(ctx, groupID, updatedGroup)
for _, movieID := range movieIDs {
movie, err := qb.UpdatePartial(ctx, movieID, updatedMovie)
if err != nil {
return err
}
ret = append(ret, group)
ret = append(ret, movie)
}
return nil
@@ -252,18 +225,16 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
return nil, err
}
var newRet []*models.Group
for _, group := range ret {
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.GroupUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.MovieUpdatePost, input, translator.getFields())
var newRet []*models.Movie
for _, movie := range ret {
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
group, err = r.getGroup(ctx, group.ID)
movie, err = r.getMovie(ctx, movie.ID)
if err != nil {
return nil, err
}
newRet = append(newRet, group)
newRet = append(newRet, movie)
}
return newRet, nil
@@ -276,26 +247,24 @@ func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyI
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.repository.Group.Destroy(ctx, id)
return r.repository.Movie.Destroy(ctx, id)
}); err != nil {
return false, err
}
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
return true, nil
}
func (r *mutationResolver) MoviesDestroy(ctx context.Context, groupIDs []string) (bool, error) {
ids, err := stringslice.StringSliceToIntSlice(groupIDs)
func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string) (bool, error) {
ids, err := stringslice.StringSliceToIntSlice(movieIDs)
if err != nil {
return false, fmt.Errorf("converting ids: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Group
qb := r.repository.Movie
for _, id := range ids {
if err := qb.Destroy(ctx, id); err != nil {
return err
@@ -308,9 +277,7 @@ func (r *mutationResolver) MoviesDestroy(ctx context.Context, groupIDs []string)
}
for _, id := range ids {
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, groupIDs, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, groupIDs, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, movieIDs, nil)
}
return true, nil

View File

@@ -12,11 +12,6 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
const (
twitterURL = "https://twitter.com"
instagramURL = "https://instagram.com"
)
// used to refetch performer after hooks run
func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *models.Performer, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
@@ -40,6 +35,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
newPerformer.Name = input.Name
newPerformer.Disambiguation = translator.string(input.Disambiguation)
newPerformer.Aliases = models.NewRelatedStrings(input.AliasList)
newPerformer.URL = translator.string(input.URL)
newPerformer.Gender = input.Gender
newPerformer.Ethnicity = translator.string(input.Ethnicity)
newPerformer.Country = translator.string(input.Country)
@@ -51,6 +47,8 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
newPerformer.CareerLength = translator.string(input.CareerLength)
newPerformer.Tattoos = translator.string(input.Tattoos)
newPerformer.Piercings = translator.string(input.Piercings)
newPerformer.Twitter = translator.string(input.Twitter)
newPerformer.Instagram = translator.string(input.Instagram)
newPerformer.Favorite = translator.bool(input.Favorite)
newPerformer.Rating = input.Rating100
newPerformer.Details = translator.string(input.Details)
@@ -58,22 +56,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(models.StashIDInputs(input.StashIds).ToStashIDs())
newPerformer.URLs = models.NewRelatedStrings([]string{})
if input.URL != nil {
newPerformer.URLs.Add(*input.URL)
}
if input.Twitter != nil {
newPerformer.URLs.Add(utils.URLFromHandle(*input.Twitter, twitterURL))
}
if input.Instagram != nil {
newPerformer.URLs.Add(utils.URLFromHandle(*input.Instagram, instagramURL))
}
if input.Urls != nil {
newPerformer.URLs.Add(input.Urls...)
}
newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds)
var err error
@@ -108,13 +91,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
return err
}
i := &models.CreatePerformerInput{
Performer: &newPerformer,
// convert json.Numbers to int/float
CustomFields: convertMapJSONNumbers(input.CustomFields),
}
err = qb.Create(ctx, i)
err = qb.Create(ctx, &newPerformer)
if err != nil {
return err
}
@@ -135,96 +112,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
return r.getPerformer(ctx, newPerformer.ID)
}
func (r *mutationResolver) validateNoLegacyURLs(translator changesetTranslator) error {
// ensure url/twitter/instagram are not included in the input
if translator.hasField("url") {
return fmt.Errorf("url field must not be included if urls is included")
}
if translator.hasField("twitter") {
return fmt.Errorf("twitter field must not be included if urls is included")
}
if translator.hasField("instagram") {
return fmt.Errorf("instagram field must not be included if urls is included")
}
return nil
}
func (r *mutationResolver) handleLegacyURLs(ctx context.Context, performerID int, legacyURL, legacyTwitter, legacyInstagram models.OptionalString, updatedPerformer *models.PerformerPartial) error {
qb := r.repository.Performer
// we need to be careful with URL/Twitter/Instagram
// treat URL as replacing the first non-Twitter/Instagram URL in the list
// twitter should replace any existing twitter URL
// instagram should replace any existing instagram URL
p, err := qb.Find(ctx, performerID)
if err != nil {
return err
}
if err := p.LoadURLs(ctx, qb); err != nil {
return fmt.Errorf("loading performer URLs: %w", err)
}
existingURLs := p.URLs.List()
// performer partial URLs should be empty
if legacyURL.Set {
replaced := false
for i, url := range existingURLs {
if !performer.IsTwitterURL(url) && !performer.IsInstagramURL(url) {
existingURLs[i] = legacyURL.Value
replaced = true
break
}
}
if !replaced {
existingURLs = append(existingURLs, legacyURL.Value)
}
}
if legacyTwitter.Set {
value := utils.URLFromHandle(legacyTwitter.Value, twitterURL)
found := false
// find and replace the first twitter URL
for i, url := range existingURLs {
if performer.IsTwitterURL(url) {
existingURLs[i] = value
found = true
break
}
}
if !found {
existingURLs = append(existingURLs, value)
}
}
if legacyInstagram.Set {
found := false
value := utils.URLFromHandle(legacyInstagram.Value, instagramURL)
// find and replace the first instagram URL
for i, url := range existingURLs {
if performer.IsInstagramURL(url) {
existingURLs[i] = value
found = true
break
}
}
if !found {
existingURLs = append(existingURLs, value)
}
}
updatedPerformer.URLs = &models.UpdateStrings{
Values: existingURLs,
Mode: models.RelationshipUpdateModeSet,
}
return nil
}
func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) {
performerID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -240,6 +127,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.Name = translator.optionalString(input.Name, "name")
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
updatedPerformer.URL = translator.optionalString(input.URL, "url")
updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender")
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.optionalString(input.Country, "country")
@@ -251,6 +139,8 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedPerformer.Details = translator.optionalString(input.Details, "details")
@@ -259,19 +149,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedPerformer.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
if translator.hasField("urls") {
// ensure url/twitter/instagram are not included in the input
if err := r.validateNoLegacyURLs(translator); err != nil {
return nil, err
}
updatedPerformer.URLs = translator.updateStrings(input.Urls, "urls")
}
legacyURL := translator.optionalString(input.URL, "url")
legacyTwitter := translator.optionalString(input.Twitter, "twitter")
legacyInstagram := translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate")
if err != nil {
return nil, fmt.Errorf("converting birthdate: %w", err)
@@ -296,11 +173,6 @@ 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 {
@@ -314,12 +186,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
if legacyURL.Set || legacyTwitter.Set || legacyInstagram.Set {
if err := r.handleLegacyURLs(ctx, performerID, legacyURL, legacyTwitter, legacyInstagram, &updatedPerformer); err != nil {
return err
}
}
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
return err
}
@@ -359,7 +225,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
updatedPerformer := models.NewPerformerPartial()
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
updatedPerformer.URL = translator.optionalString(input.URL, "url")
updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender")
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.optionalString(input.Country, "country")
@@ -371,7 +237,8 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedPerformer.Details = translator.optionalString(input.Details, "details")
@@ -379,19 +246,6 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
if translator.hasField("urls") {
// ensure url/twitter/instagram are not included in the input
if err := r.validateNoLegacyURLs(translator); err != nil {
return nil, err
}
updatedPerformer.URLs = translator.updateStringsBulk(input.Urls, "urls")
}
legacyURL := translator.optionalString(input.URL, "url")
legacyTwitter := translator.optionalString(input.Twitter, "twitter")
legacyInstagram := translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate")
if err != nil {
return nil, fmt.Errorf("converting birthdate: %w", err)
@@ -423,12 +277,6 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
qb := r.repository.Performer
for _, performerID := range performerIDs {
if legacyURL.Set || legacyTwitter.Set || legacyInstagram.Set {
if err := r.handleLegacyURLs(ctx, performerID, legacyURL, legacyTwitter, legacyInstagram, &updatedPerformer); err != nil {
return err
}
}
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
return err
}

View File

@@ -103,7 +103,7 @@ func (r *mutationResolver) SetPluginsEnabled(ctx context.Context, enabledMap map
}
}
c.SetInterface(config.DisabledPlugins, newDisabled)
c.Set(config.DisabledPlugins, newDisabled)
if err := c.Write(); err != nil {
return false, err

View File

@@ -7,10 +7,7 @@ import (
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput) (ret *models.SavedFilter, err error) {
@@ -70,48 +67,30 @@ func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input Destroy
}
func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaultFilterInput) (bool, error) {
// deprecated - write to the config in the meantime
config := config.GetInstance()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.SavedFilter
uiConfig := config.GetUIConfiguration()
if uiConfig == nil {
uiConfig = make(map[string]interface{})
}
if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil {
// clearing
def, err := qb.FindDefault(ctx, input.Mode)
if err != nil {
return err
}
m := utils.NestedMap(uiConfig)
if def != nil {
return qb.Destroy(ctx, def.ID)
}
if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil {
// clearing
m.Delete("defaultFilters." + strings.ToLower(input.Mode.String()))
config.SetUIConfiguration(m)
if err := config.Write(); err != nil {
return false, err
return nil
}
return true, nil
}
subMap := make(map[string]interface{})
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
WeaklyTypedInput: true,
Result: &subMap,
})
if err != nil {
return false, err
}
if err := d.Decode(input); err != nil {
return false, err
}
m.Set("defaultFilters."+strings.ToLower(input.Mode.String()), subMap)
config.SetUIConfiguration(m)
if err := config.Write(); err != nil {
return qb.SetDefault(ctx, &models.SavedFilter{
Mode: input.Mode,
FindFilter: input.FindFilter,
ObjectFilter: input.ObjectFilter,
UIOptions: input.UIOptions,
})
}); err != nil {
return false, err
}

View File

@@ -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(models.StashIDInputs(input.StashIds).ToStashIDs())
newScene.StashIDs = models.NewRelatedStashIDs(input.StashIds)
newScene.Date, err = translator.datePtr(input.Date)
if err != nil {
@@ -80,17 +80,9 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
// prefer groups over movies
if len(input.Groups) > 0 {
newScene.Groups, err = translator.relatedGroups(input.Groups)
if err != nil {
return nil, fmt.Errorf("converting groups: %w", err)
}
} else if len(input.Movies) > 0 {
newScene.Groups, err = translator.relatedGroupsFromMovies(input.Movies)
if err != nil {
return nil, fmt.Errorf("converting movies: %w", err)
}
newScene.Movies, err = translator.relatedMovies(input.Movies)
if err != nil {
return nil, fmt.Errorf("converting movies: %w", err)
}
var coverImageData []byte
@@ -224,16 +216,9 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
if translator.hasField("groups") {
updatedScene.GroupIDs, err = translator.updateGroupIDs(input.Groups, "groups")
if err != nil {
return nil, fmt.Errorf("converting groups: %w", err)
}
} else if translator.hasField("movies") {
updatedScene.GroupIDs, err = translator.updateGroupIDsFromMovies(input.Movies, "movies")
if err != nil {
return nil, fmt.Errorf("converting movies: %w", err)
}
updatedScene.MovieIDs, err = translator.updateMovieIDs(input.Movies, "movies")
if err != nil {
return nil, fmt.Errorf("converting movies: %w", err)
}
return &updatedScene, nil
@@ -373,16 +358,9 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
if translator.hasField("group_ids") {
updatedScene.GroupIDs, err = translator.updateGroupIDsBulk(input.GroupIds, "group_ids")
if err != nil {
return nil, fmt.Errorf("converting group ids: %w", err)
}
} else if translator.hasField("movie_ids") {
updatedScene.GroupIDs, err = translator.updateGroupIDsBulk(input.MovieIds, "movie_ids")
if err != nil {
return nil, fmt.Errorf("converting movie ids: %w", err)
}
updatedScene.MovieIDs, err = translator.updateMovieIDsBulk(input.MovieIds, "movie_ids")
if err != nil {
return nil, fmt.Errorf("converting movie ids: %w", err)
}
ret := []*models.Scene{}
@@ -655,13 +633,6 @@ 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)
@@ -687,20 +658,6 @@ 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 float64OrZero(f *float64) float64 {
if f == nil {
return 0
}
return *f
}
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) {
markerID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -716,7 +673,6 @@ 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)
@@ -757,26 +713,6 @@ 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
@@ -791,7 +727,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 || float64OrZero(existingMarker.EndSeconds) != float64OrZero(newMarker.EndSeconds) {
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds {
seconds := int(existingMarker.Seconds)
if err := fileDeleter.MarkMarkerFiles(existingScene, seconds); err != nil {
return err
@@ -821,16 +757,11 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
}
func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) {
return r.SceneMarkersDestroy(ctx, []string{id})
}
func (r *mutationResolver) SceneMarkersDestroy(ctx context.Context, markerIDs []string) (bool, error) {
ids, err := stringslice.StringSliceToIntSlice(markerIDs)
markerID, err := strconv.Atoi(id)
if err != nil {
return false, fmt.Errorf("converting ids: %w", err)
return false, fmt.Errorf("converting id: %w", err)
}
var markers []*models.SceneMarker
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
fileDeleter := &scene.FileDeleter{
@@ -843,45 +774,35 @@ func (r *mutationResolver) SceneMarkersDestroy(ctx context.Context, markerIDs []
qb := r.repository.SceneMarker
sqb := r.repository.Scene
for _, markerID := range ids {
marker, err := qb.Find(ctx, markerID)
marker, err := qb.Find(ctx, markerID)
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 err != nil {
return err
}
return nil
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)
}); err != nil {
fileDeleter.Rollback()
return false, err
}
// perform the post-commit actions
fileDeleter.Commit()
for _, marker := range markers {
r.hookExecutor.ExecutePostHooks(ctx, marker.ID, hook.SceneMarkerDestroyPost, markerIDs, nil)
}
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerDestroyPost, id, nil)
return true, nil
}
@@ -904,24 +825,6 @@ func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, res
return ret, nil
}
func (r *mutationResolver) SceneResetActivity(ctx context.Context, id string, resetResume *bool, resetDuration *bool) (ret bool, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return false, fmt.Errorf("converting id: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.ResetActivity(ctx, sceneID, utils.IsTrue(resetResume), utils.IsTrue(resetDuration))
return err
}); err != nil {
return false, err
}
return ret, nil
}
// deprecated
func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id string) (ret int, err error) {
sceneID, err := strconv.Atoi(id)

View File

@@ -6,65 +6,41 @@ import (
"strconv"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"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"
"github.com/stashapp/stash/pkg/scraper/stashbox"
)
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input StashBoxFingerprintSubmissionInput) (bool, error) {
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
if err != nil {
return false, err
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return false, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
ids, err := stringslice.StringSliceToIntSlice(input.SceneIds)
if err != nil {
return false, err
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
client := r.newStashBoxClient(*b)
var scenes []*models.Scene
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
scenes, err = r.sceneService.FindByIDs(ctx, ids, scene.LoadStashIDs, scene.LoadFiles)
return err
}); err != nil {
return false, err
}
return client.SubmitFingerprints(ctx, scenes)
return client.SubmitStashBoxFingerprints(ctx, input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
}
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) {
b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint)
if err != nil {
return "", err
}
jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, b, input)
jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, input)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) StashBoxBatchStudioTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) {
b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint)
if err != nil {
return "", err
}
jobID := manager.GetInstance().StashBoxBatchStudioTag(ctx, b, input)
jobID := manager.GetInstance().StashBoxBatchStudioTag(ctx, input)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
if err != nil {
return nil, err
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := r.newStashBoxClient(*b)
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
id, err := strconv.Atoi(input.ID)
if err != nil {
@@ -88,83 +64,25 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
logger.Errorf("Error getting scene cover: %v", err)
}
draft, err := r.makeSceneDraft(ctx, scene, cover)
if err != nil {
return err
if err := scene.LoadURLs(ctx, r.repository.Scene); err != nil {
return fmt.Errorf("loading scene URLs: %w", err)
}
res, err = client.SubmitSceneDraft(ctx, *draft)
res, err = client.SubmitSceneDraft(ctx, scene, boxes[input.StashBoxIndex].Endpoint, cover)
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 {
return nil, err
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := r.newStashBoxClient(*b)
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
id, err := strconv.Atoi(input.ID)
if err != nil {
@@ -183,22 +101,7 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
return fmt.Errorf("performer with id %d not found", id)
}
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)
res, err = client.SubmitPerformerDraft(ctx, performer, boxes[input.StashBoxIndex].Endpoint)
return err
})

View File

@@ -35,11 +35,10 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
newStudio.Name = input.Name
newStudio.URL = translator.string(input.URL)
newStudio.Rating = input.Rating100
newStudio.Favorite = translator.bool(input.Favorite)
newStudio.Details = translator.string(input.Details)
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
newStudio.Aliases = models.NewRelatedStrings(input.Aliases)
newStudio.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
newStudio.StashIDs = models.NewRelatedStashIDs(input.StashIds)
var err error
@@ -48,11 +47,6 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
return nil, fmt.Errorf("converting parent id: %w", err)
}
newStudio.TagIDs, err = translator.relatedIds(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
// Process the base 64 encoded image string
var imageData []byte
if input.Image != nil {
@@ -109,7 +103,6 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
updatedStudio.URL = translator.optionalString(input.URL, "url")
updatedStudio.Details = translator.optionalString(input.Details, "details")
updatedStudio.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedStudio.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases")
updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
@@ -119,11 +112,6 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
return nil, fmt.Errorf("converting parent id: %w", err)
}
updatedStudio.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
// Process the base 64 encoded image string
var imageData []byte
imageIncluded := translator.hasField("image")

View File

@@ -33,22 +33,25 @@ 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)
newTag.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
var err error
newTag.ParentIDs, err = translator.relatedIds(input.ParentIds)
if err != nil {
return nil, fmt.Errorf("converting parent tag ids: %w", err)
var parentIDs []int
if len(input.ParentIds) > 0 {
parentIDs, err = stringslice.StringSliceToIntSlice(input.ParentIds)
if err != nil {
return nil, fmt.Errorf("converting parent ids: %w", err)
}
}
newTag.ChildIDs, err = translator.relatedIds(input.ChildIds)
if err != nil {
return nil, fmt.Errorf("converting child tag ids: %w", err)
var childIDs []int
if len(input.ChildIds) > 0 {
childIDs, err = stringslice.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return nil, fmt.Errorf("converting child ids: %w", err)
}
}
// Process the base 64 encoded image string
@@ -64,7 +67,8 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Tag
if err := tag.ValidateCreate(ctx, newTag, qb); err != nil {
// ensure name is unique
if err := tag.EnsureTagNameUnique(ctx, 0, newTag.Name, qb); err != nil {
return err
}
@@ -80,6 +84,36 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
}
}
if len(input.Aliases) > 0 {
if err := tag.EnsureAliasesUnique(ctx, newTag.ID, input.Aliases, qb); err != nil {
return err
}
if err := qb.UpdateAliases(ctx, newTag.ID, input.Aliases); err != nil {
return err
}
}
if len(parentIDs) > 0 {
if err := qb.UpdateParentTags(ctx, newTag.ID, parentIDs); err != nil {
return err
}
}
if len(childIDs) > 0 {
if err := qb.UpdateChildTags(ctx, newTag.ID, childIDs); err != nil {
return err
}
}
// FIXME: This should be called before any changes are made, but
// requires a rewrite of ValidateHierarchy.
if len(parentIDs) > 0 || len(childIDs) > 0 {
if err := tag.ValidateHierarchy(ctx, &newTag, parentIDs, childIDs, qb); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
@@ -102,22 +136,23 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
// Populate tag from the input
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")
updatedTag.Aliases = translator.updateStrings(input.Aliases, "aliases")
updatedTag.ParentIDs, err = translator.updateIds(input.ParentIds, "parent_ids")
if err != nil {
return nil, fmt.Errorf("converting parent tag ids: %w", err)
var parentIDs []int
if translator.hasField("parent_ids") {
parentIDs, err = stringslice.StringSliceToIntSlice(input.ParentIds)
if err != nil {
return nil, fmt.Errorf("converting parent ids: %w", err)
}
}
updatedTag.ChildIDs, err = translator.updateIds(input.ChildIds, "child_ids")
if err != nil {
return nil, fmt.Errorf("converting child tag ids: %w", err)
var childIDs []int
if translator.hasField("child_ids") {
childIDs, err = stringslice.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return nil, fmt.Errorf("converting child ids: %w", err)
}
}
var imageData []byte
@@ -134,10 +169,24 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Tag
if err := tag.ValidateUpdate(ctx, tagID, updatedTag, qb); err != nil {
// ensure name is unique
t, err = qb.Find(ctx, tagID)
if err != nil {
return err
}
if t == nil {
return fmt.Errorf("tag with id %d not found", tagID)
}
if input.Name != nil && t.Name != *input.Name {
if err := tag.EnsureTagNameUnique(ctx, tagID, *input.Name, qb); err != nil {
return err
}
updatedTag.Name = models.NewOptionalString(*input.Name)
}
t, err = qb.UpdatePartial(ctx, tagID, updatedTag)
if err != nil {
return err
@@ -150,6 +199,37 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
}
}
if translator.hasField("aliases") {
if err := tag.EnsureAliasesUnique(ctx, tagID, input.Aliases, qb); err != nil {
return err
}
if err := qb.UpdateAliases(ctx, tagID, input.Aliases); err != nil {
return err
}
}
if parentIDs != nil {
if err := qb.UpdateParentTags(ctx, tagID, parentIDs); err != nil {
return err
}
}
if childIDs != nil {
if err := qb.UpdateChildTags(ctx, tagID, childIDs); err != nil {
return err
}
}
// FIXME: This should be called before any changes are made, but
// requires a rewrite of ValidateHierarchy.
if parentIDs != nil || childIDs != nil {
if err := tag.ValidateHierarchy(ctx, t, parentIDs, childIDs, qb); err != nil {
logger.Errorf("Error saving tag: %s", err)
return err
}
}
return nil
}); err != nil {
return nil, err
@@ -159,75 +239,6 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
return r.getTag(ctx, t.ID)
}
func (r *mutationResolver) BulkTagUpdate(ctx context.Context, input BulkTagUpdateInput) ([]*models.Tag, error) {
tagIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, fmt.Errorf("converting ids: %w", err)
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Populate scene from the input
updatedTag := models.NewTagPartial()
updatedTag.Description = translator.optionalString(input.Description, "description")
updatedTag.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedTag.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedTag.Aliases = translator.updateStringsBulk(input.Aliases, "aliases")
updatedTag.ParentIDs, err = translator.updateIdsBulk(input.ParentIds, "parent_ids")
if err != nil {
return nil, fmt.Errorf("converting parent tag ids: %w", err)
}
updatedTag.ChildIDs, err = translator.updateIdsBulk(input.ChildIds, "child_ids")
if err != nil {
return nil, fmt.Errorf("converting child tag ids: %w", err)
}
ret := []*models.Tag{}
// Start the transaction and save the scenes
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Tag
for _, tagID := range tagIDs {
if err := tag.ValidateUpdate(ctx, tagID, updatedTag, qb); err != nil {
return err
}
tag, err := qb.UpdatePartial(ctx, tagID, updatedTag)
if err != nil {
return err
}
ret = append(ret, tag)
}
return nil
}); err != nil {
return nil, err
}
// execute post hooks outside of txn
var newRet []*models.Tag
for _, tag := range ret {
r.hookExecutor.ExecutePostHooks(ctx, tag.ID, hook.TagUpdatePost, input, translator.getFields())
tag, err = r.getTag(ctx, tag.ID)
if err != nil {
return nil, err
}
newRet = append(newRet, tag)
}
return newRet, nil
}
func (r *mutationResolver) TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error) {
tagID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -318,7 +329,7 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput)
return err
}
err = tag.ValidateHierarchyExisting(ctx, t, parents, children, qb)
err = tag.ValidateHierarchy(ctx, t, parents, children, qb)
if err != nil {
logger.Errorf("Error merging tag: %s", err)
return err

View File

@@ -9,6 +9,7 @@ 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"
)
@@ -90,8 +91,6 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
CachePath: config.GetCachePath(),
BlobsPath: config.GetBlobsPath(),
BlobsStorage: config.GetBlobsStorage(),
FfmpegPath: config.GetFFMpegPath(),
FfprobePath: config.GetFFProbePath(),
CalculateMd5: config.IsCalculateMD5(),
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
ParallelTasks: config.GetParallelTasks(),
@@ -198,7 +197,6 @@ func makeConfigDLNAResult() *ConfigDLNAResult {
return &ConfigDLNAResult{
ServerName: config.GetDLNAServerName(),
Enabled: config.GetDLNADefaultEnabled(),
Port: config.GetDLNAPort(),
WhitelistedIPs: config.GetDLNADefaultIPWhitelist(),
Interfaces: config.GetDLNAInterfaces(),
VideoSortOrder: config.GetVideoSortOrder(),
@@ -240,7 +238,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 := r.newStashBoxClient(box)
client := stashbox.NewClient(box, r.stashboxRepository())
user, err := client.GetUser(ctx)

View File

@@ -1,59 +0,0 @@
package api
import (
"context"
"strconv"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *queryResolver) FindGroup(ctx context.Context, id string) (ret *models.Group, err error) {
idInt, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Group.Find(ctx, idInt)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) FindGroups(ctx context.Context, groupFilter *models.GroupFilterType, filter *models.FindFilterType, ids []string) (ret *FindGroupsResultType, err error) {
idInts, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var groups []*models.Group
var err error
var total int
if len(idInts) > 0 {
groups, err = r.repository.Group.FindMany(ctx, idInts)
total = len(groups)
} else {
groups, total, err = r.repository.Group.Query(ctx, groupFilter, filter)
}
if err != nil {
return err
}
ret = &FindGroupsResultType{
Count: total,
Groups: groups,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -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,21 +95,17 @@ func (r *queryResolver) FindImages(
result, err = qb.Query(ctx, models.ImageQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,
Count: slices.Contains(fields, "count"),
Count: sliceutil.Contains(fields, "count"),
},
ImageFilter: imageFilter,
Megapixels: slices.Contains(fields, "megapixels"),
TotalSize: slices.Contains(fields, "filesize"),
Megapixels: sliceutil.Contains(fields, "megapixels"),
TotalSize: sliceutil.Contains(fields, "filesize"),
})
if err == nil {
images, err = result.Resolve(ctx)
}
}
if err != nil {
return err
}
ret = &FindImagesResultType{
Count: result.Count,
Images: images,

View File

@@ -8,14 +8,14 @@ import (
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Group, err error) {
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Movie, err error) {
idInt, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Group.Find(ctx, idInt)
ret, err = r.repository.Movie.Find(ctx, idInt)
return err
}); err != nil {
return nil, err
@@ -24,22 +24,22 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.G
return ret, nil
}
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.GroupFilterType, filter *models.FindFilterType, ids []string) (ret *FindMoviesResultType, err error) {
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType, ids []string) (ret *FindMoviesResultType, err error) {
idInts, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var groups []*models.Group
var movies []*models.Movie
var err error
var total int
if len(idInts) > 0 {
groups, err = r.repository.Group.FindMany(ctx, idInts)
total = len(groups)
movies, err = r.repository.Movie.FindMany(ctx, idInts)
total = len(movies)
} else {
groups, total, err = r.repository.Group.Query(ctx, movieFilter, filter)
movies, total, err = r.repository.Movie.Query(ctx, movieFilter, filter)
}
if err != nil {
@@ -48,7 +48,7 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Grou
ret = &FindMoviesResultType{
Count: total,
Movies: groups,
Movies: movies,
}
return nil
}); err != nil {
@@ -58,9 +58,9 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Grou
return ret, nil
}
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Group, err error) {
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Movie, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Group.All(ctx)
ret, err = r.repository.Movie.All(ctx)
return err
}); err != nil {
return nil, err

View File

@@ -32,11 +32,6 @@ 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

View File

@@ -3,12 +3,8 @@ package api
import (
"context"
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *models.SavedFilter, err error) {
@@ -41,35 +37,11 @@ func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.Filte
}
func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) {
// deprecated - read from the config in the meantime
config := config.GetInstance()
uiConfig := config.GetUIConfiguration()
if uiConfig == nil {
return nil, nil
}
m := utils.NestedMap(uiConfig)
filterRaw, _ := m.Get("defaultFilters." + strings.ToLower(mode.String()))
if filterRaw == nil {
return nil, nil
}
ret = &models.SavedFilter{}
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
WeaklyTypedInput: true,
Result: ret,
})
if err != nil {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
return err
}); err != nil {
return nil, err
}
if err := d.Decode(filterRaw); err != nil {
return nil, err
}
return ret, nil
return ret, err
}

View File

@@ -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: slices.Contains(fields, "count"),
Count: sliceutil.Contains(fields, "count"),
},
SceneFilter: sceneFilter,
TotalDuration: slices.Contains(fields, "duration"),
TotalSize: slices.Contains(fields, "filesize"),
TotalDuration: sliceutil.Contains(fields, "duration"),
TotalSize: sliceutil.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: slices.Contains(fields, "count"),
Count: sliceutil.Contains(fields, "count"),
},
SceneFilter: sceneFilter,
TotalDuration: slices.Contains(fields, "duration"),
TotalSize: slices.Contains(fields, "filesize"),
TotalDuration: sliceutil.Contains(fields, "duration"),
TotalSize: sliceutil.Contains(fields, "filesize"),
})
if err != nil {
return err

View File

@@ -4,31 +4,14 @@ 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, ids []string) (ret *FindSceneMarkersResultType, err error) {
idInts, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (ret *FindSceneMarkersResultType, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
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)
}
sceneMarkers, total, err := r.repository.SceneMarker.Query(ctx, sceneMarkerFilter, filter)
if err != nil {
return err
}
ret = &FindSceneMarkersResultType{
Count: total,
SceneMarkers: sceneMarkers,

View File

@@ -62,11 +62,7 @@ func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilte
func (r *queryResolver) AllTags(ctx context.Context) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.All(ctx)
if err != nil {
return err
}
return nil
return err
}); err != nil {
return nil, err
}

View File

@@ -41,7 +41,6 @@ func jobToJobModel(j job.Job) *Job {
StartTime: j.StartTime,
EndTime: j.EndTime,
AddTime: j.AddTime,
Error: j.Error,
}
if j.Progress != -1 {

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"slices"
"sort"
"strings"
@@ -12,6 +11,7 @@ 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 slices.Contains(graphql.CollectAllFields(ctx), "source_package") {
if sliceutil.Contains(graphql.CollectAllFields(ctx), "source_package") {
ret, err = r.getInstalledPackagesWithUpgrades(ctx, pm)
if err != nil {
return nil, err

View File

@@ -4,12 +4,16 @@ import (
"context"
"errors"
"fmt"
"slices"
"regexp"
"strconv"
"strings"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/logger"
"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"
)
@@ -31,7 +35,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) ([]*models.ScrapedScene, error) {
func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string, query string) ([]*scraper.ScrapedScene, error) {
if query == "" {
return nil, nil
}
@@ -46,10 +50,56 @@ func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string,
return nil, err
}
filterSceneTags(ret)
return ret, nil
}
func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*models.ScrapedScene, error) {
// filterSceneTags removes tags matching excluded tag patterns from the provided scraped scenes
func filterSceneTags(scenes []*scraper.ScrapedScene) {
excludePatterns := manager.GetInstance().Config.GetScraperExcludeTagPatterns()
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)
}
}
if len(excludeRegexps) == 0 {
return
}
var ignoredTags []string
for _, s := range scenes {
var newTags []*models.ScrapedTag
for _, t := range s.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)
}
}
s.Tags = newTags
}
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) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeScene)
if err != nil {
return nil, err
@@ -60,30 +110,20 @@ func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*models
return nil, err
}
if ret != nil {
filterSceneTags([]*scraper.ScrapedScene{ret})
}
return ret, nil
}
func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*models.ScrapedGallery, error) {
func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*scraper.ScrapedGallery, error) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeGallery)
if err != nil {
return nil, err
}
ret, err := marshalScrapedGallery(content)
if err != nil {
return nil, err
}
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 marshalScrapedImage(content)
return marshalScrapedGallery(content)
}
func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models.ScrapedMovie, error) {
@@ -92,47 +132,23 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models
return nil, err
}
ret, err := marshalScrapedMovie(content)
if err != nil {
return nil, err
}
return ret, nil
return marshalScrapedMovie(content)
}
func (r *queryResolver) ScrapeGroupURL(ctx context.Context, url string) (*models.ScrapedGroup, error) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeGroup)
if err != nil {
return nil, err
func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) {
boxes := config.GetInstance().GetStashBoxes()
if index < 0 || index >= len(boxes) {
return nil, fmt.Errorf("%w: invalid stash_box_index %d", ErrInput, index)
}
ret, err := marshalScrapedGroup(content)
if err != nil {
return nil, err
}
// convert to scraped group
group := &models.ScrapedGroup{
StoredID: ret.StoredID,
Name: ret.Name,
Aliases: ret.Aliases,
Duration: ret.Duration,
Date: ret.Date,
Rating: ret.Rating,
Director: ret.Director,
URLs: ret.URLs,
Synopsis: ret.Synopsis,
Studio: ret.Studio,
Tags: ret.Tags,
FrontImage: ret.FrontImage,
BackImage: ret.BackImage,
}
return group, nil
return stashbox.NewClient(*boxes[index], r.stashboxRepository()), nil
}
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*models.ScrapedScene, error) {
var ret []*models.ScrapedScene
// FIXME - in the following resolvers, we're processing the deprecated field and not processing the new endpoint input
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*scraper.ScrapedScene, error) {
var ret []*scraper.ScrapedScene
var sceneID int
if input.SceneID != nil {
@@ -174,24 +190,17 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So
if err != nil {
return nil, err
}
case source.StashBoxIndex != nil || source.StashBoxEndpoint != nil:
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
case source.StashBoxIndex != nil:
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if err != nil {
return nil, err
}
client := r.newStashBoxClient(*b)
switch {
case input.SceneID != nil:
var fps []models.Fingerprints
fps, err = r.getScenesFingerprints(ctx, []int{sceneID})
if err != nil {
return nil, err
}
ret, err = client.FindSceneByFingerprints(ctx, fps[0])
ret, err = client.FindStashBoxSceneByFingerprints(ctx, sceneID)
case input.Query != nil:
ret, err = client.QueryScene(ctx, *input.Query)
ret, err = client.QueryStashBoxScene(ctx, *input.Query)
default:
return nil, fmt.Errorf("%w: scene_id or query must be set", ErrInput)
}
@@ -199,132 +208,44 @@ 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)
}
for i := range ret {
slices.SortFunc(ret[i].Tags, models.ScrapedTagSortFunction)
}
filterSceneTags(ret)
return ret, nil
}
func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.Source, input ScrapeMultiScenesInput) ([][]*models.ScrapedScene, error) {
func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.Source, input ScrapeMultiScenesInput) ([][]*scraper.ScrapedScene, error) {
if source.ScraperID != nil {
return nil, ErrNotImplemented
} else if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
} else if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if err != nil {
return nil, err
}
client := r.newStashBoxClient(*b)
sceneIDs, err := stringslice.StringSliceToIntSlice(input.SceneIds)
if err != nil {
return nil, err
}
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 client.FindStashBoxScenesByFingerprints(ctx, sceneIDs)
}
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)
if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if err != nil {
return nil, err
}
client := r.newStashBoxClient(*b)
var ret []*models.ScrapedStudio
out, err := client.FindStudio(ctx, *input.Query)
out, err := client.FindStashBoxStudio(ctx, *input.Query)
if err != nil {
return nil, err
@@ -333,17 +254,6 @@ 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
}
@@ -354,123 +264,74 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S
}
func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scraper.Source, input ScrapeSinglePerformerInput) ([]*models.ScrapedPerformer, error) {
var ret []*models.ScrapedPerformer
switch {
case source.ScraperID != nil:
switch {
case input.PerformerInput != nil:
if source.ScraperID != nil {
if input.PerformerInput != nil {
performer, err := r.scraperCache().ScrapeFragment(ctx, *source.ScraperID, scraper.Input{Performer: input.PerformerInput})
if err != nil {
return nil, err
}
ret, err = marshalScrapedPerformers([]scraper.ScrapedContent{performer})
if err != nil {
return nil, err
}
case input.Query != nil:
return marshalScrapedPerformers([]scraper.ScrapedContent{performer})
}
if input.Query != nil {
content, err := r.scraperCache().ScrapeName(ctx, *source.ScraperID, *input.Query, scraper.ScrapeContentTypePerformer)
if err != nil {
return nil, err
}
ret, err = marshalScrapedPerformers(content)
if err != nil {
return nil, err
}
default:
return nil, ErrNotImplemented
return marshalScrapedPerformers(content)
}
case source.StashBoxIndex != nil || source.StashBoxEndpoint != nil:
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
return nil, ErrNotImplemented
// FIXME - we're relying on a deprecated field and not processing the endpoint input
} else if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if err != nil {
return nil, err
}
client := r.newStashBoxClient(*b)
var query string
var ret []*stashbox.StashBoxPerformerQueryResult
switch {
case input.PerformerID != nil:
names, err := r.findPerformerNames(ctx, []string{*input.PerformerID})
if err != nil {
return nil, err
}
query = names[0]
ret, err = client.FindStashBoxPerformersByNames(ctx, []string{*input.PerformerID})
case input.Query != nil:
query = *input.Query
ret, err = client.QueryStashBoxPerformer(ctx, *input.Query)
default:
return nil, ErrNotImplemented
}
if query == "" {
return nil, nil
}
ret, err = client.QueryPerformer(ctx, query)
if err != nil {
return nil, err
}
default:
return nil, errors.New("scraper_id or stash_box_index must be set")
}
return ret, nil
}
func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source scraper.Source, input ScrapeMultiPerformersInput) ([][]*models.ScrapedPerformer, error) {
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
if len(ret) > 0 {
return ret[0].Results, nil
}
client := r.newStashBoxClient(*b)
return client.QueryPerformers(ctx, names)
return nil, nil
}
return nil, errors.New("scraper_id or stash_box_index must be set")
}
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)
func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source scraper.Source, input ScrapeMultiPerformersInput) ([][]*models.ScrapedPerformer, error) {
if source.ScraperID != nil {
return nil, ErrNotImplemented
} else if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if err != nil {
return err
return nil, err
}
for i, pp := range p {
names[i] = pp.Name
}
return nil
}); err != nil {
return nil, err
return client.FindStashBoxPerformersByPerformerNames(ctx, input.PerformerIds)
}
return names, nil
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) ([]*models.ScrapedGallery, error) {
var ret []*models.ScrapedGallery
if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.Source, input ScrapeSingleGalleryInput) ([]*scraper.ScrapedGallery, error) {
if source.StashBoxIndex != nil {
return nil, ErrNotSupported
}
@@ -490,54 +351,13 @@ func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.
if err != nil {
return nil, err
}
ret, err = marshalScrapedGalleries([]scraper.ScrapedContent{c})
if err != nil {
return nil, err
}
return marshalScrapedGalleries([]scraper.ScrapedContent{c})
case input.GalleryInput != nil:
c, err := r.scraperCache().ScrapeFragment(ctx, *source.ScraperID, scraper.Input{Gallery: input.GalleryInput})
if err != nil {
return nil, err
}
ret, err = marshalScrapedGalleries([]scraper.ScrapedContent{c})
if err != nil {
return nil, err
}
default:
return nil, ErrNotImplemented
}
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})
return marshalScrapedGalleries([]scraper.ScrapedContent{c})
default:
return nil, ErrNotImplemented
}
@@ -546,7 +366,3 @@ func (r *queryResolver) ScrapeSingleImage(ctx context.Context, source scraper.So
func (r *queryResolver) ScrapeSingleMovie(ctx context.Context, source scraper.Source, input ScrapeSingleMovieInput) ([]*models.ScrapedMovie, error) {
return nil, ErrNotSupported
}
func (r *queryResolver) ScrapeSingleGroup(ctx context.Context, source scraper.Source, input ScrapeSingleGroupInput) ([]*models.ScrapedGroup, error) {
return nil, ErrNotSupported
}

View File

@@ -1,159 +0,0 @@
package api
import (
"context"
"errors"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
type GalleryFinder interface {
models.GalleryGetter
FindByChecksum(ctx context.Context, checksum string) ([]*models.Gallery, error)
}
type GalleryImageFinder interface {
FindByGalleryIDIndex(ctx context.Context, galleryID int, index uint) (*models.Image, error)
image.Queryer
image.CoverQueryer
}
type galleryRoutes struct {
routes
imageRoutes imageRoutes
galleryFinder GalleryFinder
imageFinder GalleryImageFinder
fileGetter models.FileGetter
}
func (rs galleryRoutes) Routes() chi.Router {
r := chi.NewRouter()
r.Route("/{galleryId}", func(r chi.Router) {
r.Use(rs.GalleryCtx)
r.Get("/cover", rs.Cover)
r.Get("/preview/{imageIndex}", rs.Preview)
})
return r
}
func (rs galleryRoutes) Cover(w http.ResponseWriter, r *http.Request) {
g := r.Context().Value(galleryKey).(*models.Gallery)
var i *models.Image
_ = rs.withReadTxn(r, func(ctx context.Context) error {
// Find cover image first
i, _ = image.FindGalleryCover(ctx, rs.imageFinder, g.ID, config.GetInstance().GetGalleryCoverRegex())
if i == nil {
return nil
}
// serveThumbnail needs files populated
if err := i.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
if !errors.Is(err, context.Canceled) {
logger.Errorf("error loading primary file for image %d: %v", i.ID, err)
}
// set image to nil so that it doesn't try to use the primary file
i = nil
}
return nil
})
if i == nil {
// fallback to default image
image := static.ReadAll(static.DefaultGalleryImage)
utils.ServeImage(w, r, image)
return
}
rs.imageRoutes.serveThumbnail(w, r, i, &g.UpdatedAt)
}
func (rs galleryRoutes) Preview(w http.ResponseWriter, r *http.Request) {
g := r.Context().Value(galleryKey).(*models.Gallery)
indexQueryParam := chi.URLParam(r, "imageIndex")
var i *models.Image
index, err := strconv.Atoi(indexQueryParam)
if err != nil || index < 0 {
http.Error(w, "bad index", 400)
return
}
_ = rs.withReadTxn(r, func(ctx context.Context) error {
qb := rs.imageFinder
i, _ = qb.FindByGalleryIDIndex(ctx, g.ID, uint(index))
if i == nil {
return nil
}
// TODO - handle errors?
// serveThumbnail needs files populated
if err := i.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
if !errors.Is(err, context.Canceled) {
logger.Errorf("error loading primary file for image %d: %v", i.ID, err)
}
// set image to nil so that it doesn't try to use the primary file
i = nil
}
return nil
})
if i == nil {
http.Error(w, http.StatusText(404), 404)
return
}
rs.imageRoutes.serveThumbnail(w, r, i, nil)
}
func (rs galleryRoutes) GalleryCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
galleryIdentifierQueryParam := chi.URLParam(r, "galleryId")
galleryID, _ := strconv.Atoi(galleryIdentifierQueryParam)
var gallery *models.Gallery
_ = rs.withReadTxn(r, func(ctx context.Context) error {
qb := rs.galleryFinder
if galleryID == 0 {
galleries, _ := qb.FindByChecksum(ctx, galleryIdentifierQueryParam)
if len(galleries) > 0 {
gallery = galleries[0]
}
} else {
gallery, _ = qb.Find(ctx, galleryID)
}
if gallery != nil {
if err := gallery.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
if !errors.Is(err, context.Canceled) {
logger.Errorf("error loading primary file for gallery %d: %v", galleryID, err)
}
// set image to nil so that it doesn't try to use the primary file
gallery = nil
}
}
return nil
})
if gallery == nil {
http.Error(w, http.StatusText(404), 404)
return
}
ctx := context.WithValue(r.Context(), galleryKey, gallery)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -7,7 +7,6 @@ import (
"net/http"
"os/exec"
"strconv"
"time"
"github.com/go-chi/chi/v5"
@@ -47,22 +46,14 @@ func (rs imageRoutes) Routes() chi.Router {
}
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
img := r.Context().Value(imageKey).(*models.Image)
rs.serveThumbnail(w, r, img, nil)
}
func (rs imageRoutes) serveThumbnail(w http.ResponseWriter, r *http.Request, img *models.Image, modTime *time.Time) {
mgr := manager.GetInstance()
img := r.Context().Value(imageKey).(*models.Image)
filepath := mgr.Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
// if the thumbnail doesn't exist, encode on the fly
exists, _ := fsutil.FileExists(filepath)
if exists {
if modTime == nil {
utils.ServeStaticFile(w, r, filepath)
} else {
utils.ServeStaticFileModTime(w, r, filepath, *modTime)
}
utils.ServeStaticFile(w, r, filepath)
} else {
const useDefault = true

View File

@@ -14,22 +14,22 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
type GroupFinder interface {
models.GroupGetter
GetFrontImage(ctx context.Context, groupID int) ([]byte, error)
GetBackImage(ctx context.Context, groupID int) ([]byte, error)
type MovieFinder interface {
models.MovieGetter
GetFrontImage(ctx context.Context, movieID int) ([]byte, error)
GetBackImage(ctx context.Context, movieID int) ([]byte, error)
}
type groupRoutes struct {
type movieRoutes struct {
routes
groupFinder GroupFinder
movieFinder MovieFinder
}
func (rs groupRoutes) Routes() chi.Router {
func (rs movieRoutes) Routes() chi.Router {
r := chi.NewRouter()
r.Route("/{groupId}", func(r chi.Router) {
r.Use(rs.GroupCtx)
r.Route("/{movieId}", func(r chi.Router) {
r.Use(rs.MovieCtx)
r.Get("/frontimage", rs.FrontImage)
r.Get("/backimage", rs.BackImage)
})
@@ -37,77 +37,77 @@ func (rs groupRoutes) Routes() chi.Router {
return r
}
func (rs groupRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
group := r.Context().Value(groupKey).(*models.Group)
func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
movie := r.Context().Value(movieKey).(*models.Movie)
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
image, err = rs.groupFinder.GetFrontImage(ctx, group.ID)
image, err = rs.movieFinder.GetFrontImage(ctx, movie.ID)
return err
})
if errors.Is(readTxnErr, context.Canceled) {
return
}
if readTxnErr != nil {
logger.Warnf("read transaction error on fetch group front image: %v", readTxnErr)
logger.Warnf("read transaction error on fetch movie front image: %v", readTxnErr)
}
}
// fallback to default image
if len(image) == 0 {
image = static.ReadAll(static.DefaultGroupImage)
image = static.ReadAll(static.DefaultMovieImage)
}
utils.ServeImage(w, r, image)
}
func (rs groupRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
group := r.Context().Value(groupKey).(*models.Group)
func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
movie := r.Context().Value(movieKey).(*models.Movie)
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
image, err = rs.groupFinder.GetBackImage(ctx, group.ID)
image, err = rs.movieFinder.GetBackImage(ctx, movie.ID)
return err
})
if errors.Is(readTxnErr, context.Canceled) {
return
}
if readTxnErr != nil {
logger.Warnf("read transaction error on fetch group back image: %v", readTxnErr)
logger.Warnf("read transaction error on fetch movie back image: %v", readTxnErr)
}
}
// fallback to default image
if len(image) == 0 {
image = static.ReadAll(static.DefaultGroupImage)
image = static.ReadAll(static.DefaultMovieImage)
}
utils.ServeImage(w, r, image)
}
func (rs groupRoutes) GroupCtx(next http.Handler) http.Handler {
func (rs movieRoutes) MovieCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
groupID, err := strconv.Atoi(chi.URLParam(r, "groupId"))
movieID, err := strconv.Atoi(chi.URLParam(r, "movieId"))
if err != nil {
http.Error(w, http.StatusText(404), 404)
return
}
var group *models.Group
var movie *models.Movie
_ = rs.withReadTxn(r, func(ctx context.Context) error {
group, _ = rs.groupFinder.Find(ctx, groupID)
movie, _ = rs.movieFinder.Find(ctx, movieID)
return nil
})
if group == nil {
if movie == nil {
http.Error(w, http.StatusText(404), 404)
return
}
ctx := context.WithValue(r.Context(), groupKey, group)
ctx := context.WithValue(r.Context(), movieKey, movie)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -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) ([]*models.ScrapedScene, error) {
var ret []*models.ScrapedScene
func marshalScrapedScenes(content []scraper.ScrapedContent) ([]*scraper.ScrapedScene, error) {
var ret []*scraper.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) ([]*models.ScrapedSc
}
switch s := c.(type) {
case *models.ScrapedScene:
case *scraper.ScrapedScene:
ret = append(ret, s)
case models.ScrapedScene:
case scraper.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) ([]*models.ScrapedGallery, error) {
var ret []*models.ScrapedGallery
func marshalScrapedGalleries(content []scraper.ScrapedContent) ([]*scraper.ScrapedGallery, error) {
var ret []*scraper.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) ([]*models.Scrape
}
switch g := c.(type) {
case *models.ScrapedGallery:
case *scraper.ScrapedGallery:
ret = append(ret, g)
case models.ScrapedGallery:
case scraper.ScrapedGallery:
ret = append(ret, &g)
default:
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedGallery", models.ErrConversion)
@@ -76,27 +76,6 @@ func marshalScrapedGalleries(content []scraper.ScrapedContent) ([]*models.Scrape
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) {
@@ -113,30 +92,7 @@ func marshalScrapedMovies(content []scraper.ScrapedContent) ([]*models.ScrapedMo
case models.ScrapedMovie:
ret = append(ret, &m)
default:
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedMovie", models.ErrConversion)
}
}
return ret, nil
}
// marshalScrapedMovies converts ScrapedContent into ScrapedMovie. If conversion
// fails, an error is returned.
func marshalScrapedGroups(content []scraper.ScrapedContent) ([]*models.ScrapedGroup, error) {
var ret []*models.ScrapedGroup
for _, c := range content {
if c == nil {
// graphql schema requires groups to be non-nil
continue
}
switch m := c.(type) {
case *models.ScrapedGroup:
ret = append(ret, m)
case models.ScrapedGroup:
ret = append(ret, &m)
default:
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedGroup", models.ErrConversion)
return nil, fmt.Errorf("%w: cannot turn ScrapedConetnt into ScrapedMovie", models.ErrConversion)
}
}
@@ -154,7 +110,7 @@ func marshalScrapedPerformer(content scraper.ScrapedContent) (*models.ScrapedPer
}
// marshalScrapedScene will marshal a single scraped scene
func marshalScrapedScene(content scraper.ScrapedContent) (*models.ScrapedScene, error) {
func marshalScrapedScene(content scraper.ScrapedContent) (*scraper.ScrapedScene, error) {
s, err := marshalScrapedScenes([]scraper.ScrapedContent{content})
if err != nil {
return nil, err
@@ -164,7 +120,7 @@ func marshalScrapedScene(content scraper.ScrapedContent) (*models.ScrapedScene,
}
// marshalScrapedGallery will marshal a single scraped gallery
func marshalScrapedGallery(content scraper.ScrapedContent) (*models.ScrapedGallery, error) {
func marshalScrapedGallery(content scraper.ScrapedContent) (*scraper.ScrapedGallery, error) {
g, err := marshalScrapedGalleries([]scraper.ScrapedContent{content})
if err != nil {
return nil, err
@@ -173,16 +129,6 @@ func marshalScrapedGallery(content scraper.ScrapedContent) (*models.ScrapedGalle
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})
@@ -192,13 +138,3 @@ func marshalScrapedMovie(content scraper.ScrapedContent) (*models.ScrapedMovie,
return m[0], nil
}
// marshalScrapedMovie will marshal a single scraped movie
func marshalScrapedGroup(content scraper.ScrapedContent) (*models.ScrapedGroup, error) {
m, err := marshalScrapedGroups([]scraper.ScrapedContent{content})
if err != nil {
return nil, err
}
return m[0], nil
}

View File

@@ -27,7 +27,6 @@ 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"
@@ -41,11 +40,10 @@ import (
)
const (
loginEndpoint = "/login"
loginLocaleEndpoint = loginEndpoint + "/locale"
logoutEndpoint = "/logout"
gqlEndpoint = "/graphql"
playgroundEndpoint = "/playground"
loginEndpoint = "/login"
logoutEndpoint = "/logout"
gqlEndpoint = "/graphql"
playgroundEndpoint = "/playground"
)
type Server struct {
@@ -55,30 +53,7 @@ type Server struct {
manager *manager.Manager
}
// TODO - os.DirFS doesn't implement ReadDir, so re-implement it here
// This can be removed when we upgrade go
type osFS string
func (dir osFS) ReadDir(name string) ([]os.DirEntry, error) {
fullname := string(dir) + "/" + name
entries, err := os.ReadDir(fullname)
if err != nil {
var e *os.PathError
if errors.As(err, &e) {
// See comment in dirFS.Open.
e.Path = name
}
return nil, err
}
return entries, nil
}
func (dir osFS) Open(name string) (fs.File, error) {
return os.DirFS(string(dir)).Open(name)
}
// Initialize creates a new [Server] instance.
// It assumes that the [manager.Manager] instance has been initialised.
// Called at startup
func Initialize() (*Server, error) {
mgr := manager.GetInstance()
cfg := mgr.Config
@@ -160,13 +135,11 @@ func Initialize() (*Server, error) {
sceneService := mgr.SceneService
imageService := mgr.ImageService
galleryService := mgr.GalleryService
groupService := mgr.GroupService
resolver := &Resolver{
repository: repo,
sceneService: sceneService,
imageService: imageService,
galleryService: galleryService,
groupService: groupService,
hookExecutor: pluginCache,
}
@@ -187,7 +160,7 @@ func Initialize() (*Server, error) {
MaxUploadSize: cfg.GetMaxUploadSize(),
})
gqlSrv.SetQueryCache(gqlLru.New[*ast.QueryDocument](1000))
gqlSrv.SetQueryCache(gqlLru.New(1000))
gqlSrv.Use(gqlExtension.Introspection{})
gqlSrv.SetErrorPresenter(gqlErrorHandler)
@@ -212,10 +185,9 @@ func Initialize() (*Server, error) {
r.Mount("/performer", server.getPerformerRoutes())
r.Mount("/scene", server.getSceneRoutes())
r.Mount("/gallery", server.getGalleryRoutes())
r.Mount("/image", server.getImageRoutes())
r.Mount("/studio", server.getStudioRoutes())
r.Mount("/group", server.getGroupRoutes())
r.Mount("/movie", server.getMovieRoutes())
r.Mount("/tag", server.getTagRoutes())
r.Mount("/downloads", server.getDownloadsRoutes())
r.Mount("/plugin", server.getPluginRoutes())
@@ -229,7 +201,6 @@ 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")
@@ -242,31 +213,25 @@ func Initialize() (*Server, error) {
r.Mount("/custom", getCustomRoutes(customServedFolders))
}
var uiFS fs.FS
var staticUI *statigz.Server
customUILocation := cfg.GetUILocation()
if customUILocation != "" {
logger.Debugf("Serving UI from %s", customUILocation)
uiFS = osFS(customUILocation)
staticUI = statigz.FileServer(uiFS.(fs.ReadDirFS))
} else {
logger.Debug("Serving embedded UI")
uiFS = ui.UIBox
staticUI = statigz.FileServer(ui.UIBox.(fs.ReadDirFS))
}
customUILocation := cfg.GetCustomUILocation()
staticUI := statigz.FileServer(ui.UIBox.(fs.ReadDirFS))
// Serve the web app
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
ext := path.Ext(r.URL.Path)
if ext == ".html" || ext == "" {
w.Header().Set("Content-Type", "text/html")
setPageSecurityHeaders(w, r, pluginCache.ListPlugins())
if customUILocation != "" {
if r.URL.Path == "index.html" || ext == "" {
r.URL.Path = "/"
}
http.FileServer(http.Dir(customUILocation)).ServeHTTP(w, r)
return
}
if ext == "" || r.URL.Path == "/" || r.URL.Path == "/index.html" {
if ext == ".html" || ext == "" {
themeColor := cfg.GetThemeColor()
data, err := fs.ReadFile(uiFS, "index.html")
data, err := fs.ReadFile(ui.UIBox, "index.html")
if err != nil {
panic(err)
}
@@ -276,6 +241,9 @@ func Initialize() (*Server, error) {
indexHtml = strings.ReplaceAll(indexHtml, "%COLOR%", themeColor)
indexHtml = strings.Replace(indexHtml, `<base href="/"`, fmt.Sprintf(`<base href="%s/"`, prefix), 1)
w.Header().Set("Content-Type", "text/html")
setPageSecurityHeaders(w, r, pluginCache.ListPlugins())
utils.ServeStaticContent(w, r, []byte(indexHtml))
} else {
isStatic, _ := path.Match("/assets/*", r.URL.Path)
@@ -295,9 +263,6 @@ func Initialize() (*Server, error) {
return server, nil
}
// Start starts the server. It listens on the configured address and port.
// It calls ListenAndServeTLS if TLS is configured, otherwise it calls ListenAndServe.
// Calls to Start are blocked until the server is shutdown.
func (s *Server) Start() error {
logger.Infof("stash is listening on " + s.Addr)
logger.Infof("stash is running at " + s.displayAddress)
@@ -309,7 +274,6 @@ func (s *Server) Start() error {
}
}
// Shutdown gracefully shuts down the server without interrupting any active connections.
func (s *Server) Shutdown() {
err := s.Server.Shutdown(context.TODO())
if err != nil {
@@ -337,16 +301,6 @@ func (s *Server) getSceneRoutes() chi.Router {
}.Routes()
}
func (s *Server) getGalleryRoutes() chi.Router {
repo := s.manager.Repository
return galleryRoutes{
routes: routes{txnManager: repo.TxnManager},
imageFinder: repo.Image,
galleryFinder: repo.Gallery,
fileGetter: repo.File,
}.Routes()
}
func (s *Server) getImageRoutes() chi.Router {
repo := s.manager.Repository
return imageRoutes{
@@ -364,11 +318,11 @@ func (s *Server) getStudioRoutes() chi.Router {
}.Routes()
}
func (s *Server) getGroupRoutes() chi.Router {
func (s *Server) getMovieRoutes() chi.Router {
repo := s.manager.Repository
return groupRoutes{
return movieRoutes{
routes: routes{txnManager: repo.TxnManager},
groupFinder: repo.Group,
movieFinder: repo.Movie,
}.Routes()
}

View File

@@ -17,11 +17,7 @@ import (
"github.com/stashapp/stash/ui"
)
const (
returnURLParam = "returnURL"
defaultLocale = "en-GB"
)
const returnURLParam = "returnURL"
func getLoginPage() []byte {
data, err := fs.ReadFile(ui.LoginUIBox, "login.html")
@@ -62,47 +58,6 @@ 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)
@@ -123,26 +78,31 @@ 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 from IP: %s", err, r.RemoteAddr)
logger.Errorf("Error logging in: %v", err)
}
var invalidCredentialsError *session.InvalidCredentialsError
if errors.As(err, &invalidCredentialsError) {
http.Error(w, "Username or password is invalid", http.StatusUnauthorized)
// serve login page with an error
serveLoginPage(w, r, url, "Username or password is invalid")
return
}
if err != nil {
// don't expose the error to the user
http.Error(w, "An unexpected error occurred. See logs", http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
http.Redirect(w, r, url, http.StatusFound)
}
}

View File

@@ -1,46 +0,0 @@
package api
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/stashbox"
)
func (r *Resolver) newStashBoxClient(box models.StashBox) *stashbox.Client {
return stashbox.NewClient(box, stashbox.ExcludeTagPatterns(manager.GetInstance().Config.GetScraperExcludeTagPatterns()))
}
func resolveStashBoxFn(indexField, endpointField string) func(index *int, endpoint *string) (*models.StashBox, error) {
return func(index *int, endpoint *string) (*models.StashBox, error) {
boxes := config.GetInstance().GetStashBoxes()
// prefer endpoint over index
if endpoint != nil {
for _, box := range boxes {
if strings.EqualFold(*endpoint, box.Endpoint) {
return box, nil
}
}
return nil, fmt.Errorf("stash box not found")
}
if index != nil {
if *index < 0 || *index >= len(boxes) {
return nil, fmt.Errorf("invalid %s %d", indexField, index)
}
return boxes[*index], nil
}
return nil, fmt.Errorf("%s not provided", endpointField)
}
}
var (
resolveStashBox = resolveStashBoxFn("stash_box_index", "stash_box_endpoint")
resolveStashBoxBatchTagInput = resolveStashBoxFn("endpoint", "stash_box_endpoint")
)

View File

@@ -1,2 +0,0 @@
// Package urlbuilders provides the builders used to build URLs to pass to clients.
package urlbuilders

View File

@@ -1,27 +0,0 @@
package urlbuilders
import (
"strconv"
"github.com/stashapp/stash/pkg/models"
)
type GalleryURLBuilder struct {
BaseURL string
GalleryID string
}
func NewGalleryURLBuilder(baseURL string, gallery *models.Gallery) GalleryURLBuilder {
return GalleryURLBuilder{
BaseURL: baseURL,
GalleryID: strconv.Itoa(gallery.ID),
}
}
func (b GalleryURLBuilder) GetPreviewURL() string {
return b.BaseURL + "/gallery/" + b.GalleryID + "/preview"
}
func (b GalleryURLBuilder) GetCoverURL() string {
return b.BaseURL + "/gallery/" + b.GalleryID + "/cover"
}

View File

@@ -1,33 +0,0 @@
package urlbuilders
import (
"strconv"
"github.com/stashapp/stash/pkg/models"
)
type GroupURLBuilder struct {
BaseURL string
GroupID string
UpdatedAt string
}
func NewGroupURLBuilder(baseURL string, group *models.Group) GroupURLBuilder {
return GroupURLBuilder{
BaseURL: baseURL,
GroupID: strconv.Itoa(group.ID),
UpdatedAt: strconv.FormatInt(group.UpdatedAt.Unix(), 10),
}
}
func (b GroupURLBuilder) GetGroupFrontImageURL(hasImage bool) string {
url := b.BaseURL + "/group/" + b.GroupID + "/frontimage?t=" + b.UpdatedAt
if !hasImage {
url += "&default=true"
}
return url
}
func (b GroupURLBuilder) GetGroupBackImageURL() string {
return b.BaseURL + "/group/" + b.GroupID + "/backimage?t=" + b.UpdatedAt
}

View File

@@ -0,0 +1,32 @@
package urlbuilders
import (
"github.com/stashapp/stash/pkg/models"
"strconv"
)
type MovieURLBuilder struct {
BaseURL string
MovieID string
UpdatedAt string
}
func NewMovieURLBuilder(baseURL string, movie *models.Movie) MovieURLBuilder {
return MovieURLBuilder{
BaseURL: baseURL,
MovieID: strconv.Itoa(movie.ID),
UpdatedAt: strconv.FormatInt(movie.UpdatedAt.Unix(), 10),
}
}
func (b MovieURLBuilder) GetMovieFrontImageURL(hasImage bool) string {
url := b.BaseURL + "/movie/" + b.MovieID + "/frontimage?t=" + b.UpdatedAt
if !hasImage {
url += "&default=true"
}
return url
}
func (b MovieURLBuilder) GetMovieBackImageURL() string {
return b.BaseURL + "/movie/" + b.MovieID + "/backimage?t=" + b.UpdatedAt
}

View File

@@ -1,9 +0,0 @@
// Package autotag provides the autotagging functionality for the application.
//
// The autotag functionality sets media metadata based on the media's path.
// The functions in this package are in the form of {ObjectType}{TagTypes},
// where the ObjectType is the single object instance to run on, and TagTypes
// are the related types.
// For example, PerformerScenes finds and tags scenes with a provided performer,
// whereas ScenePerformers tags a single scene with any Performers that match.
package autotag

View File

@@ -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 slices.Contains(existing, otherID) {
if sliceutil.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 slices.Contains(existing, otherID) {
if sliceutil.Contains(existing, otherID) {
return false, nil
}

View File

@@ -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 slices.Contains(existing, otherID) {
if sliceutil.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 slices.Contains(existing, otherID) {
if sliceutil.Contains(existing, otherID) {
return false, nil
}

View File

@@ -91,7 +91,7 @@ func createPerformer(ctx context.Context, pqb models.PerformerWriter) error {
Name: testName,
}
err := pqb.Create(ctx, &models.CreatePerformerInput{Performer: &performer})
err := pqb.Create(ctx, &performer)
if err != nil {
return err
}

View File

@@ -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 slices.Contains(existing, p.ID) {
if sliceutil.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 slices.Contains(existing, p.ID) {
if sliceutil.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 slices.Contains(existing, p.ID) {
if sliceutil.Contains(existing, p.ID) {
return false, nil
}

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