mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
286 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a649f0268 | ||
|
|
0b19a00ba8 | ||
|
|
5e332514fa | ||
|
|
646f8bc02e | ||
|
|
9350be12d9 | ||
|
|
02ec98b302 | ||
|
|
a8f4c2c29c | ||
|
|
57ad12e43b | ||
|
|
f0a3a3dd44 | ||
|
|
27998c35a1 | ||
|
|
03cd9529bd | ||
|
|
3072333118 | ||
|
|
821587b166 | ||
|
|
b175f1865f | ||
|
|
5a2242e78d | ||
|
|
54c495d867 | ||
|
|
e84221ccbe | ||
|
|
8b59a3b014 | ||
|
|
f76a440e54 | ||
|
|
aafbba7d77 | ||
|
|
7bb35b2b09 | ||
|
|
af28fd0f3b | ||
|
|
abc9ec648a | ||
|
|
045ba55def | ||
|
|
0664c5b974 | ||
|
|
f39fa416a9 | ||
|
|
420c6fa9d7 | ||
|
|
b1c00a64fc | ||
|
|
4ff163d375 | ||
|
|
c1f271fc52 | ||
|
|
2cce547986 | ||
|
|
f0bf780c2e | ||
|
|
3660bf2d1a | ||
|
|
ca9c8e0a34 | ||
|
|
3a63f1f9b7 | ||
|
|
ffca8f0c0f | ||
|
|
0443439fae | ||
|
|
dc820e29af | ||
|
|
7eae751d1c | ||
|
|
f66333bac9 | ||
|
|
ce17230c13 | ||
|
|
4a054ab081 | ||
|
|
d0b0be4dd4 | ||
|
|
9df66024d1 | ||
|
|
c83ebf7c1c | ||
|
|
eb795ff9ab | ||
|
|
a2ca266cb3 | ||
|
|
3bc0de3f3a | ||
|
|
30a7482ddf | ||
|
|
04514fbc88 | ||
|
|
f1d57c3d62 | ||
|
|
404a68c994 | ||
|
|
d2743cf5fb | ||
|
|
b9e07ade92 | ||
|
|
962bc7df4e | ||
|
|
f1c454eb09 | ||
|
|
eff86bf2f8 | ||
|
|
7540d3b477 | ||
|
|
653db3cc1d | ||
|
|
2609095c7a | ||
|
|
bba7c23957 | ||
|
|
f25881a3bf | ||
|
|
270bc317cb | ||
|
|
b1fa933868 | ||
|
|
2cd9ef6302 | ||
|
|
db29246883 | ||
|
|
7b7d6758ef | ||
|
|
1c0042c4c2 | ||
|
|
a60afc162f | ||
|
|
2b80b6d8d5 | ||
|
|
3ac3fe09b8 | ||
|
|
02c2ad3f58 | ||
|
|
479ebfc88d | ||
|
|
1c92336798 | ||
|
|
5fae3cf127 | ||
|
|
47395ce13f | ||
|
|
091950615e | ||
|
|
4db0e48f73 | ||
|
|
33de28ce5d | ||
|
|
b8a8909a8e | ||
|
|
0cf06728d4 | ||
|
|
3acece2438 | ||
|
|
7104bb67ca | ||
|
|
c4c6a3f9c0 | ||
|
|
86b52fe938 | ||
|
|
a64e0929d4 | ||
|
|
4c286d7ab5 | ||
|
|
bd44571a91 | ||
|
|
396c1ffc6d | ||
|
|
6dcb1279a7 | ||
|
|
5e1948516d | ||
|
|
99bbd157d6 | ||
|
|
6488a4236e | ||
|
|
a6fd577f03 | ||
|
|
6b5d5cc628 | ||
|
|
e3cd36f25f | ||
|
|
68a1547e8b | ||
|
|
9bff498c28 | ||
|
|
6ce409cd56 | ||
|
|
b1193227d0 | ||
|
|
90fdc6b322 | ||
|
|
043b67e076 | ||
|
|
6c04f9199f | ||
|
|
351dcb708b | ||
|
|
bb250d1232 | ||
|
|
0e0d201ff3 | ||
|
|
e96a09d9fd | ||
|
|
ef9e138a2d | ||
|
|
83359b00d5 | ||
|
|
9083796a42 | ||
|
|
b160c3bb97 | ||
|
|
55001ddcf1 | ||
|
|
4c73f2f845 | ||
|
|
480ae46dde | ||
|
|
6ba9f55df0 | ||
|
|
88bfda1980 | ||
|
|
060ac00fc7 | ||
|
|
7b83d81820 | ||
|
|
9e44e13f6d | ||
|
|
51f4dd8a59 | ||
|
|
30f7a05ebf | ||
|
|
8f594e7fed | ||
|
|
c8cbb36fd5 | ||
|
|
554448594c | ||
|
|
6c6e0b6236 | ||
|
|
b8b62a36c6 | ||
|
|
b588597f3e | ||
|
|
c63c06de1c | ||
|
|
25bc750295 | ||
|
|
d274f86390 | ||
|
|
ad7fbce5f7 | ||
|
|
dce90a3ed9 | ||
|
|
00820a8789 | ||
|
|
d4e706daef | ||
|
|
0848b02e93 | ||
|
|
4089a5fccc | ||
|
|
74191c73ed | ||
|
|
c10d53ba8e | ||
|
|
10655586b0 | ||
|
|
b74428cb42 | ||
|
|
cffcd9f4b8 | ||
|
|
3fa7b470e7 | ||
|
|
98e3610ade | ||
|
|
8efbcc1c4d | ||
|
|
5e97ecd260 | ||
|
|
1207629a76 | ||
|
|
2564351265 | ||
|
|
0359ce2ed8 | ||
|
|
648247aa00 | ||
|
|
90726086e5 | ||
|
|
7a75313a1c | ||
|
|
593477cbe1 | ||
|
|
781a767fb6 | ||
|
|
d558902dfb | ||
|
|
7f5f1c7e0d | ||
|
|
e6b7d40784 | ||
|
|
5db42f4882 | ||
|
|
df9c7594c7 | ||
|
|
2368269e63 | ||
|
|
32911367b0 | ||
|
|
c43c695f5d | ||
|
|
8b79eaca67 | ||
|
|
5c383da5ec | ||
|
|
cfc8222b9a | ||
|
|
9e08edc76f | ||
|
|
cc9fc2150e | ||
|
|
13bdba5b24 | ||
|
|
0c513a604d | ||
|
|
276f14cdcb | ||
|
|
6b0bcdea88 | ||
|
|
7159ab69a3 | ||
|
|
94d39da706 | ||
|
|
273cf0383d | ||
|
|
0b534d89c6 | ||
|
|
1222b7b87b | ||
|
|
7b439556c0 | ||
|
|
0c7b5cf6a1 | ||
|
|
f4825fadf4 | ||
|
|
00608c167a | ||
|
|
9b31b20fed | ||
|
|
87167736f6 | ||
|
|
569c3a872a | ||
|
|
c825cf5d09 | ||
|
|
5843fdcecc | ||
|
|
bc47932343 | ||
|
|
abb574205a | ||
|
|
461068462c | ||
|
|
5495d72849 | ||
|
|
30877c75fb | ||
|
|
964b559309 | ||
|
|
7b5bd80515 | ||
|
|
9dcf03eb70 | ||
|
|
628afce516 | ||
|
|
f5f4cbef1e | ||
|
|
c387550c63 | ||
|
|
30879389ec | ||
|
|
90baf7a469 | ||
|
|
10bc2c6689 | ||
|
|
cba0fddf61 | ||
|
|
8e222ae387 | ||
|
|
7f86509674 | ||
|
|
4b7ef76321 | ||
|
|
c21c334553 | ||
|
|
db3138b33f | ||
|
|
e532f9a683 | ||
|
|
b8262f5641 | ||
|
|
6f7cc11b86 | ||
|
|
9ac5d3f3d9 | ||
|
|
11d5c7f386 | ||
|
|
2427519100 | ||
|
|
91e3fcc7ff | ||
|
|
4fa103dd08 | ||
|
|
3a3a296995 | ||
|
|
351472f654 | ||
|
|
92acd78401 | ||
|
|
813495c7f7 | ||
|
|
d32e375521 | ||
|
|
b3bc5b999f | ||
|
|
f3e6cb7b0e | ||
|
|
5e7bf1c2d7 | ||
|
|
63e1bbf35d | ||
|
|
3b4b20e9b2 | ||
|
|
33b68b4464 | ||
|
|
1ab02a1748 | ||
|
|
6cfb7fe79d | ||
|
|
abd76f7e58 | ||
|
|
733ca2aa6f | ||
|
|
c2f7617952 | ||
|
|
75a795b2e6 | ||
|
|
6029918d22 | ||
|
|
582ffa1420 | ||
|
|
900ba936a9 | ||
|
|
a2e8f69028 | ||
|
|
9264c15540 | ||
|
|
ff724d82cc | ||
|
|
456e9409e0 | ||
|
|
5e455d6530 | ||
|
|
3d52bad9bd | ||
|
|
803d865348 | ||
|
|
8a1c349976 | ||
|
|
d68d022893 | ||
|
|
e51083c26d | ||
|
|
49f579e08e | ||
|
|
1c18ec1501 | ||
|
|
88111e4064 | ||
|
|
ba2979096a | ||
|
|
34eb624438 | ||
|
|
a142ec223e | ||
|
|
b2ac022357 | ||
|
|
2653342ae1 | ||
|
|
df24f90735 | ||
|
|
1200d4472a | ||
|
|
55366fa66f | ||
|
|
c232372f0c | ||
|
|
53c6ae4ea5 | ||
|
|
dce4591911 | ||
|
|
31cb8e2cbd | ||
|
|
ea2fcd9d7f | ||
|
|
bc85614ff9 | ||
|
|
1e7b85fbe5 | ||
|
|
9ca3874707 | ||
|
|
73ded0d97d | ||
|
|
c1a096a1a6 | ||
|
|
ab1b30ffb7 | ||
|
|
00e98a9c09 | ||
|
|
d77f47824c | ||
|
|
727644fab3 | ||
|
|
dcd7595c07 | ||
|
|
ce175dcfc6 | ||
|
|
1964481ff2 | ||
|
|
a8456dd188 | ||
|
|
a6f15273d9 | ||
|
|
36aa51a187 | ||
|
|
1a6f5619ae | ||
|
|
21a26fadd8 | ||
|
|
20ffd4d51d | ||
|
|
e87fd516d6 | ||
|
|
0c2dc17e8e | ||
|
|
1b91937004 | ||
|
|
9e606feb76 | ||
|
|
340f47cda8 | ||
|
|
aacf07feef | ||
|
|
cdaa191155 | ||
|
|
401660e6a3 | ||
|
|
a7beeb32b0 | ||
|
|
50e83a3555 |
17
.github/workflows/build.yml
vendored
17
.github/workflows/build.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
@@ -13,7 +12,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:6
|
||||
COMPILER_IMAGE: stashapp/compiler:7
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -28,7 +27,7 @@ jobs:
|
||||
run: docker pull $COMPILER_IMAGE
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node_modules
|
||||
with:
|
||||
@@ -36,7 +35,7 @@ jobs:
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock') }}
|
||||
|
||||
- name: Cache UI build
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
id: cache-ui
|
||||
env:
|
||||
cache-name: cache-ui
|
||||
@@ -45,7 +44,7 @@ jobs:
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock', 'ui/v2.5/public/**', 'ui/v2.5/src/**', 'graphql/**/*.graphql') }}
|
||||
|
||||
- name: Cache go build
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
# increment the number suffix to bump the cache
|
||||
cache-name: cache-go-cache-1
|
||||
@@ -150,7 +149,9 @@ jobs:
|
||||
CHECKSUMS_SHA1
|
||||
|
||||
- name: Master release
|
||||
if: ${{ github.event_name == 'release' && github.ref != 'refs/tags/latest_develop' }}
|
||||
# NOTE: this isn't perfect, but should cover most scenarios
|
||||
# DON'T create tag names starting with "v" if they are not stable releases
|
||||
if: ${{ github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
uses: WithoutPants/github-release@v2.0.4
|
||||
with:
|
||||
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -181,7 +182,9 @@ jobs:
|
||||
bash ./docker/ci/x86_64/docker_push.sh development
|
||||
|
||||
- name: Release Docker
|
||||
if: ${{ github.repository == 'stashapp/stash' && github.event_name == 'release' && github.ref != 'refs/tags/latest_develop' }}
|
||||
# NOTE: this isn't perfect, but should cover most scenarios
|
||||
# DON'T create tag names starting with "v" if they are not stable releases
|
||||
if: ${{ github.repository == 'stashapp/stash' && github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
||||
6
.github/workflows/golangci-lint.yml
vendored
6
.github/workflows/golangci-lint.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:6
|
||||
COMPILER_IMAGE: stashapp/compiler:7
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
@@ -36,13 +36,13 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
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: v1.45.2
|
||||
version: latest
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
args: --modules-download-mode=vendor --timeout=3m
|
||||
args: --modules-download-mode=vendor --timeout=5m
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -16,13 +16,19 @@
|
||||
*.out
|
||||
|
||||
# GraphQL generated output
|
||||
pkg/models/generated_*.go
|
||||
internal/api/generated_*.go
|
||||
ui/v2.5/src/core/generated-*.tsx
|
||||
|
||||
####
|
||||
# Jetbrains
|
||||
####
|
||||
|
||||
|
||||
####
|
||||
# Visual Studio
|
||||
####
|
||||
/.vs
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
@@ -57,4 +63,4 @@ node_modules
|
||||
|
||||
/stash
|
||||
dist
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
@@ -1,26 +1,23 @@
|
||||
# options for analysis running
|
||||
run:
|
||||
timeout: 3m
|
||||
timeout: 5m
|
||||
modules-download-mode: vendor
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
# Default set of linters from golangci-lint
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- varcheck
|
||||
# Linters added by the stash project.
|
||||
# - contextcheck
|
||||
- dogsled
|
||||
# - errchkjson
|
||||
- errchkjson
|
||||
- errorlint
|
||||
# - exhaustive
|
||||
- exportloopref
|
||||
|
||||
@@ -7,6 +7,8 @@ client:
|
||||
models:
|
||||
Date:
|
||||
model: github.com/99designs/gqlgen/graphql.String
|
||||
SceneDraftInput:
|
||||
model: github.com/stashapp/stash/pkg/scraper/stashbox/graphql.SceneDraftInput
|
||||
endpoint:
|
||||
# This points to stashdb.org currently, but can be directed at any stash-box
|
||||
# instance. It is used for generation only.
|
||||
|
||||
41
Makefile
41
Makefile
@@ -14,11 +14,6 @@ else
|
||||
SET := export
|
||||
endif
|
||||
|
||||
IS_WIN_OS =
|
||||
ifeq ($(OS),Windows_NT)
|
||||
IS_WIN_OS = true
|
||||
endif
|
||||
|
||||
# set LDFLAGS environment variable to any extra ldflags required
|
||||
# set OUTPUT to generate a specific binary name
|
||||
|
||||
@@ -29,9 +24,14 @@ endif
|
||||
|
||||
export CGO_ENABLED = 1
|
||||
|
||||
# including netgo causes name resolution to go through the Go resolver
|
||||
# and isn't necessary for static builds on Windows
|
||||
GO_BUILD_TAGS_WINDOWS := sqlite_omit_load_extension sqlite_stat4 osusergo
|
||||
GO_BUILD_TAGS_DEFAULT = $(GO_BUILD_TAGS_WINDOWS) netgo
|
||||
|
||||
.PHONY: release pre-build
|
||||
|
||||
release: generate ui build-release
|
||||
release: pre-ui generate ui build-release
|
||||
|
||||
pre-build:
|
||||
ifndef BUILD_DATE
|
||||
@@ -47,20 +47,21 @@ ifndef STASH_VERSION
|
||||
endif
|
||||
|
||||
ifndef OFFICIAL_BUILD
|
||||
$(eval OFFICIAL_BUILD := false)
|
||||
$(eval OFFICIAL_BUILD := false)
|
||||
endif
|
||||
|
||||
ifdef IS_WIN_OS
|
||||
ifndef SUPPRESS_WINDOWSGUI
|
||||
PLATFORM_SPECIFIC_LDFLAGS := -H windowsgui
|
||||
endif
|
||||
ifndef GO_BUILD_TAGS
|
||||
$(eval GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT))
|
||||
endif
|
||||
|
||||
|
||||
# NOTE: the build target still includes netgo because we cannot detect
|
||||
# Windows easily from the Makefile.
|
||||
build: pre-build
|
||||
build:
|
||||
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/internal/api.version=$(STASH_VERSION)' -X 'github.com/stashapp/stash/internal/api.buildstamp=$(BUILD_DATE)' -X 'github.com/stashapp/stash/internal/api.githash=$(GITHASH)')
|
||||
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/internal/manager/config.officialBuild=$(OFFICIAL_BUILD)')
|
||||
go build $(OUTPUT) -mod=vendor -v -tags "sqlite_omit_load_extension osusergo netgo" $(GO_BUILD_FLAGS) -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS) $(PLATFORM_SPECIFIC_LDFLAGS)" ./cmd/stash
|
||||
go build $(OUTPUT) -mod=vendor -v -tags "$(GO_BUILD_TAGS)" $(GO_BUILD_FLAGS) -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS) $(PLATFORM_SPECIFIC_LDFLAGS)" ./cmd/stash
|
||||
|
||||
# strips debug symbols from the release build
|
||||
build-release: EXTRA_LDFLAGS := -s -w
|
||||
@@ -77,7 +78,7 @@ cross-compile-windows: export GOARCH := amd64
|
||||
cross-compile-windows: export CC := x86_64-w64-mingw32-gcc
|
||||
cross-compile-windows: export CXX := x86_64-w64-mingw32-g++
|
||||
cross-compile-windows: OUTPUT := -o dist/stash-win.exe
|
||||
cross-compile-windows: PLATFORM_SPECIFIC_LDFLAGS := -H windowsgui
|
||||
cross-compile-windows: GO_BUILD_TAGS := $(GO_BUILD_TAGS_WINDOWS)
|
||||
cross-compile-windows: build-release-static
|
||||
|
||||
cross-compile-macos-intel: export GOOS := darwin
|
||||
@@ -85,6 +86,7 @@ cross-compile-macos-intel: export GOARCH := amd64
|
||||
cross-compile-macos-intel: export CC := o64-clang
|
||||
cross-compile-macos-intel: export CXX := o64-clang++
|
||||
cross-compile-macos-intel: OUTPUT := -o dist/stash-macos-intel
|
||||
cross-compile-macos-intel: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
|
||||
# can't use static build for OSX
|
||||
cross-compile-macos-intel: build-release
|
||||
|
||||
@@ -93,6 +95,7 @@ cross-compile-macos-applesilicon: export GOARCH := arm64
|
||||
cross-compile-macos-applesilicon: export CC := oa64e-clang
|
||||
cross-compile-macos-applesilicon: export CXX := oa64e-clang++
|
||||
cross-compile-macos-applesilicon: OUTPUT := -o dist/stash-macos-applesilicon
|
||||
cross-compile-macos-applesilicon: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
|
||||
# can't use static build for OSX
|
||||
cross-compile-macos-applesilicon: build-release
|
||||
|
||||
@@ -113,17 +116,20 @@ cross-compile-macos:
|
||||
cross-compile-freebsd: export GOOS := freebsd
|
||||
cross-compile-freebsd: export GOARCH := amd64
|
||||
cross-compile-freebsd: OUTPUT := -o dist/stash-freebsd
|
||||
cross-compile-freebsd: GO_BUILD_TAGS += netgo
|
||||
cross-compile-freebsd: build-release-static
|
||||
|
||||
cross-compile-linux: export GOOS := linux
|
||||
cross-compile-linux: export GOARCH := amd64
|
||||
cross-compile-linux: OUTPUT := -o dist/stash-linux
|
||||
cross-compile-linux: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
|
||||
cross-compile-linux: build-release-static
|
||||
|
||||
cross-compile-linux-arm64v8: export GOOS := linux
|
||||
cross-compile-linux-arm64v8: export GOARCH := arm64
|
||||
cross-compile-linux-arm64v8: export CC := aarch64-linux-gnu-gcc
|
||||
cross-compile-linux-arm64v8: OUTPUT := -o dist/stash-linux-arm64v8
|
||||
cross-compile-linux-arm64v8: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
|
||||
cross-compile-linux-arm64v8: build-release-static
|
||||
|
||||
cross-compile-linux-arm32v7: export GOOS := linux
|
||||
@@ -131,6 +137,7 @@ cross-compile-linux-arm32v7: export GOARCH := arm
|
||||
cross-compile-linux-arm32v7: export GOARM := 7
|
||||
cross-compile-linux-arm32v7: export CC := arm-linux-gnueabihf-gcc
|
||||
cross-compile-linux-arm32v7: OUTPUT := -o dist/stash-linux-arm32v7
|
||||
cross-compile-linux-arm32v7: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
|
||||
cross-compile-linux-arm32v7: build-release-static
|
||||
|
||||
cross-compile-linux-arm32v6: export GOOS := linux
|
||||
@@ -138,11 +145,13 @@ cross-compile-linux-arm32v6: export GOARCH := arm
|
||||
cross-compile-linux-arm32v6: export GOARM := 6
|
||||
cross-compile-linux-arm32v6: export CC := arm-linux-gnueabi-gcc
|
||||
cross-compile-linux-arm32v6: OUTPUT := -o dist/stash-linux-arm32v6
|
||||
cross-compile-linux-arm32v6: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
|
||||
cross-compile-linux-arm32v6: build-release-static
|
||||
|
||||
cross-compile-all:
|
||||
make cross-compile-windows
|
||||
make cross-compile-macos
|
||||
make cross-compile-macos-intel
|
||||
make cross-compile-macos-applesilicon
|
||||
make cross-compile-linux
|
||||
make cross-compile-linux-arm64v8
|
||||
make cross-compile-linux-arm32v7
|
||||
@@ -169,6 +178,10 @@ generate-frontend:
|
||||
generate-backend: touch-ui
|
||||
go generate -mod=vendor ./cmd/stash
|
||||
|
||||
.PHONY: generate-dataloaders
|
||||
generate-dataloaders:
|
||||
go generate -mod=vendor ./internal/api/loaders
|
||||
|
||||
# Regenerates stash-box client files
|
||||
.PHONY: generate-stash-box-client
|
||||
generate-stash-box-client:
|
||||
|
||||
@@ -45,9 +45,9 @@ Many community-maintained scrapers are available for download at the [Community
|
||||
|
||||
# Translation
|
||||
[](https://translate.stashapp.cc/engage/stash/)
|
||||
🇧🇷 🇨🇳 🇳🇱 🇬🇧 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇪🇸 🇸🇪 🇹🇼 🇹🇷
|
||||
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇪🇪 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇷🇺 🇪🇸 🇸🇪 🇹🇼 🇹🇷
|
||||
|
||||
Stash is available in 13 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks!
|
||||
Stash is available in 18 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks!
|
||||
|
||||
# Support (FAQ)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/stashapp/stash/internal/api"
|
||||
"github.com/stashapp/stash/internal/desktop"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
@@ -17,17 +16,22 @@ import (
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// On Windows, attach to parent shell
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
fmt.Printf("FixConsoleOutput: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
manager.Initialize()
|
||||
api.Start()
|
||||
defer recoverPanic()
|
||||
|
||||
_, err := manager.Initialize()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer recoverPanic()
|
||||
if err := api.Start(); err != nil {
|
||||
handleError(err)
|
||||
} else {
|
||||
manager.GetInstance().Shutdown(0)
|
||||
}
|
||||
}()
|
||||
|
||||
go handleSignals()
|
||||
desktop.Start(manager.GetInstance(), &manager.FaviconProvider{UIBox: ui.UIBox})
|
||||
@@ -35,6 +39,21 @@ func main() {
|
||||
blockForever()
|
||||
}
|
||||
|
||||
func recoverPanic() {
|
||||
if p := recover(); p != nil {
|
||||
handleError(fmt.Errorf("Panic: %v", p))
|
||||
}
|
||||
}
|
||||
|
||||
func handleError(err error) {
|
||||
if desktop.IsDesktop() {
|
||||
desktop.FatalError(err)
|
||||
manager.GetInstance().Shutdown(0)
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleSignals() {
|
||||
// handle signals
|
||||
signals := make(chan os.Signal, 1)
|
||||
|
||||
@@ -16,7 +16,7 @@ ARG STASH_VERSION
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
|
||||
|
||||
# Build Backend
|
||||
FROM golang:1.17-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/
|
||||
|
||||
@@ -11,8 +11,7 @@ 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 vips-tools && pip install --no-cache-dir mechanicalsoup cloudscraper
|
||||
RUN ln -s /usr/bin/python3 /usr/bin/python
|
||||
RUN apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg vips-tools ruby && pip install --no-cache-dir mechanicalsoup cloudscraper && gem install faraday
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
EXPOSE 9999
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.17
|
||||
FROM golang:1.19
|
||||
|
||||
LABEL maintainer="https://discord.gg/2TsNFKt"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
user=stashapp
|
||||
repo=compiler
|
||||
version=6
|
||||
version=7
|
||||
|
||||
latest:
|
||||
docker build -t ${user}/${repo}:latest .
|
||||
|
||||
@@ -15,12 +15,12 @@ NOTE: You may need to run the `go get` commands outside the project directory to
|
||||
### Windows
|
||||
|
||||
1. Download and install [Go for Windows](https://golang.org/dl/)
|
||||
2. Download and install [MingW](https://sourceforge.net/projects/mingw-w64/)
|
||||
2. Download and extract [MingW64](https://sourceforge.net/projects/mingw-w64/files/) (scroll down and select x86_64-posix-seh, dont use the autoinstaller it doesnt work)
|
||||
3. Search for "advanced system settings" and open the system properties dialog.
|
||||
1. Click the `Environment Variables` button
|
||||
2. Under system variables find the `Path`. Edit and add `C:\Program Files\mingw-w64\*\mingw64\bin` (replace * with the correct path).
|
||||
2. Under system variables find the `Path`. Edit and add `C:\MinGW\bin` (replace with the correct path to where you extracted MingW64).
|
||||
|
||||
NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
|
||||
NOTE: The `make` command in Windows will be `mingw32-make` with MingW. For example `make pre-ui` will be `mingw32-make pre-ui`
|
||||
|
||||
### macOS
|
||||
|
||||
@@ -43,9 +43,10 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
|
||||
|
||||
## Building a release
|
||||
|
||||
1. Run `make generate` to create generated files
|
||||
2. Run `make ui` to compile the frontend
|
||||
3. Run `make build` to build the executable for your current platform
|
||||
1. Run `make pre-ui` to install UI dependencies
|
||||
2. Run `make generate` to create generated files
|
||||
3. Run `make ui` to compile the frontend
|
||||
4. Run `make build` to build the executable for your current platform
|
||||
|
||||
## Cross compiling
|
||||
|
||||
@@ -53,7 +54,7 @@ This project uses a modification of the [CI-GoReleaser](https://github.com/bep/d
|
||||
where the app can be cross-compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following
|
||||
command to open a bash shell to the container to poke around:
|
||||
|
||||
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashappdev/compiler:latest /bin/bash`
|
||||
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashapp/compiler:latest /bin/bash`
|
||||
|
||||
## Profiling
|
||||
|
||||
|
||||
23
go.mod
23
go.mod
@@ -19,7 +19,7 @@ require (
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
|
||||
github.com/jmoiron/sqlx v1.3.1
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
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
|
||||
@@ -36,31 +36,36 @@ require (
|
||||
github.com/vektra/mockery/v2 v2.10.0
|
||||
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
|
||||
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9
|
||||
golang.org/x/sys v0.0.0-20220329152356-43be30ef3008
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b
|
||||
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/text v0.3.7
|
||||
golang.org/x/tools v0.1.10 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
|
||||
github.com/asticode/go-astisub v0.20.0
|
||||
github.com/doug-martin/goqu/v9 v9.18.0
|
||||
github.com/go-chi/httplog v0.2.1
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/kermieisinthehouse/gosx-notifier v0.1.1
|
||||
github.com/kermieisinthehouse/systray v1.2.4
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/spf13/cast v1.4.1
|
||||
github.com/vearutop/statigz v1.1.6
|
||||
github.com/vektah/dataloaden v0.3.0
|
||||
github.com/vektah/gqlparser/v2 v2.4.1
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/antchfx/xpath v1.2.0 // indirect
|
||||
github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab // indirect
|
||||
github.com/asticode/go-astikit v0.20.0 // indirect
|
||||
github.com/asticode/go-astits v1.8.0 // indirect
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@@ -89,7 +94,6 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/zerolog v1.26.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/cobra v1.4.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
@@ -97,12 +101,11 @@ require (
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/urfave/cli/v2 v2.4.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
||||
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
|
||||
|
||||
go 1.17
|
||||
go 1.19
|
||||
|
||||
47
go.sum
47
go.sum
@@ -65,6 +65,8 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
|
||||
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/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
|
||||
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/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
@@ -97,10 +99,6 @@ github.com/antchfx/xpath v1.2.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwq
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/apache/arrow/go/arrow v0.0.0-20200601151325-b2287a20f230/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0=
|
||||
github.com/apache/arrow/go/arrow v0.0.0-20210521153258-78c88a9f517b/go.mod h1:R4hW3Ug0s+n4CUsWHKOj00Pu01ZqU4x/hSF5kXUcXKQ=
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29/go.mod h1:JYWahgHer+Z2xbsgHPtaDYVWzeHDminu+YIBWkxpCAY=
|
||||
github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab h1:CMGzRRCjnD50RjUFSArBLuCxiDvdp7b8YPAcikBEQ+k=
|
||||
github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab/go.mod h1:nfFtvHn2Hgs9G1u0/J6LHQv//EksNC+7G8vXmd1VTJ8=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
@@ -108,6 +106,12 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
|
||||
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
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.20.0 h1:mKuLwgGkQj35RRHFiTcq+2hgR7g1mHiYiIkr9UNTmXw=
|
||||
github.com/asticode/go-astisub v0.20.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 v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.3.2/go.mod h1:7OaACgj2SX3XGWnrIjGlJM22h6yD6MEWKvm7levnnM8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.6.0/go.mod h1:tI4KhsR5VkzlUa2DZAdwx7wCAYGwkZZ1H31PYrBFx1w=
|
||||
@@ -204,6 +208,8 @@ github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompati
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
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/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
@@ -246,8 +252,9 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
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-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
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=
|
||||
@@ -533,8 +540,9 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
|
||||
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
@@ -568,8 +576,9 @@ 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-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
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=
|
||||
@@ -629,6 +638,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -690,7 +700,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
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/snowflakedb/gosnowflake v1.4.3/go.mod h1:1kyg2XEduwti88V11PKRHImhXLK5WpGiayY6lFNYb98=
|
||||
@@ -743,6 +752,8 @@ github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I=
|
||||
github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
|
||||
github.com/vearutop/statigz v1.1.6 h1:si1zvulh/6P4S/SjFticuKQ8/EgQISglaRuycj8PWso=
|
||||
github.com/vearutop/statigz v1.1.6/go.mod h1:czAv7iXgPv/s+xsgXpVEhhD0NSOQ4wZPgmM/n7LANDI=
|
||||
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
|
||||
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/vektah/gqlparser/v2 v2.4.1 h1:QOyEn8DAPMUMARGMeshKDkDgNmVoEaEGiDB0uWxcSlQ=
|
||||
github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
@@ -853,8 +864,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -910,8 +922,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 h1:0qxwC5n+ttVOINCBeRHO0nq9X7uy8SDsPoi5OaCdIEI=
|
||||
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -955,7 +967,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
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-20190405154228-4b34438f7a67/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-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1035,11 +1046,10 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220329152356-43be30ef3008 h1:pq9pwoi2rjLWvmiVser/lIOgiyA3fli4M+RfGVMA7nE=
|
||||
golang.org/x/sys v0.0.0-20220329152356-43be30ef3008/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs=
|
||||
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -1069,6 +1079,7 @@ golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
@@ -1126,14 +1137,14 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@@ -1298,6 +1309,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
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=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
131
gqlgen.yml
131
gqlgen.yml
@@ -4,43 +4,122 @@ schema:
|
||||
- "graphql/schema/types/*.graphql"
|
||||
- "graphql/schema/*.graphql"
|
||||
exec:
|
||||
filename: pkg/models/generated_exec.go
|
||||
filename: internal/api/generated_exec.go
|
||||
model:
|
||||
filename: pkg/models/generated_models.go
|
||||
filename: internal/api/generated_models.go
|
||||
resolver:
|
||||
filename: internal/api/resolver.go
|
||||
type: Resolver
|
||||
|
||||
struct_tag: gqlgen
|
||||
|
||||
autobind:
|
||||
- github.com/stashapp/stash/pkg/models
|
||||
- github.com/stashapp/stash/pkg/plugin
|
||||
- github.com/stashapp/stash/pkg/scraper
|
||||
- github.com/stashapp/stash/internal/identify
|
||||
- github.com/stashapp/stash/internal/dlna
|
||||
- github.com/stashapp/stash/pkg/scraper/stashbox
|
||||
|
||||
models:
|
||||
# Scalars
|
||||
Timestamp:
|
||||
model: github.com/stashapp/stash/pkg/models.Timestamp
|
||||
# Objects
|
||||
Gallery:
|
||||
model: github.com/stashapp/stash/pkg/models.Gallery
|
||||
Int64:
|
||||
model: github.com/stashapp/stash/pkg/models.Int64
|
||||
# define to force resolvers
|
||||
Image:
|
||||
model: github.com/stashapp/stash/pkg/models.Image
|
||||
ImageFileType:
|
||||
model: github.com/stashapp/stash/pkg/models.ImageFileType
|
||||
Performer:
|
||||
model: github.com/stashapp/stash/pkg/models.Performer
|
||||
Scene:
|
||||
model: github.com/stashapp/stash/pkg/models.Scene
|
||||
SceneMarker:
|
||||
model: github.com/stashapp/stash/pkg/models.SceneMarker
|
||||
ScrapedItem:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedItem
|
||||
Studio:
|
||||
model: github.com/stashapp/stash/pkg/models.Studio
|
||||
Movie:
|
||||
model: github.com/stashapp/stash/pkg/models.Movie
|
||||
Tag:
|
||||
model: github.com/stashapp/stash/pkg/models.Tag
|
||||
SceneFileType:
|
||||
model: github.com/stashapp/stash/pkg/models.SceneFileType
|
||||
SavedFilter:
|
||||
model: github.com/stashapp/stash/pkg/models.SavedFilter
|
||||
StashID:
|
||||
fields:
|
||||
title:
|
||||
resolver: true
|
||||
# autobind on config causes generation issues
|
||||
StashConfig:
|
||||
model: github.com/stashapp/stash/internal/manager/config.StashConfig
|
||||
StashConfigInput:
|
||||
model: github.com/stashapp/stash/internal/manager/config.StashConfigInput
|
||||
StashBoxInput:
|
||||
model: github.com/stashapp/stash/internal/manager/config.StashBoxInput
|
||||
ConfigImageLightboxResult:
|
||||
model: github.com/stashapp/stash/internal/manager/config.ConfigImageLightboxResult
|
||||
ImageLightboxDisplayMode:
|
||||
model: github.com/stashapp/stash/internal/manager/config.ImageLightboxDisplayMode
|
||||
ImageLightboxScrollMode:
|
||||
model: github.com/stashapp/stash/internal/manager/config.ImageLightboxScrollMode
|
||||
ConfigDisableDropdownCreate:
|
||||
model: github.com/stashapp/stash/internal/manager/config.ConfigDisableDropdownCreate
|
||||
ScanMetadataOptions:
|
||||
model: github.com/stashapp/stash/internal/manager/config.ScanMetadataOptions
|
||||
AutoTagMetadataOptions:
|
||||
model: github.com/stashapp/stash/internal/manager/config.AutoTagMetadataOptions
|
||||
SceneParserInput:
|
||||
model: github.com/stashapp/stash/internal/manager.SceneParserInput
|
||||
SceneParserResult:
|
||||
model: github.com/stashapp/stash/internal/manager.SceneParserResult
|
||||
SceneMovieID:
|
||||
model: github.com/stashapp/stash/internal/manager.SceneMovieID
|
||||
SystemStatus:
|
||||
model: github.com/stashapp/stash/internal/manager.SystemStatus
|
||||
SystemStatusEnum:
|
||||
model: github.com/stashapp/stash/internal/manager.SystemStatusEnum
|
||||
ImportDuplicateEnum:
|
||||
model: github.com/stashapp/stash/internal/manager.ImportDuplicateEnum
|
||||
SetupInput:
|
||||
model: github.com/stashapp/stash/internal/manager.SetupInput
|
||||
MigrateInput:
|
||||
model: github.com/stashapp/stash/internal/manager.MigrateInput
|
||||
ScanMetadataInput:
|
||||
model: github.com/stashapp/stash/internal/manager.ScanMetadataInput
|
||||
GenerateMetadataInput:
|
||||
model: github.com/stashapp/stash/internal/manager.GenerateMetadataInput
|
||||
GeneratePreviewOptionsInput:
|
||||
model: github.com/stashapp/stash/internal/manager.GeneratePreviewOptionsInput
|
||||
AutoTagMetadataInput:
|
||||
model: github.com/stashapp/stash/internal/manager.AutoTagMetadataInput
|
||||
CleanMetadataInput:
|
||||
model: github.com/stashapp/stash/internal/manager.CleanMetadataInput
|
||||
StashBoxBatchPerformerTagInput:
|
||||
model: github.com/stashapp/stash/internal/manager.StashBoxBatchPerformerTagInput
|
||||
SceneStreamEndpoint:
|
||||
model: github.com/stashapp/stash/internal/manager.SceneStreamEndpoint
|
||||
ExportObjectTypeInput:
|
||||
model: github.com/stashapp/stash/internal/manager.ExportObjectTypeInput
|
||||
ExportObjectsInput:
|
||||
model: github.com/stashapp/stash/internal/manager.ExportObjectsInput
|
||||
ImportObjectsInput:
|
||||
model: github.com/stashapp/stash/internal/manager.ImportObjectsInput
|
||||
ScanMetaDataFilterInput:
|
||||
model: github.com/stashapp/stash/internal/manager.ScanMetaDataFilterInput
|
||||
# renamed types
|
||||
BulkUpdateIdMode:
|
||||
model: github.com/stashapp/stash/pkg/models.RelationshipUpdateMode
|
||||
DLNAStatus:
|
||||
model: github.com/stashapp/stash/internal/dlna.Status
|
||||
DLNAIP:
|
||||
model: github.com/stashapp/stash/internal/dlna.Dlnaip
|
||||
IdentifySource:
|
||||
model: github.com/stashapp/stash/internal/identify.Source
|
||||
IdentifyMetadataTaskOptions:
|
||||
model: github.com/stashapp/stash/internal/identify.Options
|
||||
IdentifyMetadataInput:
|
||||
model: github.com/stashapp/stash/internal/identify.Options
|
||||
IdentifyMetadataOptions:
|
||||
model: github.com/stashapp/stash/internal/identify.MetadataOptions
|
||||
IdentifyFieldOptions:
|
||||
model: github.com/stashapp/stash/internal/identify.FieldOptions
|
||||
IdentifyFieldStrategy:
|
||||
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:
|
||||
model: github.com/stashapp/stash/internal/identify.FieldOptions
|
||||
IdentifyMetadataOptionsInput:
|
||||
model: github.com/stashapp/stash/internal/identify.MetadataOptions
|
||||
ScraperSourceInput:
|
||||
model: github.com/stashapp/stash/pkg/scraper.Source
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
excludeImage
|
||||
}
|
||||
databasePath
|
||||
backupDirectoryPath
|
||||
generatedPath
|
||||
metadataPath
|
||||
scrapersPath
|
||||
@@ -62,6 +63,10 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
showStudioAsText
|
||||
css
|
||||
cssEnabled
|
||||
javascript
|
||||
javascriptEnabled
|
||||
customLocales
|
||||
customLocalesEnabled
|
||||
language
|
||||
imageLightbox {
|
||||
slideshowDelay
|
||||
@@ -69,6 +74,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
scaleUp
|
||||
resetZoomOnNav
|
||||
scrollMode
|
||||
scrollAttemptsBeforeChange
|
||||
}
|
||||
disableDropdownCreate {
|
||||
performer
|
||||
@@ -184,4 +190,5 @@ fragment ConfigData on ConfigResult {
|
||||
defaults {
|
||||
...ConfigDefaultSettingsData
|
||||
}
|
||||
ui
|
||||
}
|
||||
|
||||
46
graphql/documents/data/file.graphql
Normal file
46
graphql/documents/data/file.graphql
Normal file
@@ -0,0 +1,46 @@
|
||||
fragment FolderData on Folder {
|
||||
id
|
||||
path
|
||||
}
|
||||
|
||||
fragment VideoFileData on VideoFile {
|
||||
id
|
||||
path
|
||||
size
|
||||
mod_time
|
||||
duration
|
||||
video_codec
|
||||
audio_codec
|
||||
width
|
||||
height
|
||||
frame_rate
|
||||
bit_rate
|
||||
fingerprints {
|
||||
type
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fragment ImageFileData on ImageFile {
|
||||
id
|
||||
path
|
||||
size
|
||||
mod_time
|
||||
width
|
||||
height
|
||||
fingerprints {
|
||||
type
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fragment GalleryFileData on GalleryFile {
|
||||
id
|
||||
path
|
||||
size
|
||||
mod_time
|
||||
fingerprints {
|
||||
type
|
||||
value
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
fragment SlimGalleryData on Gallery {
|
||||
id
|
||||
checksum
|
||||
path
|
||||
title
|
||||
date
|
||||
url
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
organized
|
||||
files {
|
||||
...GalleryFileData
|
||||
}
|
||||
folder {
|
||||
...FolderData
|
||||
}
|
||||
image_count
|
||||
cover {
|
||||
file {
|
||||
size
|
||||
width
|
||||
height
|
||||
files {
|
||||
...ImageFileData
|
||||
}
|
||||
|
||||
paths {
|
||||
@@ -37,8 +39,6 @@ fragment SlimGalleryData on Gallery {
|
||||
image_path
|
||||
}
|
||||
scenes {
|
||||
id
|
||||
title
|
||||
path
|
||||
...SlimSceneData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
fragment GalleryData on Gallery {
|
||||
id
|
||||
checksum
|
||||
path
|
||||
created_at
|
||||
updated_at
|
||||
title
|
||||
date
|
||||
url
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
organized
|
||||
images {
|
||||
...SlimImageData
|
||||
|
||||
files {
|
||||
...GalleryFileData
|
||||
}
|
||||
folder {
|
||||
...FolderData
|
||||
}
|
||||
|
||||
cover {
|
||||
...SlimImageData
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
fragment SlimImageData on Image {
|
||||
id
|
||||
checksum
|
||||
title
|
||||
rating
|
||||
rating100
|
||||
organized
|
||||
o_counter
|
||||
path
|
||||
|
||||
file {
|
||||
size
|
||||
width
|
||||
height
|
||||
files {
|
||||
...ImageFileData
|
||||
}
|
||||
|
||||
paths {
|
||||
@@ -20,8 +16,13 @@ fragment SlimImageData on Image {
|
||||
|
||||
galleries {
|
||||
id
|
||||
path
|
||||
title
|
||||
files {
|
||||
path
|
||||
}
|
||||
folder {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
studio {
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
fragment ImageData on Image {
|
||||
id
|
||||
checksum
|
||||
title
|
||||
rating
|
||||
rating100
|
||||
organized
|
||||
o_counter
|
||||
path
|
||||
created_at
|
||||
updated_at
|
||||
|
||||
file {
|
||||
size
|
||||
width
|
||||
height
|
||||
files {
|
||||
...ImageFileData
|
||||
}
|
||||
|
||||
paths {
|
||||
|
||||
@@ -2,4 +2,5 @@ fragment SlimMovieData on Movie {
|
||||
id
|
||||
name
|
||||
front_image_path
|
||||
rating100
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ fragment MovieData on Movie {
|
||||
aliases
|
||||
duration
|
||||
date
|
||||
rating
|
||||
rating100
|
||||
director
|
||||
|
||||
studio {
|
||||
|
||||
@@ -13,7 +13,7 @@ fragment SlimPerformerData on Performer {
|
||||
ethnicity
|
||||
hair_color
|
||||
eye_color
|
||||
height
|
||||
height_cm
|
||||
fake_tits
|
||||
career_length
|
||||
tattoos
|
||||
@@ -26,7 +26,7 @@ fragment SlimPerformerData on Performer {
|
||||
endpoint
|
||||
stash_id
|
||||
}
|
||||
rating
|
||||
rating100
|
||||
death_date
|
||||
weight
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ fragment PerformerData on Performer {
|
||||
ethnicity
|
||||
country
|
||||
eye_color
|
||||
height
|
||||
height_cm
|
||||
measurements
|
||||
fake_tits
|
||||
career_length
|
||||
@@ -33,7 +33,7 @@ fragment PerformerData on Performer {
|
||||
stash_id
|
||||
endpoint
|
||||
}
|
||||
rating
|
||||
rating100
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
fragment SlimSceneData on Scene {
|
||||
id
|
||||
checksum
|
||||
oshash
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
url
|
||||
date
|
||||
rating
|
||||
rating100
|
||||
o_counter
|
||||
organized
|
||||
path
|
||||
phash
|
||||
interactive
|
||||
interactive_speed
|
||||
resume_time
|
||||
play_duration
|
||||
play_count
|
||||
|
||||
file {
|
||||
size
|
||||
duration
|
||||
video_codec
|
||||
audio_codec
|
||||
width
|
||||
height
|
||||
framerate
|
||||
bitrate
|
||||
files {
|
||||
...VideoFileData
|
||||
}
|
||||
|
||||
paths {
|
||||
@@ -31,21 +25,27 @@ fragment SlimSceneData on Scene {
|
||||
stream
|
||||
webp
|
||||
vtt
|
||||
chapters_vtt
|
||||
sprite
|
||||
funscript
|
||||
interactive_heatmap
|
||||
caption
|
||||
}
|
||||
|
||||
scene_markers {
|
||||
id
|
||||
title
|
||||
seconds
|
||||
primary_tag {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
galleries {
|
||||
id
|
||||
path
|
||||
files {
|
||||
path
|
||||
}
|
||||
title
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
fragment SceneData on Scene {
|
||||
id
|
||||
checksum
|
||||
oshash
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
url
|
||||
date
|
||||
rating
|
||||
rating100
|
||||
o_counter
|
||||
organized
|
||||
path
|
||||
phash
|
||||
interactive
|
||||
interactive_speed
|
||||
captions {
|
||||
language_code
|
||||
caption_type
|
||||
}
|
||||
created_at
|
||||
updated_at
|
||||
resume_time
|
||||
last_played_at
|
||||
play_duration
|
||||
play_count
|
||||
|
||||
file {
|
||||
size
|
||||
duration
|
||||
video_codec
|
||||
audio_codec
|
||||
width
|
||||
height
|
||||
framerate
|
||||
bitrate
|
||||
files {
|
||||
...VideoFileData
|
||||
}
|
||||
|
||||
paths {
|
||||
@@ -33,10 +32,10 @@ fragment SceneData on Scene {
|
||||
stream
|
||||
webp
|
||||
vtt
|
||||
chapters_vtt
|
||||
sprite
|
||||
funscript
|
||||
interactive_heatmap
|
||||
caption
|
||||
}
|
||||
|
||||
scene_markers {
|
||||
|
||||
@@ -105,7 +105,9 @@ fragment ScrapedSceneTagData on ScrapedTag {
|
||||
|
||||
fragment ScrapedSceneData on ScrapedScene {
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
url
|
||||
date
|
||||
image
|
||||
@@ -166,7 +168,9 @@ fragment ScrapedGalleryData on ScrapedGallery {
|
||||
|
||||
fragment ScrapedStashBoxSceneData on ScrapedScene {
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
url
|
||||
date
|
||||
image
|
||||
|
||||
@@ -10,6 +10,6 @@ fragment SlimStudioData on Studio {
|
||||
id
|
||||
}
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
aliases
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ fragment StudioData on Studio {
|
||||
endpoint
|
||||
}
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
aliases
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
fragment TagData on Tag {
|
||||
id
|
||||
name
|
||||
description
|
||||
aliases
|
||||
ignore_auto_tag
|
||||
image_path
|
||||
|
||||
@@ -36,6 +36,10 @@ mutation ConfigureDefaults($input: ConfigDefaultSettingsInput!) {
|
||||
}
|
||||
}
|
||||
|
||||
mutation ConfigureUI($input: Map!) {
|
||||
configureUI(input: $input)
|
||||
}
|
||||
|
||||
mutation GenerateAPIKey($input: GenerateAPIKeyInput!) {
|
||||
generateAPIKey(input: $input)
|
||||
}
|
||||
|
||||
3
graphql/documents/mutations/file.graphql
Normal file
3
graphql/documents/mutations/file.graphql
Normal file
@@ -0,0 +1,3 @@
|
||||
mutation DeleteFiles($ids: [ID!]!) {
|
||||
deleteFiles(ids: $ids)
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
mutation SceneCreate(
|
||||
$input: SceneCreateInput!) {
|
||||
|
||||
sceneCreate(input: $input) {
|
||||
...SceneData
|
||||
}
|
||||
}
|
||||
|
||||
mutation SceneUpdate(
|
||||
$input: SceneUpdateInput!) {
|
||||
|
||||
@@ -20,6 +28,14 @@ mutation ScenesUpdate($input : [SceneUpdateInput!]!) {
|
||||
}
|
||||
}
|
||||
|
||||
mutation SceneSaveActivity($id: ID!, $resume_time: Float, $playDuration: Float) {
|
||||
sceneSaveActivity(id: $id, resume_time: $resume_time, playDuration: $playDuration)
|
||||
}
|
||||
|
||||
mutation SceneIncrementPlayCount($id: ID!) {
|
||||
sceneIncrementPlayCount(id: $id)
|
||||
}
|
||||
|
||||
mutation SceneIncrementO($id: ID!) {
|
||||
sceneIncrementO(id: $id)
|
||||
}
|
||||
@@ -43,3 +59,13 @@ mutation ScenesDestroy($ids: [ID!]!, $delete_file: Boolean, $delete_generated :
|
||||
mutation SceneGenerateScreenshot($id: ID!, $at: Float) {
|
||||
sceneGenerateScreenshot(id: $id, at: $at)
|
||||
}
|
||||
|
||||
mutation SceneAssignFile($input: AssignSceneFileInput!) {
|
||||
sceneAssignFile(input: $input)
|
||||
}
|
||||
|
||||
mutation SceneMerge($input: SceneMergeInput!) {
|
||||
sceneMerge(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
query FindSavedFilters($mode: FilterMode!) {
|
||||
query FindSavedFilter($id: ID!) {
|
||||
findSavedFilter(id: $id) {
|
||||
...SavedFilterData
|
||||
}
|
||||
}
|
||||
|
||||
query FindSavedFilters($mode: FilterMode) {
|
||||
findSavedFilters(mode: $mode) {
|
||||
...SavedFilterData
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene
|
||||
filesize
|
||||
duration
|
||||
scenes {
|
||||
...SceneData
|
||||
...SlimSceneData
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,9 @@ query ParseSceneFilenames($filter: FindFilterType!, $config: SceneParserInput!)
|
||||
...SlimSceneData
|
||||
}
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
url
|
||||
date
|
||||
rating
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""The query root for this schema"""
|
||||
type Query {
|
||||
# Filters
|
||||
findSavedFilters(mode: FilterMode!): [SavedFilter!]!
|
||||
findSavedFilter(id: ID!): SavedFilter
|
||||
findSavedFilters(mode: FilterMode): [SavedFilter!]!
|
||||
findDefaultFilter(mode: FilterMode!): SavedFilter
|
||||
|
||||
"""Find a scene by ID or Checksum"""
|
||||
@@ -114,12 +115,6 @@ type Query {
|
||||
"""Scrape a list of performers from a query"""
|
||||
scrapeFreeonesPerformerList(query: String!): [String!]! @deprecated(reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones")
|
||||
|
||||
"""Query StashBox for scenes"""
|
||||
queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]! @deprecated(reason: "use scrapeSingleScene or scrapeMultiScenes")
|
||||
"""Query StashBox for performers"""
|
||||
queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]! @deprecated(reason: "use scrapeSinglePerformer or scrapeMultiPerformers")
|
||||
# === end deprecated methods ===
|
||||
|
||||
# Plugins
|
||||
"""List loaded plugins"""
|
||||
plugins: [Plugin!]
|
||||
@@ -167,7 +162,9 @@ type Mutation {
|
||||
setup(input: SetupInput!): Boolean!
|
||||
migrate(input: MigrateInput!): Boolean!
|
||||
|
||||
sceneCreate(input: SceneCreateInput!): Scene
|
||||
sceneUpdate(input: SceneUpdateInput!): Scene
|
||||
sceneMerge(input: SceneMergeInput!): Scene
|
||||
bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!]
|
||||
sceneDestroy(input: SceneDestroyInput!): Boolean!
|
||||
scenesDestroy(input: ScenesDestroyInput!): Boolean!
|
||||
@@ -180,6 +177,12 @@ type Mutation {
|
||||
"""Resets the o-counter for a scene to 0. Returns the new value"""
|
||||
sceneResetO(id: ID!): Int!
|
||||
|
||||
"""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!
|
||||
|
||||
"""Increments the play count for the scene. Returns the new play count value."""
|
||||
sceneIncrementPlayCount(id: ID!): Int!
|
||||
|
||||
"""Generates screenshot at specified time in seconds. Leave empty to generate default screenshot"""
|
||||
sceneGenerateScreenshot(id: ID!, at: Float): String!
|
||||
|
||||
@@ -187,6 +190,8 @@ type Mutation {
|
||||
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
|
||||
sceneMarkerDestroy(id: ID!): Boolean!
|
||||
|
||||
sceneAssignFile(input: AssignSceneFileInput!): Boolean!
|
||||
|
||||
imageUpdate(input: ImageUpdateInput!): Image
|
||||
bulkImageUpdate(input: BulkImageUpdateInput!): [Image!]
|
||||
imageDestroy(input: ImageDestroyInput!): Boolean!
|
||||
@@ -232,6 +237,8 @@ type Mutation {
|
||||
tagsDestroy(ids: [ID!]!): Boolean!
|
||||
tagsMerge(input: TagsMergeInput!): Tag
|
||||
|
||||
deleteFiles(ids: [ID!]!): Boolean!
|
||||
|
||||
# Saved filters
|
||||
saveFilter(input: SaveFilterInput!): SavedFilter!
|
||||
destroySavedFilter(input: DestroyFilterInput!): Boolean!
|
||||
@@ -244,6 +251,11 @@ type Mutation {
|
||||
configureScraping(input: ConfigScrapingInput!): ConfigScrapingResult!
|
||||
configureDefaults(input: ConfigDefaultSettingsInput!): ConfigDefaultSettingsResult!
|
||||
|
||||
# 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"""
|
||||
generateAPIKey(input: GenerateAPIKeyInput!): String!
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ input ConfigGeneralInput {
|
||||
stashes: [StashConfigInput!]
|
||||
"""Path to the SQLite database"""
|
||||
databasePath: String
|
||||
"""Path to backup directory"""
|
||||
backupDirectoryPath: String
|
||||
"""Path to generated files"""
|
||||
generatedPath: String
|
||||
"""Path to import/export files"""
|
||||
@@ -116,6 +118,8 @@ type ConfigGeneralResult {
|
||||
stashes: [StashConfig!]!
|
||||
"""Path to the SQLite database"""
|
||||
databasePath: String!
|
||||
"""Path to backup directory"""
|
||||
backupDirectoryPath: String!
|
||||
"""Path to generated files"""
|
||||
generatedPath: String!
|
||||
"""Path to import/export files"""
|
||||
@@ -217,6 +221,7 @@ input ConfigImageLightboxInput {
|
||||
scaleUp: Boolean
|
||||
resetZoomOnNav: Boolean
|
||||
scrollMode: ImageLightboxScrollMode
|
||||
scrollAttemptsBeforeChange: Int
|
||||
}
|
||||
|
||||
type ConfigImageLightboxResult {
|
||||
@@ -225,6 +230,7 @@ type ConfigImageLightboxResult {
|
||||
scaleUp: Boolean
|
||||
resetZoomOnNav: Boolean
|
||||
scrollMode: ImageLightboxScrollMode
|
||||
scrollAttemptsBeforeChange: Int!
|
||||
}
|
||||
|
||||
input ConfigInterfaceInput {
|
||||
@@ -257,6 +263,14 @@ input ConfigInterfaceInput {
|
||||
"""Custom CSS"""
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
|
||||
"""Custom Javascript"""
|
||||
javascript: String
|
||||
javascriptEnabled: Boolean
|
||||
|
||||
"""Custom Locales"""
|
||||
customLocales: String
|
||||
customLocalesEnabled: Boolean
|
||||
|
||||
"""Interface language"""
|
||||
language: String
|
||||
@@ -320,6 +334,14 @@ type ConfigInterfaceResult {
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
|
||||
"""Custom Javascript"""
|
||||
javascript: String
|
||||
javascriptEnabled: Boolean
|
||||
|
||||
"""Custom Locales"""
|
||||
customLocales: String
|
||||
customLocalesEnabled: Boolean
|
||||
|
||||
"""Interface language"""
|
||||
language: String
|
||||
|
||||
@@ -411,6 +433,7 @@ type ConfigResult {
|
||||
dlna: ConfigDLNAResult!
|
||||
scraping: ConfigScrapingResult!
|
||||
defaults: ConfigDefaultSettingsResult!
|
||||
ui: Map!
|
||||
}
|
||||
|
||||
"""Directory structure of a path"""
|
||||
|
||||
97
graphql/schema/types/file.graphql
Normal file
97
graphql/schema/types/file.graphql
Normal file
@@ -0,0 +1,97 @@
|
||||
type Fingerprint {
|
||||
type: String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
type Folder {
|
||||
id: ID!
|
||||
path: String!
|
||||
|
||||
parent_folder_id: ID
|
||||
zip_file_id: ID
|
||||
|
||||
mod_time: Time!
|
||||
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
}
|
||||
|
||||
interface BaseFile {
|
||||
id: ID!
|
||||
path: String!
|
||||
basename: String!
|
||||
|
||||
parent_folder_id: ID!
|
||||
zip_file_id: ID
|
||||
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
}
|
||||
|
||||
type VideoFile implements BaseFile {
|
||||
id: ID!
|
||||
path: String!
|
||||
basename: String!
|
||||
|
||||
parent_folder_id: ID!
|
||||
zip_file_id: ID
|
||||
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
format: String!
|
||||
width: Int!
|
||||
height: Int!
|
||||
duration: Float!
|
||||
video_codec: String!
|
||||
audio_codec: String!
|
||||
frame_rate: Float!
|
||||
bit_rate: Int!
|
||||
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
}
|
||||
|
||||
type ImageFile implements BaseFile {
|
||||
id: ID!
|
||||
path: String!
|
||||
basename: String!
|
||||
|
||||
parent_folder_id: ID!
|
||||
zip_file_id: ID
|
||||
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
width: Int!
|
||||
height: Int!
|
||||
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
}
|
||||
|
||||
type GalleryFile implements BaseFile {
|
||||
id: ID!
|
||||
path: String!
|
||||
basename: String!
|
||||
|
||||
parent_folder_id: ID!
|
||||
zip_file_id: ID
|
||||
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
}
|
||||
@@ -39,6 +39,14 @@ input PHashDuplicationCriterionInput {
|
||||
distance: Int
|
||||
}
|
||||
|
||||
input StashIDCriterionInput {
|
||||
"""If present, this value is treated as a predicate.
|
||||
That is, it will filter based on stash_ids with the matching endpoint"""
|
||||
endpoint: String
|
||||
stash_id: String
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input PerformerFilterType {
|
||||
AND: PerformerFilterType
|
||||
OR: PerformerFilterType
|
||||
@@ -60,7 +68,9 @@ input PerformerFilterType {
|
||||
"""Filter by eye color"""
|
||||
eye_color: StringCriterionInput
|
||||
"""Filter by height"""
|
||||
height: StringCriterionInput
|
||||
height: StringCriterionInput @deprecated(reason: "Use height_cm instead")
|
||||
"""Filter by height in cm"""
|
||||
height_cm: IntCriterionInput
|
||||
"""Filter by measurements"""
|
||||
measurements: StringCriterionInput
|
||||
"""Filter by fake tits value"""
|
||||
@@ -88,9 +98,13 @@ input PerformerFilterType {
|
||||
"""Filter by gallery count"""
|
||||
gallery_count: IntCriterionInput
|
||||
"""Filter by StashID"""
|
||||
stash_id: StringCriterionInput
|
||||
stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead")
|
||||
"""Filter by StashID"""
|
||||
stash_id_endpoint: StashIDCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
"""Filter by hair color"""
|
||||
@@ -103,6 +117,14 @@ input PerformerFilterType {
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"""Filter by autotag ignore value"""
|
||||
ignore_auto_tag: Boolean
|
||||
"""Filter by birthdate"""
|
||||
birthdate: DateCriterionInput
|
||||
"""Filter by death date"""
|
||||
death_date: DateCriterionInput
|
||||
"""Filter by creation time"""
|
||||
created_at: TimestampCriterionInput
|
||||
"""Filter by last update time"""
|
||||
updated_at: TimestampCriterionInput
|
||||
}
|
||||
|
||||
input SceneMarkerFilterType {
|
||||
@@ -114,6 +136,16 @@ input SceneMarkerFilterType {
|
||||
scene_tags: HierarchicalMultiCriterionInput
|
||||
"""Filter to only include scene markers with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter by creation time"""
|
||||
created_at: TimestampCriterionInput
|
||||
"""Filter by last update time"""
|
||||
updated_at: TimestampCriterionInput
|
||||
"""Filter by scene date"""
|
||||
scene_date: DateCriterionInput
|
||||
"""Filter by cscene reation time"""
|
||||
scene_created_at: TimestampCriterionInput
|
||||
"""Filter by lscene ast update time"""
|
||||
scene_updated_at: TimestampCriterionInput
|
||||
}
|
||||
|
||||
input SceneFilterType {
|
||||
@@ -121,8 +153,11 @@ input SceneFilterType {
|
||||
OR: SceneFilterType
|
||||
NOT: SceneFilterType
|
||||
|
||||
id: IntCriterionInput
|
||||
title: StringCriterionInput
|
||||
code: StringCriterionInput
|
||||
details: StringCriterionInput
|
||||
director: StringCriterionInput
|
||||
|
||||
"""Filter by file oshash"""
|
||||
oshash: StringCriterionInput
|
||||
@@ -132,8 +167,12 @@ input SceneFilterType {
|
||||
phash: StringCriterionInput
|
||||
"""Filter by path"""
|
||||
path: StringCriterionInput
|
||||
"""Filter by file count"""
|
||||
file_count: IntCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter by organized"""
|
||||
organized: Boolean
|
||||
"""Filter by o-counter"""
|
||||
@@ -167,13 +206,29 @@ input SceneFilterType {
|
||||
"""Filter by performer count"""
|
||||
performer_count: IntCriterionInput
|
||||
"""Filter by StashID"""
|
||||
stash_id: StringCriterionInput
|
||||
stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead")
|
||||
"""Filter by StashID"""
|
||||
stash_id_endpoint: StashIDCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
"""Filter by interactive"""
|
||||
interactive: Boolean
|
||||
"""Filter by InteractiveSpeed"""
|
||||
interactive_speed: IntCriterionInput
|
||||
"""Filter by captions"""
|
||||
captions: StringCriterionInput
|
||||
"""Filter by resume time"""
|
||||
resume_time: IntCriterionInput
|
||||
"""Filter by play count"""
|
||||
play_count: IntCriterionInput
|
||||
"""Filter by play duration (in seconds)"""
|
||||
play_duration: IntCriterionInput
|
||||
"""Filter by date"""
|
||||
date: DateCriterionInput
|
||||
"""Filter by creation time"""
|
||||
created_at: TimestampCriterionInput
|
||||
"""Filter by last update time"""
|
||||
updated_at: TimestampCriterionInput
|
||||
}
|
||||
|
||||
input MovieFilterType {
|
||||
@@ -185,7 +240,9 @@ input MovieFilterType {
|
||||
"""Filter by duration (in seconds)"""
|
||||
duration: IntCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter to only include movies with this studio"""
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"""Filter to only include movies missing this property"""
|
||||
@@ -194,6 +251,12 @@ input MovieFilterType {
|
||||
url: StringCriterionInput
|
||||
"""Filter to only include movies where performer appears in a scene"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter by date"""
|
||||
date: DateCriterionInput
|
||||
"""Filter by creation time"""
|
||||
created_at: TimestampCriterionInput
|
||||
"""Filter by last update time"""
|
||||
updated_at: TimestampCriterionInput
|
||||
}
|
||||
|
||||
input StudioFilterType {
|
||||
@@ -206,11 +269,15 @@ input StudioFilterType {
|
||||
"""Filter to only include studios with this parent studio"""
|
||||
parents: MultiCriterionInput
|
||||
"""Filter by StashID"""
|
||||
stash_id: StringCriterionInput
|
||||
stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead")
|
||||
"""Filter by StashID"""
|
||||
stash_id_endpoint: StashIDCriterionInput
|
||||
"""Filter to only include studios missing this property"""
|
||||
is_missing: String
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter by scene count"""
|
||||
scene_count: IntCriterionInput
|
||||
"""Filter by image count"""
|
||||
@@ -223,6 +290,10 @@ input StudioFilterType {
|
||||
aliases: StringCriterionInput
|
||||
"""Filter by autotag ignore value"""
|
||||
ignore_auto_tag: Boolean
|
||||
"""Filter by creation time"""
|
||||
created_at: TimestampCriterionInput
|
||||
"""Filter by last update time"""
|
||||
updated_at: TimestampCriterionInput
|
||||
}
|
||||
|
||||
input GalleryFilterType {
|
||||
@@ -230,6 +301,7 @@ input GalleryFilterType {
|
||||
OR: GalleryFilterType
|
||||
NOT: GalleryFilterType
|
||||
|
||||
id: IntCriterionInput
|
||||
title: StringCriterionInput
|
||||
details: StringCriterionInput
|
||||
|
||||
@@ -237,12 +309,16 @@ input GalleryFilterType {
|
||||
checksum: StringCriterionInput
|
||||
"""Filter by path"""
|
||||
path: StringCriterionInput
|
||||
"""Filter by zip-file count"""
|
||||
file_count: IntCriterionInput
|
||||
"""Filter to only include galleries missing this property"""
|
||||
is_missing: String
|
||||
"""Filter to include/exclude galleries that were created from zip"""
|
||||
is_zip: Boolean
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter by organized"""
|
||||
organized: Boolean
|
||||
"""Filter by average image resolution"""
|
||||
@@ -267,6 +343,12 @@ input GalleryFilterType {
|
||||
image_count: IntCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
"""Filter by date"""
|
||||
date: DateCriterionInput
|
||||
"""Filter by creation time"""
|
||||
created_at: TimestampCriterionInput
|
||||
"""Filter by last update time"""
|
||||
updated_at: TimestampCriterionInput
|
||||
}
|
||||
|
||||
input TagFilterType {
|
||||
@@ -280,6 +362,9 @@ input TagFilterType {
|
||||
"""Filter by tag aliases"""
|
||||
aliases: StringCriterionInput
|
||||
|
||||
"""Filter by tag description"""
|
||||
description: StringCriterionInput
|
||||
|
||||
"""Filter to only include tags missing this property"""
|
||||
is_missing: String
|
||||
|
||||
@@ -312,6 +397,12 @@ input TagFilterType {
|
||||
|
||||
"""Filter by autotag ignore value"""
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
"""Filter by creation time"""
|
||||
created_at: TimestampCriterionInput
|
||||
|
||||
"""Filter by last update time"""
|
||||
updated_at: TimestampCriterionInput
|
||||
}
|
||||
|
||||
input ImageFilterType {
|
||||
@@ -321,12 +412,18 @@ input ImageFilterType {
|
||||
|
||||
title: StringCriterionInput
|
||||
|
||||
""" Filter by image id"""
|
||||
id: IntCriterionInput
|
||||
"""Filter by file checksum"""
|
||||
checksum: StringCriterionInput
|
||||
"""Filter by path"""
|
||||
path: StringCriterionInput
|
||||
"""Filter by file count"""
|
||||
file_count: IntCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"""Filter by organized"""
|
||||
organized: Boolean
|
||||
"""Filter by o-counter"""
|
||||
@@ -351,6 +448,10 @@ input ImageFilterType {
|
||||
performer_favorite: Boolean
|
||||
"""Filter to only include images with these galleries"""
|
||||
galleries: MultiCriterionInput
|
||||
"""Filter by creation time"""
|
||||
created_at: TimestampCriterionInput
|
||||
"""Filter by last update time"""
|
||||
updated_at: TimestampCriterionInput
|
||||
}
|
||||
|
||||
enum CriterionModifier {
|
||||
@@ -407,6 +508,18 @@ input HierarchicalMultiCriterionInput {
|
||||
depth: Int
|
||||
}
|
||||
|
||||
input DateCriterionInput {
|
||||
value: String!
|
||||
value2: String
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input TimestampCriterionInput {
|
||||
value: String!
|
||||
value2: String
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
enum FilterMode {
|
||||
SCENES,
|
||||
PERFORMERS,
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
"""Gallery type"""
|
||||
type Gallery {
|
||||
id: ID!
|
||||
checksum: String!
|
||||
path: String
|
||||
checksum: String! @deprecated(reason: "Use files.fingerprints")
|
||||
path: String @deprecated(reason: "Use files.path")
|
||||
title: String
|
||||
url: String
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean!
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
file_mod_time: Time
|
||||
file_mod_time: Time @deprecated(reason: "Use files.mod_time")
|
||||
|
||||
files: [GalleryFile!]!
|
||||
folder: Folder
|
||||
|
||||
scenes: [Scene!]!
|
||||
studio: Studio
|
||||
@@ -20,22 +26,19 @@ type Gallery {
|
||||
performers: [Performer!]!
|
||||
|
||||
"""The images in the gallery"""
|
||||
images: [Image!]! # Resolver
|
||||
images: [Image!]! @deprecated(reason: "Use findImages")
|
||||
cover: Image
|
||||
}
|
||||
|
||||
type GalleryFilesType {
|
||||
index: Int!
|
||||
name: String
|
||||
path: String
|
||||
}
|
||||
|
||||
input GalleryCreateInput {
|
||||
title: String!
|
||||
url: String
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
scene_ids: [ID!]
|
||||
studio_id: ID
|
||||
@@ -50,12 +53,17 @@ input GalleryUpdateInput {
|
||||
url: String
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
scene_ids: [ID!]
|
||||
studio_id: ID
|
||||
tag_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
|
||||
primary_file_id: ID
|
||||
}
|
||||
|
||||
input BulkGalleryUpdateInput {
|
||||
@@ -64,7 +72,10 @@ input BulkGalleryUpdateInput {
|
||||
url: String
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
scene_ids: BulkUpdateIds
|
||||
studio_id: ID
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
type Image {
|
||||
id: ID!
|
||||
checksum: String
|
||||
checksum: String @deprecated(reason: "Use files.fingerprints")
|
||||
title: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
o_counter: Int
|
||||
organized: Boolean!
|
||||
path: String!
|
||||
path: String! @deprecated(reason: "Use files.path")
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
file_mod_time: Time
|
||||
|
||||
file_mod_time: Time @deprecated(reason: "Use files.mod_time")
|
||||
|
||||
file: ImageFileType! # Resolver
|
||||
file: ImageFileType! @deprecated(reason: "Use files.mod_time")
|
||||
files: [ImageFile!]!
|
||||
paths: ImagePathsType! # Resolver
|
||||
|
||||
galleries: [Gallery!]!
|
||||
@@ -20,9 +25,10 @@ type Image {
|
||||
}
|
||||
|
||||
type ImageFileType {
|
||||
size: Int
|
||||
width: Int
|
||||
height: Int
|
||||
mod_time: Time!
|
||||
size: Int!
|
||||
width: Int!
|
||||
height: Int!
|
||||
}
|
||||
|
||||
type ImagePathsType {
|
||||
@@ -34,20 +40,28 @@ input ImageUpdateInput {
|
||||
clientMutationId: String
|
||||
id: ID!
|
||||
title: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
|
||||
studio_id: ID
|
||||
performer_ids: [ID!]
|
||||
tag_ids: [ID!]
|
||||
gallery_ids: [ID!]
|
||||
|
||||
primary_file_id: ID
|
||||
}
|
||||
|
||||
input BulkImageUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
title: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
|
||||
studio_id: ID
|
||||
|
||||
@@ -71,10 +71,19 @@ input ScanMetaDataFilterInput {
|
||||
input ScanMetadataInput {
|
||||
paths: [String!]
|
||||
|
||||
# useFileMetadata is deprecated with the new file management system
|
||||
# if this functionality is desired, then we can make a built in scraper instead.
|
||||
|
||||
"""Set name, date, details from metadata (if present)"""
|
||||
useFileMetadata: Boolean
|
||||
useFileMetadata: Boolean @deprecated(reason: "Not implemented")
|
||||
|
||||
# stripFileExtension is deprecated since we no longer set the title from the
|
||||
# filename - it is automatically returned if the object has no title. If this
|
||||
# functionality is desired, then we could make this an option to not include
|
||||
# the extension in the auto-generated title.
|
||||
|
||||
"""Strip file extension from title"""
|
||||
stripFileExtension: Boolean
|
||||
stripFileExtension: Boolean @deprecated(reason: "Not implemented")
|
||||
"""Generate previews during scan"""
|
||||
scanGeneratePreviews: Boolean
|
||||
"""Generate image previews during scan"""
|
||||
|
||||
@@ -6,7 +6,10 @@ type Movie {
|
||||
"""Duration in seconds"""
|
||||
duration: Int
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio: Studio
|
||||
director: String
|
||||
synopsis: String
|
||||
@@ -26,7 +29,10 @@ input MovieCreateInput {
|
||||
"""Duration in seconds"""
|
||||
duration: Int
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
@@ -43,7 +49,10 @@ input MovieUpdateInput {
|
||||
aliases: String
|
||||
duration: Int
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
@@ -57,7 +66,10 @@ input MovieUpdateInput {
|
||||
input BulkMovieUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ type Performer {
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
height: String
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
career_length: String
|
||||
@@ -36,7 +37,10 @@ type Performer {
|
||||
gallery_count: Int # Resolver
|
||||
scenes: [Scene!]!
|
||||
stash_ids: [StashID!]!
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
@@ -55,7 +59,9 @@ input PerformerCreateInput {
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
height: String
|
||||
# height must be parsable into an integer
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
career_length: String
|
||||
@@ -69,7 +75,10 @@ input PerformerCreateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
@@ -86,7 +95,9 @@ input PerformerUpdateInput {
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
height: String
|
||||
# height must be parsable into an integer
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
career_length: String
|
||||
@@ -100,7 +111,10 @@ input PerformerUpdateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
@@ -117,7 +131,9 @@ input BulkPerformerUpdateInput {
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
height: String
|
||||
# height must be parsable into an integer
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
career_length: String
|
||||
@@ -128,7 +144,10 @@ input BulkPerformerUpdateInput {
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
tag_ids: BulkUpdateIds
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
|
||||
@@ -4,4 +4,11 @@ Timestamp is a point in time. It is always output as RFC3339-compatible time poi
|
||||
It can be input as a RFC3339 string, or as "<4h" for "4 hours in the past" or ">5m"
|
||||
for "5 minutes in the future"
|
||||
"""
|
||||
scalar Timestamp
|
||||
scalar Timestamp
|
||||
|
||||
# generic JSON object
|
||||
scalar Map
|
||||
|
||||
scalar Any
|
||||
|
||||
scalar Int64
|
||||
@@ -15,10 +15,11 @@ type ScenePathsType {
|
||||
stream: String # Resolver
|
||||
webp: String # Resolver
|
||||
vtt: String # Resolver
|
||||
chapters_vtt: String # Resolver
|
||||
chapters_vtt: String @deprecated
|
||||
sprite: String # Resolver
|
||||
funscript: String # Resolver
|
||||
interactive_heatmap: String # Resolver
|
||||
caption: String # Resolver
|
||||
}
|
||||
|
||||
type SceneMovie {
|
||||
@@ -26,26 +27,46 @@ type SceneMovie {
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
type VideoCaption {
|
||||
language_code: String!
|
||||
caption_type: String!
|
||||
}
|
||||
|
||||
type Scene {
|
||||
id: ID!
|
||||
checksum: String
|
||||
oshash: String
|
||||
checksum: String @deprecated(reason: "Use files.fingerprints")
|
||||
oshash: String @deprecated(reason: "Use files.fingerprints")
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean!
|
||||
o_counter: Int
|
||||
path: String!
|
||||
phash: String
|
||||
path: String! @deprecated(reason: "Use files.path")
|
||||
phash: String @deprecated(reason: "Use files.fingerprints")
|
||||
interactive: Boolean!
|
||||
interactive_speed: Int
|
||||
captions: [VideoCaption!]
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
file_mod_time: Time
|
||||
"""The last time play count was updated"""
|
||||
last_played_at: Time
|
||||
"""The time index a scene was left at"""
|
||||
resume_time: Float
|
||||
"""The total time a scene has spent playing"""
|
||||
play_duration: Float
|
||||
"""The number ot times a scene has been played"""
|
||||
play_count: Int
|
||||
|
||||
file: SceneFileType! # Resolver
|
||||
file: SceneFileType! @deprecated(reason: "Use files")
|
||||
files: [VideoFile!]!
|
||||
paths: ScenePathsType! # Resolver
|
||||
|
||||
scene_markers: [SceneMarker!]!
|
||||
@@ -65,14 +86,17 @@ input SceneMovieInput {
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
input SceneUpdateInput {
|
||||
clientMutationId: String
|
||||
id: ID!
|
||||
input SceneCreateInput {
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
studio_id: ID
|
||||
gallery_ids: [ID!]
|
||||
@@ -82,6 +106,44 @@ input SceneUpdateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
cover_image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
|
||||
"""The first id will be assigned as primary. Files will be reassigned from
|
||||
existing scenes if applicable. Files must not already be primary for another scene"""
|
||||
file_ids: [ID!]
|
||||
}
|
||||
|
||||
input SceneUpdateInput {
|
||||
clientMutationId: String
|
||||
id: ID!
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
o_counter: Int
|
||||
organized: Boolean
|
||||
studio_id: ID
|
||||
gallery_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
movies: [SceneMovieInput!]
|
||||
tag_ids: [ID!]
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
cover_image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
|
||||
"""The time index a scene was left at"""
|
||||
resume_time: Float
|
||||
"""The total time a scene has spent playing"""
|
||||
play_duration: Float
|
||||
"""The number ot times a scene has been played"""
|
||||
play_count: Int
|
||||
|
||||
primary_file_id: ID
|
||||
}
|
||||
|
||||
enum BulkUpdateIdMode {
|
||||
@@ -99,10 +161,15 @@ input BulkSceneUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
studio_id: ID
|
||||
gallery_ids: BulkUpdateIds
|
||||
@@ -147,10 +214,15 @@ type SceneMovieID {
|
||||
type SceneParserResult {
|
||||
scene: Scene!
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
gallery_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
@@ -173,3 +245,17 @@ type SceneStreamEndpoint {
|
||||
mime_type: String
|
||||
label: String
|
||||
}
|
||||
|
||||
input AssignSceneFileInput {
|
||||
scene_id: ID!
|
||||
file_id: ID!
|
||||
}
|
||||
|
||||
input SceneMergeInput {
|
||||
"""If destination scene has no files, then the primary file of the
|
||||
first source scene will be assigned as primary"""
|
||||
source: [ID!]!
|
||||
destination: ID!
|
||||
# values defined here will override values in the destination
|
||||
values: SceneUpdateInput
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ type ScrapedTag {
|
||||
|
||||
type ScrapedScene {
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
|
||||
@@ -82,7 +84,9 @@ type ScrapedScene {
|
||||
|
||||
input ScrapedSceneInput {
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
director: String
|
||||
url: String
|
||||
date: String
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ type Studio {
|
||||
image_count: Int # Resolver
|
||||
gallery_count: Int # Resolver
|
||||
stash_ids: [StashID!]!
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
@@ -28,7 +31,10 @@ input StudioCreateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
@@ -42,7 +48,10 @@ input StudioUpdateInput {
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
type Tag {
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String
|
||||
aliases: [String!]!
|
||||
ignore_auto_tag: Boolean!
|
||||
created_at: Time!
|
||||
@@ -19,6 +20,7 @@ type Tag {
|
||||
|
||||
input TagCreateInput {
|
||||
name: String!
|
||||
description: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
@@ -32,6 +34,7 @@ input TagCreateInput {
|
||||
input TagUpdateInput {
|
||||
id: ID!
|
||||
name: String
|
||||
description: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
|
||||
@@ -94,7 +94,9 @@ fragment FingerprintFragment on Fingerprint {
|
||||
fragment SceneFragment on Scene {
|
||||
id
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
duration
|
||||
date
|
||||
urls {
|
||||
@@ -129,6 +131,12 @@ query FindScenesByFullFingerprints($fingerprints: [FingerprintQueryInput!]!) {
|
||||
}
|
||||
}
|
||||
|
||||
query FindScenesBySceneFingerprints($fingerprints: [[FingerprintQueryInput!]!]!) {
|
||||
findScenesBySceneFingerprints(fingerprints: $fingerprints) {
|
||||
...SceneFragment
|
||||
}
|
||||
}
|
||||
|
||||
query SearchScene($term: String!) {
|
||||
searchScene(term: $term) {
|
||||
...SceneFragment
|
||||
|
||||
@@ -26,7 +26,8 @@ const (
|
||||
)
|
||||
|
||||
func allowUnauthenticated(r *http.Request) bool {
|
||||
return strings.HasPrefix(r.URL.Path, loginEndPoint) || r.URL.Path == "/css"
|
||||
// #2715 - allow access to UI files
|
||||
return strings.HasPrefix(r.URL.Path, loginEndPoint) || r.URL.Path == "/css" || strings.HasPrefix(r.URL.Path, "/assets")
|
||||
}
|
||||
|
||||
func authenticateHandler() func(http.Handler) http.Handler {
|
||||
|
||||
@@ -3,7 +3,9 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -18,19 +20,35 @@ func getArgumentMap(ctx context.Context) map[string]interface{} {
|
||||
}
|
||||
|
||||
func getUpdateInputMap(ctx context.Context) map[string]interface{} {
|
||||
return getNamedUpdateInputMap(ctx, updateInputField)
|
||||
}
|
||||
|
||||
func getNamedUpdateInputMap(ctx context.Context, field string) map[string]interface{} {
|
||||
args := getArgumentMap(ctx)
|
||||
|
||||
input := args[updateInputField]
|
||||
var ret map[string]interface{}
|
||||
if input != nil {
|
||||
ret, _ = input.(map[string]interface{})
|
||||
// field can be qualified
|
||||
fields := strings.Split(field, ".")
|
||||
|
||||
currArgs := args
|
||||
|
||||
for _, f := range fields {
|
||||
v, found := currArgs[f]
|
||||
if !found {
|
||||
currArgs = nil
|
||||
break
|
||||
}
|
||||
|
||||
currArgs, _ = v.(map[string]interface{})
|
||||
if currArgs == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ret == nil {
|
||||
ret = make(map[string]interface{})
|
||||
if currArgs != nil {
|
||||
return currArgs
|
||||
}
|
||||
|
||||
return ret
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
func getUpdateInputMaps(ctx context.Context) []map[string]interface{} {
|
||||
@@ -89,6 +107,22 @@ func (t changesetTranslator) nullString(value *string, field string) *sql.NullSt
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t changesetTranslator) string(value *string, field string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return *value
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalString(value *string, field string) models.OptionalString {
|
||||
if !t.hasField(field) {
|
||||
return models.OptionalString{}
|
||||
}
|
||||
|
||||
return models.NewOptionalStringPtr(value)
|
||||
}
|
||||
|
||||
func (t changesetTranslator) sqliteDate(value *string, field string) *models.SQLiteDate {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
@@ -104,6 +138,42 @@ func (t changesetTranslator) sqliteDate(value *string, field string) *models.SQL
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalDate(value *string, field string) models.OptionalDate {
|
||||
if !t.hasField(field) {
|
||||
return models.OptionalDate{}
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
return models.OptionalDate{
|
||||
Set: true,
|
||||
Null: true,
|
||||
}
|
||||
}
|
||||
|
||||
return models.NewOptionalDate(models.NewDate(*value))
|
||||
}
|
||||
|
||||
func (t changesetTranslator) datePtr(value *string, field string) *models.Date {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
d := models.NewDate(*value)
|
||||
return &d
|
||||
}
|
||||
|
||||
func (t changesetTranslator) intPtrFromString(value *string, field string) (*int, error) {
|
||||
if value == nil || *value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
vv, err := strconv.Atoi(*value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting %v to int: %w", *value, err)
|
||||
}
|
||||
return &vv, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) nullInt64(value *int, field string) *sql.NullInt64 {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
@@ -119,6 +189,64 @@ func (t changesetTranslator) nullInt64(value *int, field string) *sql.NullInt64
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t changesetTranslator) ratingConversion(legacyValue *int, rating100Value *int) *sql.NullInt64 {
|
||||
const (
|
||||
legacyField = "rating"
|
||||
rating100Field = "rating100"
|
||||
)
|
||||
|
||||
legacyRating := t.nullInt64(legacyValue, legacyField)
|
||||
if legacyRating != nil {
|
||||
if legacyRating.Valid {
|
||||
legacyRating.Int64 = int64(models.Rating5To100(int(legacyRating.Int64)))
|
||||
}
|
||||
return legacyRating
|
||||
}
|
||||
return t.nullInt64(rating100Value, rating100Field)
|
||||
}
|
||||
|
||||
func (t changesetTranslator) ratingConversionInt(legacyValue *int, rating100Value *int) *int {
|
||||
const (
|
||||
legacyField = "rating"
|
||||
rating100Field = "rating100"
|
||||
)
|
||||
|
||||
legacyRating := t.optionalInt(legacyValue, legacyField)
|
||||
if legacyRating.Set && !(legacyRating.Null) {
|
||||
ret := int(models.Rating5To100(int(legacyRating.Value)))
|
||||
return &ret
|
||||
}
|
||||
|
||||
o := t.optionalInt(rating100Value, rating100Field)
|
||||
if o.Set && !(o.Null) {
|
||||
return &o.Value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) ratingConversionOptional(legacyValue *int, rating100Value *int) models.OptionalInt {
|
||||
const (
|
||||
legacyField = "rating"
|
||||
rating100Field = "rating100"
|
||||
)
|
||||
|
||||
legacyRating := t.optionalInt(legacyValue, legacyField)
|
||||
if legacyRating.Set && !(legacyRating.Null) {
|
||||
legacyRating.Value = int(models.Rating5To100(int(legacyRating.Value)))
|
||||
return legacyRating
|
||||
}
|
||||
return t.optionalInt(rating100Value, rating100Field)
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalInt(value *int, field string) models.OptionalInt {
|
||||
if !t.hasField(field) {
|
||||
return models.OptionalInt{}
|
||||
}
|
||||
|
||||
return models.NewOptionalIntPtr(value)
|
||||
}
|
||||
|
||||
func (t changesetTranslator) nullInt64FromString(value *string, field string) *sql.NullInt64 {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
@@ -134,17 +262,45 @@ func (t changesetTranslator) nullInt64FromString(value *string, field string) *s
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t changesetTranslator) nullBool(value *bool, field string) *sql.NullBool {
|
||||
func (t changesetTranslator) optionalIntFromString(value *string, field string) (models.OptionalInt, error) {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
return models.OptionalInt{}, nil
|
||||
}
|
||||
|
||||
ret := &sql.NullBool{}
|
||||
|
||||
if value != nil {
|
||||
ret.Bool = *value
|
||||
ret.Valid = true
|
||||
if value == nil {
|
||||
return models.OptionalInt{
|
||||
Set: true,
|
||||
Null: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return ret
|
||||
vv, err := strconv.Atoi(*value)
|
||||
if err != nil {
|
||||
return models.OptionalInt{}, fmt.Errorf("converting %v to int: %w", *value, err)
|
||||
}
|
||||
return models.NewOptionalInt(vv), nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) bool(value *bool, field string) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return *value
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalBool(value *bool, field string) models.OptionalBool {
|
||||
if !t.hasField(field) {
|
||||
return models.OptionalBool{}
|
||||
}
|
||||
|
||||
return models.NewOptionalBoolPtr(value)
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalFloat64(value *float64, field string) models.OptionalFloat64 {
|
||||
if !t.hasField(field) {
|
||||
return models.OptionalFloat64{}
|
||||
}
|
||||
|
||||
return models.NewOptionalFloat64Ptr(value)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/hash"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type imageBox struct {
|
||||
@@ -32,6 +33,10 @@ func newImageBox(box fs.FS) (*imageBox, error) {
|
||||
}
|
||||
|
||||
err := fs.WalkDir(box, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
@@ -82,7 +87,7 @@ func initialiseCustomImages() {
|
||||
}
|
||||
}
|
||||
|
||||
func getRandomPerformerImageUsingName(name, gender, customPath string) ([]byte, error) {
|
||||
func getRandomPerformerImageUsingName(name string, gender models.GenderEnum, customPath string) ([]byte, error) {
|
||||
var box *imageBox
|
||||
|
||||
// If we have a custom path, we should return a new box in the given path.
|
||||
@@ -91,10 +96,10 @@ func getRandomPerformerImageUsingName(name, gender, customPath string) ([]byte,
|
||||
}
|
||||
|
||||
if box == nil {
|
||||
switch strings.ToUpper(gender) {
|
||||
case "FEMALE":
|
||||
switch gender {
|
||||
case models.GenderEnumFemale:
|
||||
box = performerBox
|
||||
case "MALE":
|
||||
case models.GenderEnumMale:
|
||||
box = performerBoxMale
|
||||
default:
|
||||
box = performerBox
|
||||
|
||||
261
internal/api/loaders/dataloaders.go
Normal file
261
internal/api/loaders/dataloaders.go
Normal file
@@ -0,0 +1,261 @@
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden SceneLoader int *github.com/stashapp/stash/pkg/models.Scene
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden GalleryLoader int *github.com/stashapp/stash/pkg/models.Gallery
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden ImageLoader int *github.com/stashapp/stash/pkg/models.Image
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden PerformerLoader int *github.com/stashapp/stash/pkg/models.Performer
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden StudioLoader int *github.com/stashapp/stash/pkg/models.Studio
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden TagLoader int *github.com/stashapp/stash/pkg/models.Tag
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden MovieLoader int *github.com/stashapp/stash/pkg/models.Movie
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden FileLoader github.com/stashapp/stash/pkg/file.ID github.com/stashapp/stash/pkg/file.File
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
type contextKey struct{ name string }
|
||||
|
||||
var (
|
||||
loadersCtxKey = &contextKey{"loaders"}
|
||||
)
|
||||
|
||||
const (
|
||||
wait = 1 * time.Millisecond
|
||||
maxBatch = 100
|
||||
)
|
||||
|
||||
type Loaders struct {
|
||||
SceneByID *SceneLoader
|
||||
SceneFiles *SceneFileIDsLoader
|
||||
ImageFiles *ImageFileIDsLoader
|
||||
GalleryFiles *GalleryFileIDsLoader
|
||||
|
||||
GalleryByID *GalleryLoader
|
||||
ImageByID *ImageLoader
|
||||
PerformerByID *PerformerLoader
|
||||
StudioByID *StudioLoader
|
||||
TagByID *TagLoader
|
||||
MovieByID *MovieLoader
|
||||
FileByID *FileLoader
|
||||
}
|
||||
|
||||
type Middleware struct {
|
||||
DatabaseProvider txn.DatabaseProvider
|
||||
Repository manager.Repository
|
||||
}
|
||||
|
||||
func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ldrs := Loaders{
|
||||
SceneByID: &SceneLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenes(ctx),
|
||||
},
|
||||
GalleryByID: &GalleryLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGalleries(ctx),
|
||||
},
|
||||
ImageByID: &ImageLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchImages(ctx),
|
||||
},
|
||||
PerformerByID: &PerformerLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchPerformers(ctx),
|
||||
},
|
||||
StudioByID: &StudioLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchStudios(ctx),
|
||||
},
|
||||
TagByID: &TagLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchTags(ctx),
|
||||
},
|
||||
MovieByID: &MovieLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchMovies(ctx),
|
||||
},
|
||||
FileByID: &FileLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchFiles(ctx),
|
||||
},
|
||||
SceneFiles: &SceneFileIDsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesFileIDs(ctx),
|
||||
},
|
||||
ImageFiles: &ImageFileIDsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchImagesFileIDs(ctx),
|
||||
},
|
||||
GalleryFiles: &GalleryFileIDsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGalleriesFileIDs(ctx),
|
||||
},
|
||||
}
|
||||
|
||||
newCtx := context.WithValue(r.Context(), loadersCtxKey, ldrs)
|
||||
next.ServeHTTP(w, r.WithContext(newCtx))
|
||||
})
|
||||
}
|
||||
|
||||
func From(ctx context.Context) Loaders {
|
||||
return ctx.Value(loadersCtxKey).(Loaders)
|
||||
}
|
||||
|
||||
func toErrorSlice(err error) []error {
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Middleware) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
return txn.WithDatabase(ctx, m.DatabaseProvider, fn)
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models.Scene, []error) {
|
||||
return func(keys []int) (ret []*models.Scene, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.FindMany(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models.Image, []error) {
|
||||
return func(keys []int) (ret []*models.Image, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Image.FindMany(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchGalleries(ctx context.Context) func(keys []int) ([]*models.Gallery, []error) {
|
||||
return func(keys []int) (ret []*models.Gallery, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Gallery.FindMany(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*models.Performer, []error) {
|
||||
return func(keys []int) (ret []*models.Performer, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Performer.FindMany(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*models.Studio, []error) {
|
||||
return func(keys []int) (ret []*models.Studio, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Studio.FindMany(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.Tag, []error) {
|
||||
return func(keys []int) (ret []*models.Tag, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Tag.FindMany(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models.Movie, []error) {
|
||||
return func(keys []int) (ret []*models.Movie, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Movie.FindMany(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchFiles(ctx context.Context) func(keys []file.ID) ([]file.File, []error) {
|
||||
return func(keys []file.ID) (ret []file.File, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.File.Find(ctx, keys...)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
|
||||
return func(keys []int) (ret [][]file.ID, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyFileIDs(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
|
||||
return func(keys []int) (ret [][]file.ID, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Image.GetManyFileIDs(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
|
||||
return func(keys []int) (ret [][]file.ID, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Gallery.GetManyFileIDs(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
221
internal/api/loaders/fileloader_gen.go
Normal file
221
internal/api/loaders/fileloader_gen.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
)
|
||||
|
||||
// FileLoaderConfig captures the config to create a new FileLoader
|
||||
type FileLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []file.ID) ([]file.File, []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
|
||||
}
|
||||
|
||||
// NewFileLoader creates a new FileLoader given a fetch, wait, and maxBatch
|
||||
func NewFileLoader(config FileLoaderConfig) *FileLoader {
|
||||
return &FileLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// FileLoader batches and caches requests
|
||||
type FileLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []file.ID) ([]file.File, []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[file.ID]file.File
|
||||
|
||||
// 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 *fileLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type fileLoaderBatch struct {
|
||||
keys []file.ID
|
||||
data []file.File
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a File by key, batching and caching will be applied automatically
|
||||
func (l *FileLoader) Load(key file.ID) (file.File, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a File.
|
||||
// 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 *FileLoader) LoadThunk(key file.ID) func() (file.File, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (file.File, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &fileLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (file.File, error) {
|
||||
<-batch.done
|
||||
|
||||
var data file.File
|
||||
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 *FileLoader) LoadAll(keys []file.ID) ([]file.File, []error) {
|
||||
results := make([]func() (file.File, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
files := make([]file.File, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
files[i], errors[i] = thunk()
|
||||
}
|
||||
return files, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Files.
|
||||
// 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 *FileLoader) LoadAllThunk(keys []file.ID) func() ([]file.File, []error) {
|
||||
results := make([]func() (file.File, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]file.File, []error) {
|
||||
files := make([]file.File, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
files[i], errors[i] = thunk()
|
||||
}
|
||||
return files, 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 *FileLoader) Prime(key file.ID, value file.File) 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 *FileLoader) Clear(key file.ID) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *FileLoader) unsafeSet(key file.ID, value file.File) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[file.ID]file.File{}
|
||||
}
|
||||
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 *fileLoaderBatch) keyIndex(l *FileLoader, key file.ID) 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 *fileLoaderBatch) startTimer(l *FileLoader) {
|
||||
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 *fileLoaderBatch) end(l *FileLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
225
internal/api/loaders/galleryfileidsloader_gen.go
Normal file
225
internal/api/loaders/galleryfileidsloader_gen.go
Normal file
@@ -0,0 +1,225 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
)
|
||||
|
||||
// GalleryFileIDsLoaderConfig captures the config to create a new GalleryFileIDsLoader
|
||||
type GalleryFileIDsLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([][]file.ID, []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
|
||||
}
|
||||
|
||||
// NewGalleryFileIDsLoader creates a new GalleryFileIDsLoader given a fetch, wait, and maxBatch
|
||||
func NewGalleryFileIDsLoader(config GalleryFileIDsLoaderConfig) *GalleryFileIDsLoader {
|
||||
return &GalleryFileIDsLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// GalleryFileIDsLoader batches and caches requests
|
||||
type GalleryFileIDsLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([][]file.ID, []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][]file.ID
|
||||
|
||||
// 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 *galleryFileIDsLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type galleryFileIDsLoaderBatch struct {
|
||||
keys []int
|
||||
data [][]file.ID
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a ID by key, batching and caching will be applied automatically
|
||||
func (l *GalleryFileIDsLoader) Load(key int) ([]file.ID, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a ID.
|
||||
// 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 *GalleryFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]file.ID, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &galleryFileIDsLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]file.ID, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []file.ID
|
||||
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 *GalleryFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a IDs.
|
||||
// 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 *GalleryFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]file.ID, []error) {
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, 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 *GalleryFileIDsLoader) Prime(key int, value []file.ID) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]file.ID, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *GalleryFileIDsLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *GalleryFileIDsLoader) unsafeSet(key int, value []file.ID) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int][]file.ID{}
|
||||
}
|
||||
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 *galleryFileIDsLoaderBatch) keyIndex(l *GalleryFileIDsLoader, 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 *galleryFileIDsLoaderBatch) startTimer(l *GalleryFileIDsLoader) {
|
||||
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 *galleryFileIDsLoaderBatch) end(l *GalleryFileIDsLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
224
internal/api/loaders/galleryloader_gen.go
Normal file
224
internal/api/loaders/galleryloader_gen.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// GalleryLoaderConfig captures the config to create a new GalleryLoader
|
||||
type GalleryLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*models.Gallery, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewGalleryLoader creates a new GalleryLoader given a fetch, wait, and maxBatch
|
||||
func NewGalleryLoader(config GalleryLoaderConfig) *GalleryLoader {
|
||||
return &GalleryLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// GalleryLoader batches and caches requests
|
||||
type GalleryLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]*models.Gallery, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]*models.Gallery
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *galleryLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type galleryLoaderBatch struct {
|
||||
keys []int
|
||||
data []*models.Gallery
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Gallery by key, batching and caching will be applied automatically
|
||||
func (l *GalleryLoader) Load(key int) (*models.Gallery, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Gallery.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *GalleryLoader) LoadThunk(key int) func() (*models.Gallery, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (*models.Gallery, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &galleryLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (*models.Gallery, error) {
|
||||
<-batch.done
|
||||
|
||||
var data *models.Gallery
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *GalleryLoader) LoadAll(keys []int) ([]*models.Gallery, []error) {
|
||||
results := make([]func() (*models.Gallery, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
gallerys := make([]*models.Gallery, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
gallerys[i], errors[i] = thunk()
|
||||
}
|
||||
return gallerys, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Gallerys.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *GalleryLoader) LoadAllThunk(keys []int) func() ([]*models.Gallery, []error) {
|
||||
results := make([]func() (*models.Gallery, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]*models.Gallery, []error) {
|
||||
gallerys := make([]*models.Gallery, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
gallerys[i], errors[i] = thunk()
|
||||
}
|
||||
return gallerys, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *GalleryLoader) Prime(key int, value *models.Gallery) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := *value
|
||||
l.unsafeSet(key, &cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *GalleryLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *GalleryLoader) unsafeSet(key int, value *models.Gallery) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]*models.Gallery{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *galleryLoaderBatch) keyIndex(l *GalleryLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *galleryLoaderBatch) startTimer(l *GalleryLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *galleryLoaderBatch) end(l *GalleryLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
225
internal/api/loaders/imagefileidsloader_gen.go
Normal file
225
internal/api/loaders/imagefileidsloader_gen.go
Normal file
@@ -0,0 +1,225 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
)
|
||||
|
||||
// ImageFileIDsLoaderConfig captures the config to create a new ImageFileIDsLoader
|
||||
type ImageFileIDsLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([][]file.ID, []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
|
||||
}
|
||||
|
||||
// NewImageFileIDsLoader creates a new ImageFileIDsLoader given a fetch, wait, and maxBatch
|
||||
func NewImageFileIDsLoader(config ImageFileIDsLoaderConfig) *ImageFileIDsLoader {
|
||||
return &ImageFileIDsLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// ImageFileIDsLoader batches and caches requests
|
||||
type ImageFileIDsLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([][]file.ID, []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][]file.ID
|
||||
|
||||
// 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 *imageFileIDsLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type imageFileIDsLoaderBatch struct {
|
||||
keys []int
|
||||
data [][]file.ID
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a ID by key, batching and caching will be applied automatically
|
||||
func (l *ImageFileIDsLoader) Load(key int) ([]file.ID, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a ID.
|
||||
// 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 *ImageFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]file.ID, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &imageFileIDsLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]file.ID, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []file.ID
|
||||
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 *ImageFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a IDs.
|
||||
// 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 *ImageFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]file.ID, []error) {
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, 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 *ImageFileIDsLoader) Prime(key int, value []file.ID) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]file.ID, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *ImageFileIDsLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *ImageFileIDsLoader) unsafeSet(key int, value []file.ID) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int][]file.ID{}
|
||||
}
|
||||
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 *imageFileIDsLoaderBatch) keyIndex(l *ImageFileIDsLoader, 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 *imageFileIDsLoaderBatch) startTimer(l *ImageFileIDsLoader) {
|
||||
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 *imageFileIDsLoaderBatch) end(l *ImageFileIDsLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
224
internal/api/loaders/imageloader_gen.go
Normal file
224
internal/api/loaders/imageloader_gen.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// ImageLoaderConfig captures the config to create a new ImageLoader
|
||||
type ImageLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*models.Image, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewImageLoader creates a new ImageLoader given a fetch, wait, and maxBatch
|
||||
func NewImageLoader(config ImageLoaderConfig) *ImageLoader {
|
||||
return &ImageLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// ImageLoader batches and caches requests
|
||||
type ImageLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]*models.Image, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]*models.Image
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *imageLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type imageLoaderBatch struct {
|
||||
keys []int
|
||||
data []*models.Image
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Image by key, batching and caching will be applied automatically
|
||||
func (l *ImageLoader) Load(key int) (*models.Image, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Image.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *ImageLoader) LoadThunk(key int) func() (*models.Image, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (*models.Image, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &imageLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (*models.Image, error) {
|
||||
<-batch.done
|
||||
|
||||
var data *models.Image
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *ImageLoader) LoadAll(keys []int) ([]*models.Image, []error) {
|
||||
results := make([]func() (*models.Image, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
images := make([]*models.Image, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
images[i], errors[i] = thunk()
|
||||
}
|
||||
return images, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Images.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *ImageLoader) LoadAllThunk(keys []int) func() ([]*models.Image, []error) {
|
||||
results := make([]func() (*models.Image, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]*models.Image, []error) {
|
||||
images := make([]*models.Image, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
images[i], errors[i] = thunk()
|
||||
}
|
||||
return images, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *ImageLoader) Prime(key int, value *models.Image) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := *value
|
||||
l.unsafeSet(key, &cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *ImageLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *ImageLoader) unsafeSet(key int, value *models.Image) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]*models.Image{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *imageLoaderBatch) keyIndex(l *ImageLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *imageLoaderBatch) startTimer(l *ImageLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *imageLoaderBatch) end(l *ImageLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
224
internal/api/loaders/movieloader_gen.go
Normal file
224
internal/api/loaders/movieloader_gen.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// MovieLoaderConfig captures the config to create a new MovieLoader
|
||||
type MovieLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*models.Movie, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewMovieLoader creates a new MovieLoader given a fetch, wait, and maxBatch
|
||||
func NewMovieLoader(config MovieLoaderConfig) *MovieLoader {
|
||||
return &MovieLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// MovieLoader batches and caches requests
|
||||
type MovieLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]*models.Movie, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]*models.Movie
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *movieLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type movieLoaderBatch struct {
|
||||
keys []int
|
||||
data []*models.Movie
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Movie by key, batching and caching will be applied automatically
|
||||
func (l *MovieLoader) Load(key int) (*models.Movie, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Movie.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *MovieLoader) LoadThunk(key int) func() (*models.Movie, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (*models.Movie, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &movieLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (*models.Movie, error) {
|
||||
<-batch.done
|
||||
|
||||
var data *models.Movie
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *MovieLoader) LoadAll(keys []int) ([]*models.Movie, []error) {
|
||||
results := make([]func() (*models.Movie, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
movies := make([]*models.Movie, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
movies[i], errors[i] = thunk()
|
||||
}
|
||||
return movies, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Movies.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *MovieLoader) LoadAllThunk(keys []int) func() ([]*models.Movie, []error) {
|
||||
results := make([]func() (*models.Movie, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]*models.Movie, []error) {
|
||||
movies := make([]*models.Movie, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
movies[i], errors[i] = thunk()
|
||||
}
|
||||
return movies, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *MovieLoader) Prime(key int, value *models.Movie) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := *value
|
||||
l.unsafeSet(key, &cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *MovieLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *MovieLoader) unsafeSet(key int, value *models.Movie) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]*models.Movie{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *movieLoaderBatch) keyIndex(l *MovieLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *movieLoaderBatch) startTimer(l *MovieLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *movieLoaderBatch) end(l *MovieLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
224
internal/api/loaders/performerloader_gen.go
Normal file
224
internal/api/loaders/performerloader_gen.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// PerformerLoaderConfig captures the config to create a new PerformerLoader
|
||||
type PerformerLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*models.Performer, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewPerformerLoader creates a new PerformerLoader given a fetch, wait, and maxBatch
|
||||
func NewPerformerLoader(config PerformerLoaderConfig) *PerformerLoader {
|
||||
return &PerformerLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// PerformerLoader batches and caches requests
|
||||
type PerformerLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]*models.Performer, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]*models.Performer
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *performerLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type performerLoaderBatch struct {
|
||||
keys []int
|
||||
data []*models.Performer
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Performer by key, batching and caching will be applied automatically
|
||||
func (l *PerformerLoader) Load(key int) (*models.Performer, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Performer.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *PerformerLoader) LoadThunk(key int) func() (*models.Performer, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (*models.Performer, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &performerLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (*models.Performer, error) {
|
||||
<-batch.done
|
||||
|
||||
var data *models.Performer
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *PerformerLoader) LoadAll(keys []int) ([]*models.Performer, []error) {
|
||||
results := make([]func() (*models.Performer, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
performers := make([]*models.Performer, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
performers[i], errors[i] = thunk()
|
||||
}
|
||||
return performers, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Performers.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *PerformerLoader) LoadAllThunk(keys []int) func() ([]*models.Performer, []error) {
|
||||
results := make([]func() (*models.Performer, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]*models.Performer, []error) {
|
||||
performers := make([]*models.Performer, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
performers[i], errors[i] = thunk()
|
||||
}
|
||||
return performers, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *PerformerLoader) Prime(key int, value *models.Performer) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := *value
|
||||
l.unsafeSet(key, &cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *PerformerLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *PerformerLoader) unsafeSet(key int, value *models.Performer) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]*models.Performer{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *performerLoaderBatch) keyIndex(l *PerformerLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *performerLoaderBatch) startTimer(l *PerformerLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *performerLoaderBatch) end(l *PerformerLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
225
internal/api/loaders/scenefileidsloader_gen.go
Normal file
225
internal/api/loaders/scenefileidsloader_gen.go
Normal file
@@ -0,0 +1,225 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
)
|
||||
|
||||
// SceneFileIDsLoaderConfig captures the config to create a new SceneFileIDsLoader
|
||||
type SceneFileIDsLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([][]file.ID, []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
|
||||
}
|
||||
|
||||
// NewSceneFileIDsLoader creates a new SceneFileIDsLoader given a fetch, wait, and maxBatch
|
||||
func NewSceneFileIDsLoader(config SceneFileIDsLoaderConfig) *SceneFileIDsLoader {
|
||||
return &SceneFileIDsLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// SceneFileIDsLoader batches and caches requests
|
||||
type SceneFileIDsLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([][]file.ID, []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][]file.ID
|
||||
|
||||
// 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 *sceneFileIDsLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type sceneFileIDsLoaderBatch struct {
|
||||
keys []int
|
||||
data [][]file.ID
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a ID by key, batching and caching will be applied automatically
|
||||
func (l *SceneFileIDsLoader) Load(key int) ([]file.ID, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a ID.
|
||||
// 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 *SceneFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]file.ID, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &sceneFileIDsLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]file.ID, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []file.ID
|
||||
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 *SceneFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a IDs.
|
||||
// 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 *SceneFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]file.ID, []error) {
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, 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 *SceneFileIDsLoader) Prime(key int, value []file.ID) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]file.ID, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *SceneFileIDsLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *SceneFileIDsLoader) unsafeSet(key int, value []file.ID) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int][]file.ID{}
|
||||
}
|
||||
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 *sceneFileIDsLoaderBatch) keyIndex(l *SceneFileIDsLoader, 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 *sceneFileIDsLoaderBatch) startTimer(l *SceneFileIDsLoader) {
|
||||
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 *sceneFileIDsLoaderBatch) end(l *SceneFileIDsLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
224
internal/api/loaders/sceneloader_gen.go
Normal file
224
internal/api/loaders/sceneloader_gen.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// SceneLoaderConfig captures the config to create a new SceneLoader
|
||||
type SceneLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*models.Scene, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewSceneLoader creates a new SceneLoader given a fetch, wait, and maxBatch
|
||||
func NewSceneLoader(config SceneLoaderConfig) *SceneLoader {
|
||||
return &SceneLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// SceneLoader batches and caches requests
|
||||
type SceneLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]*models.Scene, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]*models.Scene
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *sceneLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type sceneLoaderBatch struct {
|
||||
keys []int
|
||||
data []*models.Scene
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Scene by key, batching and caching will be applied automatically
|
||||
func (l *SceneLoader) Load(key int) (*models.Scene, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Scene.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *SceneLoader) LoadThunk(key int) func() (*models.Scene, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (*models.Scene, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &sceneLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (*models.Scene, error) {
|
||||
<-batch.done
|
||||
|
||||
var data *models.Scene
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *SceneLoader) LoadAll(keys []int) ([]*models.Scene, []error) {
|
||||
results := make([]func() (*models.Scene, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
scenes := make([]*models.Scene, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
scenes[i], errors[i] = thunk()
|
||||
}
|
||||
return scenes, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Scenes.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *SceneLoader) LoadAllThunk(keys []int) func() ([]*models.Scene, []error) {
|
||||
results := make([]func() (*models.Scene, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]*models.Scene, []error) {
|
||||
scenes := make([]*models.Scene, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
scenes[i], errors[i] = thunk()
|
||||
}
|
||||
return scenes, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *SceneLoader) Prime(key int, value *models.Scene) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := *value
|
||||
l.unsafeSet(key, &cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *SceneLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *SceneLoader) unsafeSet(key int, value *models.Scene) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]*models.Scene{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *sceneLoaderBatch) keyIndex(l *SceneLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *sceneLoaderBatch) startTimer(l *SceneLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *sceneLoaderBatch) end(l *SceneLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
224
internal/api/loaders/studioloader_gen.go
Normal file
224
internal/api/loaders/studioloader_gen.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// StudioLoaderConfig captures the config to create a new StudioLoader
|
||||
type StudioLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*models.Studio, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewStudioLoader creates a new StudioLoader given a fetch, wait, and maxBatch
|
||||
func NewStudioLoader(config StudioLoaderConfig) *StudioLoader {
|
||||
return &StudioLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// StudioLoader batches and caches requests
|
||||
type StudioLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]*models.Studio, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]*models.Studio
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *studioLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type studioLoaderBatch struct {
|
||||
keys []int
|
||||
data []*models.Studio
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Studio by key, batching and caching will be applied automatically
|
||||
func (l *StudioLoader) Load(key int) (*models.Studio, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Studio.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *StudioLoader) LoadThunk(key int) func() (*models.Studio, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (*models.Studio, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &studioLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (*models.Studio, error) {
|
||||
<-batch.done
|
||||
|
||||
var data *models.Studio
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *StudioLoader) LoadAll(keys []int) ([]*models.Studio, []error) {
|
||||
results := make([]func() (*models.Studio, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
studios := make([]*models.Studio, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
studios[i], errors[i] = thunk()
|
||||
}
|
||||
return studios, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Studios.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *StudioLoader) LoadAllThunk(keys []int) func() ([]*models.Studio, []error) {
|
||||
results := make([]func() (*models.Studio, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]*models.Studio, []error) {
|
||||
studios := make([]*models.Studio, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
studios[i], errors[i] = thunk()
|
||||
}
|
||||
return studios, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *StudioLoader) Prime(key int, value *models.Studio) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := *value
|
||||
l.unsafeSet(key, &cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *StudioLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *StudioLoader) unsafeSet(key int, value *models.Studio) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]*models.Studio{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *studioLoaderBatch) keyIndex(l *StudioLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *studioLoaderBatch) startTimer(l *StudioLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *studioLoaderBatch) end(l *StudioLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
224
internal/api/loaders/tagloader_gen.go
Normal file
224
internal/api/loaders/tagloader_gen.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// TagLoaderConfig captures the config to create a new TagLoader
|
||||
type TagLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*models.Tag, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewTagLoader creates a new TagLoader given a fetch, wait, and maxBatch
|
||||
func NewTagLoader(config TagLoaderConfig) *TagLoader {
|
||||
return &TagLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// TagLoader batches and caches requests
|
||||
type TagLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]*models.Tag, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]*models.Tag
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *tagLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type tagLoaderBatch struct {
|
||||
keys []int
|
||||
data []*models.Tag
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Tag by key, batching and caching will be applied automatically
|
||||
func (l *TagLoader) Load(key int) (*models.Tag, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Tag.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *TagLoader) LoadThunk(key int) func() (*models.Tag, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (*models.Tag, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &tagLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (*models.Tag, error) {
|
||||
<-batch.done
|
||||
|
||||
var data *models.Tag
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *TagLoader) LoadAll(keys []int) ([]*models.Tag, []error) {
|
||||
results := make([]func() (*models.Tag, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
tags := make([]*models.Tag, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
tags[i], errors[i] = thunk()
|
||||
}
|
||||
return tags, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Tags.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *TagLoader) LoadAllThunk(keys []int) func() ([]*models.Tag, []error) {
|
||||
results := make([]func() (*models.Tag, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]*models.Tag, []error) {
|
||||
tags := make([]*models.Tag, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
tags[i], errors[i] = thunk()
|
||||
}
|
||||
return tags, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *TagLoader) Prime(key int, value *models.Tag) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := *value
|
||||
l.unsafeSet(key, &cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *TagLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *TagLoader) unsafeSet(key int, value *models.Tag) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]*models.Tag{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *tagLoaderBatch) keyIndex(l *TagLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *tagLoaderBatch) startTimer(l *TagLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *tagLoaderBatch) end(l *TagLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
@@ -23,6 +23,17 @@ var matcher = language.NewMatcher([]language.Tag{
|
||||
language.MustParse("nl-NL"),
|
||||
language.MustParse("ru-RU"),
|
||||
language.MustParse("tr-TR"),
|
||||
language.MustParse("da-DK"),
|
||||
language.MustParse("pl-PL"),
|
||||
language.MustParse("ko-KR"),
|
||||
language.MustParse("cs-CZ"),
|
||||
language.MustParse("bn-BD"),
|
||||
language.MustParse("et-EE"),
|
||||
language.MustParse("fa-IR"),
|
||||
language.MustParse("hu-HU"),
|
||||
language.MustParse("ro-RO"),
|
||||
language.MustParse("th-TH"),
|
||||
language.MustParse("uk-UA"),
|
||||
})
|
||||
|
||||
// newCollator parses a locale into a collator
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -30,7 +31,12 @@ type hookExecutor interface {
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
txnManager models.TransactionManager
|
||||
txnManager txn.Manager
|
||||
repository manager.Repository
|
||||
sceneService manager.SceneService
|
||||
imageService manager.ImageService
|
||||
galleryService manager.GalleryService
|
||||
|
||||
hookExecutor hookExecutor
|
||||
}
|
||||
|
||||
@@ -38,37 +44,37 @@ func (r *Resolver) scraperCache() *scraper.Cache {
|
||||
return manager.GetInstance().ScraperCache
|
||||
}
|
||||
|
||||
func (r *Resolver) Gallery() models.GalleryResolver {
|
||||
func (r *Resolver) Gallery() GalleryResolver {
|
||||
return &galleryResolver{r}
|
||||
}
|
||||
func (r *Resolver) Mutation() models.MutationResolver {
|
||||
func (r *Resolver) Mutation() MutationResolver {
|
||||
return &mutationResolver{r}
|
||||
}
|
||||
func (r *Resolver) Performer() models.PerformerResolver {
|
||||
func (r *Resolver) Performer() PerformerResolver {
|
||||
return &performerResolver{r}
|
||||
}
|
||||
func (r *Resolver) Query() models.QueryResolver {
|
||||
func (r *Resolver) Query() QueryResolver {
|
||||
return &queryResolver{r}
|
||||
}
|
||||
func (r *Resolver) Scene() models.SceneResolver {
|
||||
func (r *Resolver) Scene() SceneResolver {
|
||||
return &sceneResolver{r}
|
||||
}
|
||||
func (r *Resolver) Image() models.ImageResolver {
|
||||
func (r *Resolver) Image() ImageResolver {
|
||||
return &imageResolver{r}
|
||||
}
|
||||
func (r *Resolver) SceneMarker() models.SceneMarkerResolver {
|
||||
func (r *Resolver) SceneMarker() SceneMarkerResolver {
|
||||
return &sceneMarkerResolver{r}
|
||||
}
|
||||
func (r *Resolver) Studio() models.StudioResolver {
|
||||
func (r *Resolver) Studio() StudioResolver {
|
||||
return &studioResolver{r}
|
||||
}
|
||||
func (r *Resolver) Movie() models.MovieResolver {
|
||||
func (r *Resolver) Movie() MovieResolver {
|
||||
return &movieResolver{r}
|
||||
}
|
||||
func (r *Resolver) Subscription() models.SubscriptionResolver {
|
||||
func (r *Resolver) Subscription() SubscriptionResolver {
|
||||
return &subscriptionResolver{r}
|
||||
}
|
||||
func (r *Resolver) Tag() models.TagResolver {
|
||||
func (r *Resolver) Tag() TagResolver {
|
||||
return &tagResolver{r}
|
||||
}
|
||||
|
||||
@@ -85,17 +91,17 @@ type studioResolver struct{ *Resolver }
|
||||
type movieResolver struct{ *Resolver }
|
||||
type tagResolver struct{ *Resolver }
|
||||
|
||||
func (r *Resolver) withTxn(ctx context.Context, fn func(r models.Repository) error) error {
|
||||
return r.txnManager.WithTxn(ctx, fn)
|
||||
func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
return txn.WithTxn(ctx, r.txnManager, fn)
|
||||
}
|
||||
|
||||
func (r *Resolver) withReadTxn(ctx context.Context, fn func(r models.ReaderRepository) error) error {
|
||||
return r.txnManager.WithReadTxn(ctx, fn)
|
||||
func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
return txn.WithReadTxn(ctx, r.txnManager, fn)
|
||||
}
|
||||
|
||||
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.SceneMarker().Wall(q)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.Wall(ctx, q)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -104,8 +110,8 @@ func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*model
|
||||
}
|
||||
|
||||
func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Scene().Wall(q)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Scene.Wall(ctx, q)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -115,8 +121,8 @@ func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models
|
||||
}
|
||||
|
||||
func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *string) (ret []*models.MarkerStringsResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.SceneMarker().GetMarkerStrings(q, sort)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.GetMarkerStrings(ctx, q, sort)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -125,28 +131,29 @@ func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *stri
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) {
|
||||
var ret models.StatsResultType
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
scenesQB := repo.Scene()
|
||||
imageQB := repo.Image()
|
||||
galleryQB := repo.Gallery()
|
||||
studiosQB := repo.Studio()
|
||||
performersQB := repo.Performer()
|
||||
moviesQB := repo.Movie()
|
||||
tagsQB := repo.Tag()
|
||||
scenesCount, _ := scenesQB.Count()
|
||||
scenesSize, _ := scenesQB.Size()
|
||||
scenesDuration, _ := scenesQB.Duration()
|
||||
imageCount, _ := imageQB.Count()
|
||||
imageSize, _ := imageQB.Size()
|
||||
galleryCount, _ := galleryQB.Count()
|
||||
performersCount, _ := performersQB.Count()
|
||||
studiosCount, _ := studiosQB.Count()
|
||||
moviesCount, _ := moviesQB.Count()
|
||||
tagsCount, _ := tagsQB.Count()
|
||||
func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
||||
var ret StatsResultType
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
repo := r.repository
|
||||
scenesQB := repo.Scene
|
||||
imageQB := repo.Image
|
||||
galleryQB := repo.Gallery
|
||||
studiosQB := repo.Studio
|
||||
performersQB := repo.Performer
|
||||
moviesQB := repo.Movie
|
||||
tagsQB := repo.Tag
|
||||
scenesCount, _ := scenesQB.Count(ctx)
|
||||
scenesSize, _ := scenesQB.Size(ctx)
|
||||
scenesDuration, _ := scenesQB.Duration(ctx)
|
||||
imageCount, _ := imageQB.Count(ctx)
|
||||
imageSize, _ := imageQB.Size(ctx)
|
||||
galleryCount, _ := galleryQB.Count(ctx)
|
||||
performersCount, _ := performersQB.Count(ctx)
|
||||
studiosCount, _ := studiosQB.Count(ctx)
|
||||
moviesCount, _ := moviesQB.Count(ctx)
|
||||
tagsCount, _ := tagsQB.Count(ctx)
|
||||
|
||||
ret = models.StatsResultType{
|
||||
ret = StatsResultType{
|
||||
SceneCount: scenesCount,
|
||||
ScenesSize: scenesSize,
|
||||
ScenesDuration: scenesDuration,
|
||||
@@ -167,10 +174,10 @@ func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, err
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Version(ctx context.Context) (*models.Version, error) {
|
||||
func (r *queryResolver) Version(ctx context.Context) (*Version, error) {
|
||||
version, hash, buildtime := GetVersion()
|
||||
|
||||
return &models.Version{
|
||||
return &Version{
|
||||
Version: &version,
|
||||
Hash: hash,
|
||||
BuildTime: buildtime,
|
||||
@@ -178,7 +185,7 @@ func (r *queryResolver) Version(ctx context.Context) (*models.Version, error) {
|
||||
}
|
||||
|
||||
// Latestversion returns the latest git shorthash commit.
|
||||
func (r *queryResolver) Latestversion(ctx context.Context) (*models.ShortVersion, error) {
|
||||
func (r *queryResolver) Latestversion(ctx context.Context) (*ShortVersion, error) {
|
||||
ver, url, err := GetLatestVersion(ctx, true)
|
||||
if err == nil {
|
||||
logger.Infof("Retrieved latest hash: %s", ver)
|
||||
@@ -186,37 +193,37 @@ func (r *queryResolver) Latestversion(ctx context.Context) (*models.ShortVersion
|
||||
logger.Errorf("Error while retrieving latest hash: %s", err)
|
||||
}
|
||||
|
||||
return &models.ShortVersion{
|
||||
return &ShortVersion{
|
||||
Shorthash: ver,
|
||||
URL: url,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Get scene marker tags which show up under the video.
|
||||
func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([]*models.SceneMarkerTag, error) {
|
||||
func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([]*SceneMarkerTag, error) {
|
||||
sceneID, err := strconv.Atoi(scene_id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var keys []int
|
||||
tags := make(map[int]*models.SceneMarkerTag)
|
||||
tags := make(map[int]*SceneMarkerTag)
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
sceneMarkers, err := repo.SceneMarker().FindBySceneID(sceneID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
sceneMarkers, err := r.repository.SceneMarker.FindBySceneID(ctx, sceneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tqb := repo.Tag()
|
||||
tqb := r.repository.Tag
|
||||
for _, sceneMarker := range sceneMarkers {
|
||||
markerPrimaryTag, err := tqb.Find(sceneMarker.PrimaryTagID)
|
||||
markerPrimaryTag, err := tqb.Find(ctx, sceneMarker.PrimaryTagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, hasKey := tags[markerPrimaryTag.ID]
|
||||
if !hasKey {
|
||||
sceneMarkerTag := &models.SceneMarkerTag{Tag: markerPrimaryTag}
|
||||
sceneMarkerTag := &SceneMarkerTag{Tag: markerPrimaryTag}
|
||||
tags[markerPrimaryTag.ID] = sceneMarkerTag
|
||||
keys = append(keys, markerPrimaryTag.ID)
|
||||
}
|
||||
@@ -235,10 +242,20 @@ func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([
|
||||
return a.SceneMarkers[0].Seconds < b.SceneMarkers[0].Seconds
|
||||
})
|
||||
|
||||
var result []*models.SceneMarkerTag
|
||||
var result []*SceneMarkerTag
|
||||
for _, key := range keys {
|
||||
result = append(result, tags[key])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func firstError(errs []error) error {
|
||||
for _, e := range errs {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,34 +2,135 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *galleryResolver) Path(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if obj.Path.Valid {
|
||||
return &obj.Path.String, nil
|
||||
func (r *galleryResolver) getPrimaryFile(ctx context.Context, obj *models.Gallery) (file.File, error) {
|
||||
if obj.PrimaryFileID != nil {
|
||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Title(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if obj.Title.Valid {
|
||||
return &obj.Title.String, nil
|
||||
func (r *galleryResolver) getFiles(ctx context.Context, obj *models.Gallery) ([]file.File, error) {
|
||||
fileIDs, err := loaders.From(ctx).GalleryFiles.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
|
||||
return files, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Files(ctx context.Context, obj *models.Gallery) ([]*GalleryFile, error) {
|
||||
files, err := r.getFiles(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]*GalleryFile, len(files))
|
||||
|
||||
for i, f := range files {
|
||||
base := f.Base()
|
||||
ret[i] = &GalleryFile{
|
||||
ID: strconv.Itoa(int(base.ID)),
|
||||
Path: base.Path,
|
||||
Basename: base.Basename,
|
||||
ParentFolderID: strconv.Itoa(int(base.ParentFolderID)),
|
||||
ModTime: base.ModTime,
|
||||
Size: base.Size,
|
||||
CreatedAt: base.CreatedAt,
|
||||
UpdatedAt: base.UpdatedAt,
|
||||
Fingerprints: resolveFingerprints(base),
|
||||
}
|
||||
|
||||
if base.ZipFileID != nil {
|
||||
zipFileID := strconv.Itoa(int(*base.ZipFileID))
|
||||
ret[i].ZipFileID = &zipFileID
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*Folder, error) {
|
||||
if obj.FolderID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var ret *file.Folder
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
ret, err = r.repository.Folder.Find(ctx, *obj.FolderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ret == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rr := &Folder{
|
||||
ID: ret.ID.String(),
|
||||
Path: ret.Path,
|
||||
ModTime: ret.ModTime,
|
||||
CreatedAt: ret.CreatedAt,
|
||||
UpdatedAt: ret.UpdatedAt,
|
||||
}
|
||||
|
||||
if ret.ParentFolderID != nil {
|
||||
pfidStr := ret.ParentFolderID.String()
|
||||
rr.ParentFolderID = &pfidStr
|
||||
}
|
||||
|
||||
if ret.ZipFileID != nil {
|
||||
zfidStr := ret.ZipFileID.String()
|
||||
rr.ZipFileID = &zfidStr
|
||||
}
|
||||
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f != nil {
|
||||
return &f.Base().ModTime, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Images is deprecated, slow and shouldn't be used
|
||||
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret []*models.Image, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// #2376 - sort images by path
|
||||
// doing this via Query is really slow, so stick with FindByGalleryID
|
||||
ret, err = repo.Image().FindByGalleryID(obj.ID)
|
||||
ret, err = r.repository.Image.FindByGalleryID(ctx, obj.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -43,25 +144,10 @@ func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *models.Image, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
// doing this via Query is really slow, so stick with FindByGalleryID
|
||||
imgs, err := repo.Image().FindByGalleryID(obj.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(imgs) > 0 {
|
||||
ret = imgs[0]
|
||||
}
|
||||
|
||||
for _, img := range imgs {
|
||||
if image.IsCover(img) {
|
||||
ret = img
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
// find cover.jpg first
|
||||
ret, err = image.FindGalleryCover(ctx, r.repository.Image, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -70,91 +156,91 @@ func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if obj.Date.Valid {
|
||||
result := utils.GetYMDFromDatabaseDate(obj.Date.String)
|
||||
if obj.Date != nil {
|
||||
result := obj.Date.String()
|
||||
return &result, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) URL(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if obj.URL.Valid {
|
||||
return &obj.URL.String, nil
|
||||
func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (string, error) {
|
||||
if !obj.Files.PrimaryLoaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadPrimaryFile(ctx, r.repository.File)
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Details(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if obj.Details.Valid {
|
||||
return &obj.Details.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
return obj.PrimaryChecksum(), nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Rating100(ctx context.Context, obj *models.Gallery) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Scene().FindByGalleryID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.SceneIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadSceneIDs(ctx, r.repository.Gallery)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).SceneByID.LoadAll(obj.SceneIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret *models.Studio, err error) {
|
||||
if !obj.StudioID.Valid {
|
||||
if obj.StudioID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Tag().FindByGalleryID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadTagIDs(ctx, r.repository.Gallery)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Performer().FindByGalleryID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.PerformerIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadPerformerIDs(ctx, r.repository.Gallery)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = repo.Image().CountByGalleryID(obj.ID)
|
||||
ret, err = r.repository.Image.CountByGalleryID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -162,15 +248,3 @@ func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) CreatedAt(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
|
||||
return &obj.CreatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) UpdatedAt(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
|
||||
return &obj.UpdatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
|
||||
return &obj.FileModTime.Timestamp, nil
|
||||
}
|
||||
|
||||
@@ -2,105 +2,192 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string, error) {
|
||||
ret := image.GetTitle(obj)
|
||||
return &ret, nil
|
||||
}
|
||||
func (r *imageResolver) getPrimaryFile(ctx context.Context, obj *models.Image) (*file.ImageFile, error) {
|
||||
if obj.PrimaryFileID != nil {
|
||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
ret, ok := f.(*file.ImageFile)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file %T is not an image file", f)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*models.ImageFileType, error) {
|
||||
width := int(obj.Width.Int64)
|
||||
height := int(obj.Height.Int64)
|
||||
size := int(obj.Size.Int64)
|
||||
return &models.ImageFileType{
|
||||
Size: &size,
|
||||
Width: &width,
|
||||
Height: &height,
|
||||
func (r *imageResolver) getFiles(ctx context.Context, obj *models.Image) ([]*file.ImageFile, error) {
|
||||
fileIDs, err := loaders.From(ctx).ImageFiles.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
|
||||
ret := make([]*file.ImageFile, len(files))
|
||||
for i, bf := range files {
|
||||
f, ok := bf.(*file.ImageFile)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file %T is not an image file", f)
|
||||
}
|
||||
|
||||
ret[i] = f
|
||||
}
|
||||
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string, error) {
|
||||
ret := obj.GetTitle()
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFileType, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
width := f.Width
|
||||
height := f.Height
|
||||
size := f.Size
|
||||
return &ImageFileType{
|
||||
Size: int(size),
|
||||
Width: width,
|
||||
Height: height,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*models.ImagePathsType, error) {
|
||||
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) {
|
||||
files, err := r.getFiles(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]*ImageFile, len(files))
|
||||
|
||||
for i, f := range files {
|
||||
ret[i] = &ImageFile{
|
||||
ID: strconv.Itoa(int(f.ID)),
|
||||
Path: f.Path,
|
||||
Basename: f.Basename,
|
||||
ParentFolderID: strconv.Itoa(int(f.ParentFolderID)),
|
||||
ModTime: f.ModTime,
|
||||
Size: f.Size,
|
||||
Width: f.Width,
|
||||
Height: f.Height,
|
||||
CreatedAt: f.CreatedAt,
|
||||
UpdatedAt: f.UpdatedAt,
|
||||
Fingerprints: resolveFingerprints(f.Base()),
|
||||
}
|
||||
|
||||
if f.ZipFileID != nil {
|
||||
zipFileID := strconv.Itoa(int(*f.ZipFileID))
|
||||
ret[i].ZipFileID = &zipFileID
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) FileModTime(ctx context.Context, obj *models.Image) (*time.Time, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f != nil {
|
||||
return &f.ModTime, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*ImagePathsType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
builder := urlbuilders.NewImageURLBuilder(baseURL, obj)
|
||||
thumbnailPath := builder.GetThumbnailURL()
|
||||
imagePath := builder.GetImageURL()
|
||||
return &models.ImagePathsType{
|
||||
return &ImagePathsType{
|
||||
Image: &imagePath,
|
||||
Thumbnail: &thumbnailPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret []*models.Gallery, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Gallery().FindByImageID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.GalleryIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadGalleryIDs(ctx, r.repository.Image)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).GalleryByID.LoadAll(obj.GalleryIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Rating100(ctx context.Context, obj *models.Image) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *models.Studio, err error) {
|
||||
if !obj.StudioID.Valid {
|
||||
if obj.StudioID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
|
||||
}
|
||||
|
||||
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindByImageID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadTagIDs(ctx, r.repository.Image)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret []*models.Performer, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().FindByImageID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.PerformerIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadPerformerIDs(ctx, r.repository.Image)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) CreatedAt(ctx context.Context, obj *models.Image) (*time.Time, error) {
|
||||
return &obj.CreatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) UpdatedAt(ctx context.Context, obj *models.Image) (*time.Time, error) {
|
||||
return &obj.UpdatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) FileModTime(ctx context.Context, obj *models.Image) (*time.Time, error) {
|
||||
return &obj.FileModTime.Timestamp, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -47,6 +48,14 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e
|
||||
}
|
||||
|
||||
func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := models.Rating100To5(int(obj.Rating.Int64))
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
@@ -56,14 +65,7 @@ func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, er
|
||||
|
||||
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *models.Studio, err error) {
|
||||
if obj.StudioID.Valid {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return loaders.From(ctx).StudioByID.Load(int(obj.StudioID.Int64))
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
@@ -92,9 +94,9 @@ func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (
|
||||
func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
// don't return any thing if there is no back image
|
||||
var img []byte
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
img, err = repo.Movie().GetBackImage(obj.ID)
|
||||
img, err = r.repository.Movie.GetBackImage(ctx, obj.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -115,8 +117,8 @@ func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*
|
||||
|
||||
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Scene().CountByMovieID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = r.repository.Scene.CountByMovieID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -126,9 +128,9 @@ func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret
|
||||
}
|
||||
|
||||
func (r *movieResolver) Scenes(ctx context.Context, obj *models.Movie) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = repo.Scene().FindByMovieID(obj.ID)
|
||||
ret, err = r.repository.Scene.FindByMovieID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -2,7 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
@@ -10,131 +10,26 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *performerResolver) Name(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Name.Valid {
|
||||
return &obj.Name.String, nil
|
||||
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Height != nil {
|
||||
ret := strconv.Itoa(*obj.Height)
|
||||
return &ret, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.URL.Valid {
|
||||
return &obj.URL.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Gender(ctx context.Context, obj *models.Performer) (*models.GenderEnum, error) {
|
||||
var ret models.GenderEnum
|
||||
|
||||
if obj.Gender.Valid {
|
||||
ret = models.GenderEnum(obj.Gender.String)
|
||||
if ret.IsValid() {
|
||||
return &ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Twitter.Valid {
|
||||
return &obj.Twitter.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Instagram(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Instagram.Valid {
|
||||
return &obj.Instagram.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
func (r *performerResolver) HeightCm(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
return obj.Height, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Birthdate(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Birthdate.Valid {
|
||||
return &obj.Birthdate.String, nil
|
||||
if obj.Birthdate != nil {
|
||||
ret := obj.Birthdate.String()
|
||||
return &ret, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Ethnicity(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Ethnicity.Valid {
|
||||
return &obj.Ethnicity.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Country(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Country.Valid {
|
||||
return &obj.Country.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) EyeColor(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.EyeColor.Valid {
|
||||
return &obj.EyeColor.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Height.Valid {
|
||||
return &obj.Height.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Measurements(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Measurements.Valid {
|
||||
return &obj.Measurements.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) FakeTits(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.FakeTits.Valid {
|
||||
return &obj.FakeTits.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) CareerLength(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.CareerLength.Valid {
|
||||
return &obj.CareerLength.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Tattoos(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Tattoos.Valid {
|
||||
return &obj.Tattoos.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Piercings(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Piercings.Valid {
|
||||
return &obj.Piercings.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Aliases(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Aliases.Valid {
|
||||
return &obj.Aliases.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Favorite(ctx context.Context, obj *models.Performer) (bool, error) {
|
||||
if obj.Favorite.Valid {
|
||||
return obj.Favorite.Bool, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
imagePath := urlbuilders.NewPerformerURLBuilder(baseURL, obj).GetPerformerImageURL()
|
||||
@@ -142,8 +37,8 @@ func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer
|
||||
}
|
||||
|
||||
func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindByPerformerID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.FindByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -154,8 +49,8 @@ func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (re
|
||||
|
||||
func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Scene().CountByPerformerID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = r.repository.Scene.CountByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -166,8 +61,8 @@ func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performe
|
||||
|
||||
func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = image.CountByPerformerID(repo.Image(), obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = image.CountByPerformerID(ctx, r.repository.Image, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -178,8 +73,8 @@ func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performe
|
||||
|
||||
func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = gallery.CountByPerformerID(repo.Gallery(), obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = gallery.CountByPerformerID(ctx, r.repository.Gallery, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -189,8 +84,8 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
|
||||
}
|
||||
|
||||
func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Scene().FindByPerformerID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Scene.FindByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -199,65 +94,42 @@ func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) (ret []*models.StashID, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().GetStashIDs(obj.ID)
|
||||
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) {
|
||||
var ret []models.StashID
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = r.repository.Performer.GetStashIDs(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return stashIDsSliceToPtrSlice(ret), nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Details(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Details.Valid {
|
||||
return &obj.Details.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
func (r *performerResolver) Rating100(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.DeathDate.Valid {
|
||||
return &obj.DeathDate.String, nil
|
||||
if obj.DeathDate != nil {
|
||||
ret := obj.DeathDate.String()
|
||||
return &ret, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) HairColor(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.HairColor.Valid {
|
||||
return &obj.HairColor.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Weight(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
if obj.Weight.Valid {
|
||||
weight := int(obj.Weight.Int64)
|
||||
return &weight, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) CreatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
|
||||
return &obj.CreatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) UpdatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
|
||||
return &obj.UpdatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().FindByPerformerID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -268,8 +140,8 @@ func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (
|
||||
|
||||
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Movie().CountByPerformerID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -2,105 +2,196 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *sceneResolver) Checksum(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
if obj.Checksum.Valid {
|
||||
return &obj.Checksum.String, nil
|
||||
func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (*file.VideoFile, error) {
|
||||
if obj.PrimaryFileID != nil {
|
||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret, ok := f.(*file.VideoFile)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file %T is not an image file", f)
|
||||
}
|
||||
|
||||
obj.Files.SetPrimary(ret)
|
||||
|
||||
return ret, nil
|
||||
} else {
|
||||
_ = obj.LoadPrimaryFile(ctx, r.repository.File)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Oshash(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
if obj.OSHash.Valid {
|
||||
return &obj.OSHash.String, nil
|
||||
func (r *sceneResolver) getFiles(ctx context.Context, obj *models.Scene) ([]*file.VideoFile, error) {
|
||||
fileIDs, err := loaders.From(ctx).SceneFiles.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
|
||||
ret := make([]*file.VideoFile, len(files))
|
||||
for i, bf := range files {
|
||||
f, ok := bf.(*file.VideoFile)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file %T is not a video file", f)
|
||||
}
|
||||
|
||||
ret[i] = f
|
||||
}
|
||||
|
||||
obj.Files.Set(ret)
|
||||
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Title(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
if obj.Title.Valid {
|
||||
return &obj.Title.String, nil
|
||||
func (r *sceneResolver) FileModTime(ctx context.Context, obj *models.Scene) (*time.Time, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Details(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
if obj.Details.Valid {
|
||||
return &obj.Details.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) URL(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
if obj.URL.Valid {
|
||||
return &obj.URL.String, nil
|
||||
if f != nil {
|
||||
return &f.ModTime, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Date(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
if obj.Date.Valid {
|
||||
result := utils.GetYMDFromDatabaseDate(obj.Date.String)
|
||||
if obj.Date != nil {
|
||||
result := obj.Date.String()
|
||||
return &result, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// File is deprecated
|
||||
func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.SceneFileType, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
bitrate := int(f.BitRate)
|
||||
size := strconv.FormatInt(f.Size, 10)
|
||||
|
||||
return &models.SceneFileType{
|
||||
Size: &size,
|
||||
Duration: handleFloat64(f.Duration),
|
||||
VideoCodec: &f.VideoCodec,
|
||||
AudioCodec: &f.AudioCodec,
|
||||
Width: &f.Width,
|
||||
Height: &f.Height,
|
||||
Framerate: handleFloat64(f.FrameRate),
|
||||
Bitrate: &bitrate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoFile, error) {
|
||||
files, err := r.getFiles(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]*VideoFile, len(files))
|
||||
|
||||
for i, f := range files {
|
||||
ret[i] = &VideoFile{
|
||||
ID: strconv.Itoa(int(f.ID)),
|
||||
Path: f.Path,
|
||||
Basename: f.Basename,
|
||||
ParentFolderID: strconv.Itoa(int(f.ParentFolderID)),
|
||||
ModTime: f.ModTime,
|
||||
Format: f.Format,
|
||||
Size: f.Size,
|
||||
Duration: handleFloat64Value(f.Duration),
|
||||
VideoCodec: f.VideoCodec,
|
||||
AudioCodec: f.AudioCodec,
|
||||
Width: f.Width,
|
||||
Height: f.Height,
|
||||
FrameRate: handleFloat64Value(f.FrameRate),
|
||||
BitRate: int(f.BitRate),
|
||||
CreatedAt: f.CreatedAt,
|
||||
UpdatedAt: f.UpdatedAt,
|
||||
Fingerprints: resolveFingerprints(f.Base()),
|
||||
}
|
||||
|
||||
if f.ZipFileID != nil {
|
||||
zipFileID := strconv.Itoa(int(*f.ZipFileID))
|
||||
ret[i].ZipFileID = &zipFileID
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) InteractiveSpeed(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
if obj.InteractiveSpeed.Valid {
|
||||
interactive_speed := int(obj.InteractiveSpeed.Int64)
|
||||
return &interactive_speed, nil
|
||||
func (r *sceneResolver) Rating100(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func resolveFingerprints(f *file.BaseFile) []*Fingerprint {
|
||||
ret := make([]*Fingerprint, len(f.Fingerprints))
|
||||
|
||||
for i, fp := range f.Fingerprints {
|
||||
ret[i] = &Fingerprint{
|
||||
Type: fp.Type,
|
||||
Value: formatFingerprint(fp.Fingerprint),
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.SceneFileType, error) {
|
||||
width := int(obj.Width.Int64)
|
||||
height := int(obj.Height.Int64)
|
||||
bitrate := int(obj.Bitrate.Int64)
|
||||
return &models.SceneFileType{
|
||||
Size: &obj.Size.String,
|
||||
Duration: handleFloat64(obj.Duration.Float64),
|
||||
VideoCodec: &obj.VideoCodec.String,
|
||||
AudioCodec: &obj.AudioCodec.String,
|
||||
Width: &width,
|
||||
Height: &height,
|
||||
Framerate: handleFloat64(obj.Framerate.Float64),
|
||||
Bitrate: &bitrate,
|
||||
}, nil
|
||||
func formatFingerprint(fp interface{}) string {
|
||||
switch v := fp.(type) {
|
||||
case int64:
|
||||
return strconv.FormatUint(uint64(v), 16)
|
||||
default:
|
||||
return fmt.Sprintf("%v", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.ScenePathsType, error) {
|
||||
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePathsType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
config := manager.GetInstance().Config
|
||||
builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
|
||||
builder.APIKey = config.GetAPIKey()
|
||||
screenshotPath := builder.GetScreenshotURL(obj.UpdatedAt.Timestamp)
|
||||
screenshotPath := builder.GetScreenshotURL(obj.UpdatedAt)
|
||||
previewPath := builder.GetStreamPreviewURL()
|
||||
streamPath := builder.GetStreamURL()
|
||||
streamPath := builder.GetStreamURL().String()
|
||||
webpPath := builder.GetStreamPreviewImageURL()
|
||||
vttPath := builder.GetSpriteVTTURL()
|
||||
spritePath := builder.GetSpriteURL()
|
||||
chaptersVttPath := builder.GetChaptersVTTURL()
|
||||
funscriptPath := builder.GetFunscriptURL()
|
||||
captionBasePath := builder.GetCaptionURL()
|
||||
interactiveHeatmap := builder.GetInteractiveHeatmapURL()
|
||||
|
||||
return &models.ScenePathsType{
|
||||
return &ScenePathsType{
|
||||
Screenshot: &screenshotPath,
|
||||
Preview: &previewPath,
|
||||
Stream: &streamPath,
|
||||
@@ -110,12 +201,13 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.S
|
||||
Sprite: &spritePath,
|
||||
Funscript: &funscriptPath,
|
||||
InteractiveHeatmap: &interactiveHeatmap,
|
||||
Caption: &captionBasePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (ret []*models.SceneMarker, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.SceneMarker().FindBySceneID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.FindBySceneID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -124,126 +216,186 @@ func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (re
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Gallery().FindBySceneID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []*models.VideoCaption, err error) {
|
||||
primaryFile, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *models.Studio, err error) {
|
||||
if !obj.StudioID.Valid {
|
||||
if primaryFile == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.File.GetCaptions(ctx, primaryFile.Base().ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*models.SceneMovie, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Scene()
|
||||
mqb := repo.Movie()
|
||||
|
||||
sceneMovies, err := qb.GetMovies(obj.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
|
||||
if !obj.GalleryIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadGalleryIDs(ctx, r.repository.Scene)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sm := range sceneMovies {
|
||||
movie, err := mqb.Find(sm.MovieID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sceneIdx := sm.SceneIndex
|
||||
sceneMovie := &models.SceneMovie{
|
||||
Movie: movie,
|
||||
}
|
||||
|
||||
if sceneIdx.Valid {
|
||||
idx := int(sceneIdx.Int64)
|
||||
sceneMovie.SceneIndex = &idx
|
||||
}
|
||||
|
||||
ret = append(ret, sceneMovie)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).GalleryByID.LoadAll(obj.GalleryIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *models.Studio, err error) {
|
||||
if obj.StudioID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) {
|
||||
if !obj.Movies.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
return obj.LoadMovies(ctx, qb)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
loader := loaders.From(ctx).MovieByID
|
||||
|
||||
for _, sm := range obj.Movies.List() {
|
||||
movie, err := loader.Load(sm.MovieID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sceneIdx := sm.SceneIndex
|
||||
sceneMovie := &SceneMovie{
|
||||
Movie: movie,
|
||||
SceneIndex: sceneIdx,
|
||||
}
|
||||
|
||||
ret = append(ret, sceneMovie)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindBySceneID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadTagIDs(ctx, r.repository.Scene)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().FindBySceneID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.PerformerIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadPerformerIDs(ctx, r.repository.Scene)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID {
|
||||
ret := make([]*models.StashID, len(v))
|
||||
for i, vv := range v {
|
||||
c := vv
|
||||
ret[i] = &c
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Scene().GetStashIDs(obj.ID)
|
||||
return err
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadStashIDs(ctx, r.repository.Scene)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Phash(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
if obj.Phash.Valid {
|
||||
hexval := utils.PhashToString(obj.Phash.Int64)
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
val := f.Fingerprints.Get(file.FingerprintTypePhash)
|
||||
if val == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
phash, _ := val.(int64)
|
||||
|
||||
if phash != 0 {
|
||||
hexval := utils.PhashToString(phash)
|
||||
return &hexval, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) CreatedAt(ctx context.Context, obj *models.Scene) (*time.Time, error) {
|
||||
return &obj.CreatedAt.Timestamp, nil
|
||||
}
|
||||
func (r *sceneResolver) SceneStreams(ctx context.Context, obj *models.Scene) ([]*manager.SceneStreamEndpoint, error) {
|
||||
// load the primary file into the scene
|
||||
_, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (r *sceneResolver) UpdatedAt(ctx context.Context, obj *models.Scene) (*time.Time, error) {
|
||||
return &obj.UpdatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) FileModTime(ctx context.Context, obj *models.Scene) (*time.Time, error) {
|
||||
return &obj.FileModTime.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) SceneStreams(ctx context.Context, obj *models.Scene) ([]*models.SceneStreamEndpoint, error) {
|
||||
config := manager.GetInstance().Config
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
|
||||
builder.APIKey = config.GetAPIKey()
|
||||
|
||||
return manager.GetSceneStreamPaths(obj, builder.GetStreamURL(), config.GetMaxStreamingTranscodeSize())
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Interactive(ctx context.Context, obj *models.Scene) (bool, error) {
|
||||
primaryFile, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if primaryFile == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return primaryFile.Interactive, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) InteractiveSpeed(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
primaryFile, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if primaryFile == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return primaryFile.InteractiveSpeed, nil
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker
|
||||
panic("Invalid scene id")
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
sceneID := int(obj.SceneID.Int64)
|
||||
ret, err = repo.Scene().Find(sceneID)
|
||||
ret, err = r.repository.Scene.Find(ctx, sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -25,8 +25,8 @@ func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker
|
||||
}
|
||||
|
||||
func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneMarker) (ret *models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().Find(obj.PrimaryTagID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.Find(ctx, obj.PrimaryTagID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -36,8 +36,8 @@ func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneM
|
||||
}
|
||||
|
||||
func (r *sceneMarkerResolver) Tags(ctx context.Context, obj *models.SceneMarker) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindBySceneMarkerID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.FindBySceneMarkerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
@@ -29,9 +30,9 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
|
||||
imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj).GetStudioImageURL()
|
||||
|
||||
var hasImage bool
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
hasImage, err = repo.Studio().HasImage(obj.ID)
|
||||
hasImage, err = r.repository.Studio.HasImage(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -46,8 +47,8 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
|
||||
}
|
||||
|
||||
func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) (ret []string, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().GetAliases(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Studio.GetAliases(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -58,8 +59,8 @@ func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) (ret [
|
||||
|
||||
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Scene().CountByStudioID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = r.repository.Scene.CountByStudioID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -70,8 +71,8 @@ func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (re
|
||||
|
||||
func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = image.CountByStudioID(repo.Image(), obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = image.CountByStudioID(ctx, r.repository.Image, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -82,8 +83,8 @@ func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (re
|
||||
|
||||
func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = gallery.CountByStudioID(repo.Gallery(), obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = gallery.CountByStudioID(ctx, r.repository.Gallery, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -97,19 +98,12 @@ func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().Find(int(obj.ParentID.Int64))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return loaders.From(ctx).StudioByID.Load(int(obj.ParentID.Int64))
|
||||
}
|
||||
|
||||
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (ret []*models.Studio, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().FindChildren(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Studio.FindChildren(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -118,18 +112,28 @@ func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) (ret []*models.StashID, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().GetStashIDs(obj.ID)
|
||||
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*models.StashID, error) {
|
||||
var ret []models.StashID
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = r.repository.Studio.GetStashIDs(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return stashIDsSliceToPtrSlice(ret), nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := models.Rating100To5(int(obj.Rating.Int64))
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
@@ -153,8 +157,8 @@ func (r *studioResolver) UpdatedAt(ctx context.Context, obj *models.Studio) (*ti
|
||||
}
|
||||
|
||||
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().FindByStudioID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -165,8 +169,8 @@ func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []
|
||||
|
||||
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Movie().CountByStudioID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = r.repository.Movie.CountByStudioID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -10,9 +10,16 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *tagResolver) Description(ctx context.Context, obj *models.Tag) (*string, error) {
|
||||
if obj.Description.Valid {
|
||||
return &obj.Description.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindByChildTagID(obj.ID)
|
||||
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
|
||||
@@ -22,8 +29,8 @@ func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*mode
|
||||
}
|
||||
|
||||
func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindByParentTagID(obj.ID)
|
||||
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
|
||||
@@ -33,8 +40,8 @@ func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*mod
|
||||
}
|
||||
|
||||
func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []string, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().GetAliases(obj.ID)
|
||||
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
|
||||
@@ -45,8 +52,8 @@ func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []strin
|
||||
|
||||
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||
var count int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
count, err = repo.Scene().CountByTagID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
count, err = r.repository.Scene.CountByTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -57,8 +64,8 @@ func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (ret *int
|
||||
|
||||
func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||
var count int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
count, err = repo.SceneMarker().CountByTagID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
count, err = r.repository.SceneMarker.CountByTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -69,8 +76,8 @@ func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (re
|
||||
|
||||
func (r *tagResolver) ImageCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = image.CountByTagID(repo.Image(), obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = image.CountByTagID(ctx, r.repository.Image, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -81,8 +88,8 @@ func (r *tagResolver) ImageCount(ctx context.Context, obj *models.Tag) (ret *int
|
||||
|
||||
func (r *tagResolver) GalleryCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = gallery.CountByTagID(repo.Gallery(), obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = gallery.CountByTagID(ctx, r.repository.Gallery, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -93,8 +100,8 @@ func (r *tagResolver) GalleryCount(ctx context.Context, obj *models.Tag) (ret *i
|
||||
|
||||
func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||
var count int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
count, err = repo.Performer().CountByTagID(obj.ID)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
count, err = r.repository.Performer.CountByTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -15,21 +15,21 @@ import (
|
||||
|
||||
var ErrOverriddenConfig = errors.New("cannot set overridden value")
|
||||
|
||||
func (r *mutationResolver) Setup(ctx context.Context, input models.SetupInput) (bool, error) {
|
||||
func (r *mutationResolver) Setup(ctx context.Context, input manager.SetupInput) (bool, error) {
|
||||
err := manager.GetInstance().Setup(ctx, input)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) Migrate(ctx context.Context, input models.MigrateInput) (bool, error) {
|
||||
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 models.ConfigGeneralInput) (*models.ConfigGeneralResult, error) {
|
||||
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGeneralInput) (*ConfigGeneralResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
existingPaths := c.GetStashPaths()
|
||||
if len(input.Stashes) > 0 {
|
||||
if input.Stashes != nil {
|
||||
for _, s := range input.Stashes {
|
||||
// Only validate existence of new paths
|
||||
isNew := true
|
||||
@@ -58,7 +58,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
}
|
||||
|
||||
validateDir := func(key string, value string, optional bool) error {
|
||||
if err := checkConfigOverride(config.Metadata); err != nil {
|
||||
if err := checkConfigOverride(key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -84,6 +84,15 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
c.Set(config.Database, input.DatabasePath)
|
||||
}
|
||||
|
||||
existingBackupDirectoryPath := c.GetBackupDirectoryPath()
|
||||
if input.BackupDirectoryPath != nil && existingBackupDirectoryPath != *input.BackupDirectoryPath {
|
||||
if err := validateDir(config.BackupDirectoryPath, *input.BackupDirectoryPath, true); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.BackupDirectoryPath, input.BackupDirectoryPath)
|
||||
}
|
||||
|
||||
existingGeneratedPath := c.GetGeneratedPath()
|
||||
if input.GeneratedPath != nil && existingGeneratedPath != *input.GeneratedPath {
|
||||
if err := validateDir(config.Generated, *input.GeneratedPath, false); err != nil {
|
||||
@@ -132,7 +141,9 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
}
|
||||
|
||||
// validate changing VideoFileNamingAlgorithm
|
||||
if err := manager.ValidateVideoFileNamingAlgorithm(r.txnManager, *input.VideoFileNamingAlgorithm); err != nil {
|
||||
if err := r.withTxn(context.TODO(), func(ctx context.Context) error {
|
||||
return manager.ValidateVideoFileNamingAlgorithm(ctx, r.repository.Scene, *input.VideoFileNamingAlgorithm)
|
||||
}); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
@@ -281,7 +292,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
return makeConfigGeneralResult(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) {
|
||||
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
setBool := func(key string, v *bool) {
|
||||
@@ -338,10 +349,14 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
||||
c.Set(config.ImageLightboxSlideshowDelay, *options.SlideshowDelay)
|
||||
}
|
||||
|
||||
setString(config.ImageLightboxDisplayMode, (*string)(options.DisplayMode))
|
||||
setString(config.ImageLightboxDisplayModeKey, (*string)(options.DisplayMode))
|
||||
setBool(config.ImageLightboxScaleUp, options.ScaleUp)
|
||||
setBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
|
||||
setString(config.ImageLightboxScrollMode, (*string)(options.ScrollMode))
|
||||
setString(config.ImageLightboxScrollModeKey, (*string)(options.ScrollMode))
|
||||
|
||||
if options.ScrollAttemptsBeforeChange != nil {
|
||||
c.Set(config.ImageLightboxScrollAttemptsBeforeChange, *options.ScrollAttemptsBeforeChange)
|
||||
}
|
||||
}
|
||||
|
||||
if input.CSS != nil {
|
||||
@@ -350,6 +365,18 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
||||
|
||||
setBool(config.CSSEnabled, input.CSSEnabled)
|
||||
|
||||
if input.Javascript != nil {
|
||||
c.SetJavascript(*input.Javascript)
|
||||
}
|
||||
|
||||
setBool(config.JavascriptEnabled, input.JavascriptEnabled)
|
||||
|
||||
if input.CustomLocales != nil {
|
||||
c.SetCustomLocales(*input.CustomLocales)
|
||||
}
|
||||
|
||||
setBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
|
||||
|
||||
if input.DisableDropdownCreate != nil {
|
||||
ddc := input.DisableDropdownCreate
|
||||
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
|
||||
@@ -372,7 +399,7 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
||||
return makeConfigInterfaceResult(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureDlna(ctx context.Context, input models.ConfigDLNAInput) (*models.ConfigDLNAResult, error) {
|
||||
func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAInput) (*ConfigDLNAResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
if input.ServerName != nil {
|
||||
@@ -409,7 +436,7 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input models.Confi
|
||||
return makeConfigDLNAResult(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureScraping(ctx context.Context, input models.ConfigScrapingInput) (*models.ConfigScrapingResult, error) {
|
||||
func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigScrapingInput) (*ConfigScrapingResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
refreshScraperCache := false
|
||||
@@ -441,7 +468,7 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input models.C
|
||||
return makeConfigScrapingResult(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.ConfigDefaultSettingsInput) (*models.ConfigDefaultSettingsResult, error) {
|
||||
func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input ConfigDefaultSettingsInput) (*ConfigDefaultSettingsResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
if input.Identify != nil {
|
||||
@@ -449,7 +476,7 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.C
|
||||
}
|
||||
|
||||
if input.Scan != nil {
|
||||
c.Set(config.DefaultScanSettings, input.Scan)
|
||||
c.Set(config.DefaultScanSettings, input.Scan.ScanMetadataOptions)
|
||||
}
|
||||
|
||||
if input.AutoTag != nil {
|
||||
@@ -475,7 +502,7 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.C
|
||||
return makeConfigDefaultsResult(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input models.GenerateAPIKeyInput) (string, error) {
|
||||
func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input GenerateAPIKeyInput) (string, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
var newAPIKey string
|
||||
@@ -497,3 +524,23 @@ func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input models.Gene
|
||||
|
||||
return newAPIKey, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
|
||||
c := config.GetInstance()
|
||||
c.SetUIConfiguration(input)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return c.GetUIConfiguration(), err
|
||||
}
|
||||
|
||||
return c.GetUIConfiguration(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, value interface{}) (map[string]interface{}, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
cfg := c.GetUIConfiguration()
|
||||
cfg[key] = value
|
||||
|
||||
return r.ConfigureUI(ctx, cfg)
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) EnableDlna(ctx context.Context, input models.EnableDLNAInput) (bool, error) {
|
||||
func (r *mutationResolver) EnableDlna(ctx context.Context, input EnableDLNAInput) (bool, error) {
|
||||
err := manager.GetInstance().DLNAService.Start(parseMinutes(input.Duration))
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -16,17 +15,17 @@ func (r *mutationResolver) EnableDlna(ctx context.Context, input models.EnableDL
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DisableDlna(ctx context.Context, input models.DisableDLNAInput) (bool, error) {
|
||||
func (r *mutationResolver) DisableDlna(ctx context.Context, input DisableDLNAInput) (bool, error) {
|
||||
manager.GetInstance().DLNAService.Stop(parseMinutes(input.Duration))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddTempDlnaip(ctx context.Context, input models.AddTempDLNAIPInput) (bool, error) {
|
||||
func (r *mutationResolver) AddTempDlnaip(ctx context.Context, input AddTempDLNAIPInput) (bool, error) {
|
||||
manager.GetInstance().DLNAService.AddTempDLNAIP(input.Address, parseMinutes(input.Duration))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveTempDlnaip(ctx context.Context, input models.RemoveTempDLNAIPInput) (bool, error) {
|
||||
func (r *mutationResolver) RemoveTempDlnaip(ctx context.Context, input RemoveTempDLNAIPInput) (bool, error) {
|
||||
ret := manager.GetInstance().DLNAService.RemoveTempDLNAIP(input.Address)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
74
internal/api/resolver_mutation_file.go
Normal file
74
internal/api/resolver_mutation_file.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret bool, err error) {
|
||||
fileIDs, err := stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
fileDeleter := file.NewDeleter()
|
||||
destroyer := &file.ZipDestroyer{
|
||||
FileDestroyer: r.repository.File,
|
||||
FolderDestroyer: r.repository.Folder,
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.File
|
||||
|
||||
for _, fileIDInt := range fileIDs {
|
||||
fileID := file.ID(fileIDInt)
|
||||
f, err := qb.Find(ctx, fileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := f[0].Base().Path
|
||||
|
||||
// ensure not a primary file
|
||||
isPrimary, err := qb.IsPrimary(ctx, fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking if file %s is primary: %w", path, err)
|
||||
}
|
||||
|
||||
if isPrimary {
|
||||
return fmt.Errorf("cannot delete primary file %s", path)
|
||||
}
|
||||
|
||||
// destroy files in zip file
|
||||
inZip, err := qb.FindByZipFileID(ctx, fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding zip file contents for %s: %w", path, err)
|
||||
}
|
||||
|
||||
for _, ff := range inZip {
|
||||
const deleteFileInZip = false
|
||||
if err := file.Destroy(ctx, qb, ff, fileDeleter, deleteFileInZip); err != nil {
|
||||
return fmt.Errorf("destroying file %s: %w", ff.Base().Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFile = true
|
||||
if err := destroyer.DestroyZip(ctx, f[0], fileDeleter, deleteFile); err != nil {
|
||||
return fmt.Errorf("deleting file %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
fileDeleter.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
// perform the post-commit actions
|
||||
fileDeleter.Commit()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -11,18 +10,16 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/hash/md5"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) getGallery(ctx context.Context, id int) (ret *models.Gallery, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Gallery().Find(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Gallery.Find(ctx, id)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -31,75 +28,63 @@ func (r *mutationResolver) getGallery(ctx context.Context, id int) (ret *models.
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.GalleryCreateInput) (*models.Gallery, error) {
|
||||
func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreateInput) (*models.Gallery, error) {
|
||||
// name must be provided
|
||||
if input.Title == "" {
|
||||
return nil, errors.New("title must not be empty")
|
||||
}
|
||||
|
||||
// for manually created galleries, generate checksum from title
|
||||
checksum := md5.FromString(input.Title)
|
||||
|
||||
// Populate a new performer from the input
|
||||
performerIDs, err := stringslice.StringSliceToIntSlice(input.PerformerIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
sceneIDs, err := stringslice.StringSliceToIntSlice(input.SceneIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
|
||||
currentTime := time.Now()
|
||||
newGallery := models.Gallery{
|
||||
Title: sql.NullString{
|
||||
String: input.Title,
|
||||
Valid: true,
|
||||
},
|
||||
Checksum: checksum,
|
||||
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
Title: input.Title,
|
||||
PerformerIDs: models.NewRelatedIDs(performerIDs),
|
||||
TagIDs: models.NewRelatedIDs(tagIDs),
|
||||
SceneIDs: models.NewRelatedIDs(sceneIDs),
|
||||
CreatedAt: currentTime,
|
||||
UpdatedAt: currentTime,
|
||||
}
|
||||
if input.URL != nil {
|
||||
newGallery.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
newGallery.URL = *input.URL
|
||||
}
|
||||
if input.Details != nil {
|
||||
newGallery.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
if input.URL != nil {
|
||||
newGallery.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
newGallery.Details = *input.Details
|
||||
}
|
||||
|
||||
if input.Date != nil {
|
||||
newGallery.Date = models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
d := models.NewDate(*input.Date)
|
||||
newGallery.Date = &d
|
||||
}
|
||||
if input.Rating != nil {
|
||||
newGallery.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
// rating must be nullable
|
||||
newGallery.Rating = sql.NullInt64{Valid: false}
|
||||
|
||||
if input.Rating100 != nil {
|
||||
newGallery.Rating = input.Rating100
|
||||
} else if input.Rating != nil {
|
||||
rating := models.Rating5To100(*input.Rating)
|
||||
newGallery.Rating = &rating
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
newGallery.StudioID = sql.NullInt64{Int64: studioID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
newGallery.StudioID = sql.NullInt64{Valid: false}
|
||||
studioID, _ := strconv.Atoi(*input.StudioID)
|
||||
newGallery.StudioID = &studioID
|
||||
}
|
||||
|
||||
// Start the transaction and save the gallery
|
||||
var gallery *models.Gallery
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Gallery()
|
||||
var err error
|
||||
gallery, err = qb.Create(newGallery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
if err := r.updateGalleryPerformers(qb, gallery.ID, input.PerformerIds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if err := r.updateGalleryTags(qb, gallery.ID, input.TagIds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the scenes
|
||||
if err := r.updateGalleryScenes(qb, gallery.ID, input.SceneIds); err != nil {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Gallery
|
||||
if err := qb.Create(ctx, &newGallery, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -108,32 +93,12 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.Galle
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryCreatePost, input, nil)
|
||||
return r.getGallery(ctx, gallery.ID)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGallery.ID, plugin.GalleryCreatePost, input, nil)
|
||||
return r.getGallery(ctx, newGallery.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateGalleryPerformers(qb models.GalleryReaderWriter, galleryID int, performerIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(performerIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdatePerformers(galleryID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateGalleryTags(qb models.GalleryReaderWriter, galleryID int, tagIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(tagIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateTags(galleryID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateGalleryScenes(qb models.GalleryReaderWriter, galleryID int, sceneIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(sceneIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateScenes(galleryID, ids)
|
||||
type GallerySceneUpdater interface {
|
||||
UpdateScenes(ctx context.Context, galleryID int, sceneIDs []int) error
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.GalleryUpdateInput) (ret *models.Gallery, err error) {
|
||||
@@ -142,8 +107,8 @@ func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.Galle
|
||||
}
|
||||
|
||||
// Start the transaction and save the gallery
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
ret, err = r.galleryUpdate(input, translator, repo)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.galleryUpdate(ctx, input, translator)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -158,13 +123,13 @@ func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.
|
||||
inputMaps := getUpdateInputMaps(ctx)
|
||||
|
||||
// Start the transaction and save the gallery
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
for i, gallery := range input {
|
||||
translator := changesetTranslator{
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
thisGallery, err := r.galleryUpdate(*gallery, translator, repo)
|
||||
thisGallery, err := r.galleryUpdate(ctx, *gallery, translator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -196,8 +161,8 @@ func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Gallery, error) {
|
||||
qb := repo.Gallery()
|
||||
func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.GalleryUpdateInput, translator changesetTranslator) (*models.Gallery, error) {
|
||||
qb := r.repository.Gallery
|
||||
|
||||
// Populate gallery from the input
|
||||
galleryID, err := strconv.Atoi(input.ID)
|
||||
@@ -205,7 +170,7 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, transl
|
||||
return nil, err
|
||||
}
|
||||
|
||||
originalGallery, err := qb.Find(galleryID)
|
||||
originalGallery, err := qb.Find(ctx, galleryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -214,136 +179,139 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, transl
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
|
||||
updatedTime := time.Now()
|
||||
updatedGallery := models.GalleryPartial{
|
||||
ID: galleryID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
updatedGallery := models.NewGalleryPartial()
|
||||
|
||||
if input.Title != nil {
|
||||
// ensure title is not empty
|
||||
if *input.Title == "" {
|
||||
return nil, errors.New("title must not be empty")
|
||||
if *input.Title == "" && originalGallery.IsUserCreated() {
|
||||
return nil, errors.New("title must not be empty for user-created galleries")
|
||||
}
|
||||
|
||||
// if gallery is not zip-based, then generate the checksum from the title
|
||||
if !originalGallery.Path.Valid {
|
||||
checksum := md5.FromString(*input.Title)
|
||||
updatedGallery.Checksum = &checksum
|
||||
}
|
||||
|
||||
updatedGallery.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||
updatedGallery.Title = models.NewOptionalString(*input.Title)
|
||||
}
|
||||
|
||||
updatedGallery.Details = translator.nullString(input.Details, "details")
|
||||
updatedGallery.URL = translator.nullString(input.URL, "url")
|
||||
updatedGallery.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedGallery.Organized = input.Organized
|
||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
||||
updatedGallery.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
if input.PrimaryFileID != nil {
|
||||
primaryFileID, err := strconv.Atoi(*input.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||
}
|
||||
|
||||
converted := file.ID(primaryFileID)
|
||||
updatedGallery.PrimaryFileID = &converted
|
||||
|
||||
if err := originalGallery.LoadFiles(ctx, r.repository.Gallery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that new primary file is associated with scene
|
||||
var f file.File
|
||||
for _, ff := range originalGallery.Files.List() {
|
||||
if ff.Base().ID == converted {
|
||||
f = ff
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, fmt.Errorf("file with id %d not associated with gallery", converted)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("performer_ids") {
|
||||
updatedGallery.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedGallery.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("scene_ids") {
|
||||
updatedGallery.SceneIDs, err = translateUpdateIDs(input.SceneIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// gallery scene is set from the scene only
|
||||
|
||||
gallery, err := qb.UpdatePartial(updatedGallery)
|
||||
gallery, err := qb.UpdatePartial(ctx, galleryID, updatedGallery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
if translator.hasField("performer_ids") {
|
||||
if err := r.updateGalleryPerformers(qb, galleryID, input.PerformerIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
if err := r.updateGalleryTags(qb, galleryID, input.TagIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the scenes
|
||||
if translator.hasField("scene_ids") {
|
||||
if err := r.updateGalleryScenes(qb, galleryID, input.SceneIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return gallery, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.BulkGalleryUpdateInput) ([]*models.Gallery, error) {
|
||||
func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGalleryUpdateInput) ([]*models.Gallery, error) {
|
||||
// Populate gallery from the input
|
||||
updatedTime := time.Now()
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
updatedGallery := models.GalleryPartial{
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
updatedGallery := models.NewGalleryPartial()
|
||||
|
||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
||||
updatedGallery.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
var err error
|
||||
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
if translator.hasField("performer_ids") {
|
||||
updatedGallery.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
updatedGallery.Details = translator.nullString(input.Details, "details")
|
||||
updatedGallery.URL = translator.nullString(input.URL, "url")
|
||||
updatedGallery.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedGallery.Organized = input.Organized
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedGallery.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("scene_ids") {
|
||||
updatedGallery.SceneIDs, err = translateUpdateIDs(input.SceneIds.Ids, input.SceneIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
ret := []*models.Gallery{}
|
||||
|
||||
// Start the transaction and save the galleries
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Gallery()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Gallery
|
||||
|
||||
for _, galleryIDStr := range input.Ids {
|
||||
galleryID, _ := strconv.Atoi(galleryIDStr)
|
||||
updatedGallery.ID = galleryID
|
||||
|
||||
gallery, err := qb.UpdatePartial(updatedGallery)
|
||||
gallery, err := qb.UpdatePartial(ctx, galleryID, updatedGallery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, gallery)
|
||||
|
||||
// Save the performers
|
||||
if translator.hasField("performer_ids") {
|
||||
performerIDs, err := adjustGalleryPerformerIDs(qb, galleryID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdatePerformers(galleryID, performerIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
tagIDs, err := adjustGalleryTagIDs(qb, galleryID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateTags(galleryID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the scenes
|
||||
if translator.hasField("scene_ids") {
|
||||
sceneIDs, err := adjustGallerySceneIDs(qb, galleryID, *input.SceneIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateScenes(galleryID, sceneIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -367,31 +335,8 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.B
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func adjustGalleryPerformerIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetPerformerIDs(galleryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustGalleryTagIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetTagIDs(galleryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustGallerySceneIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetSceneIDs(galleryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
type GallerySceneGetter interface {
|
||||
GetSceneIDs(ctx context.Context, galleryID int) ([]int, error)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.GalleryDestroyInput) (bool, error) {
|
||||
@@ -403,19 +348,18 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
var galleries []*models.Gallery
|
||||
var imgsDestroyed []*models.Image
|
||||
fileDeleter := &image.FileDeleter{
|
||||
Deleter: *file.NewDeleter(),
|
||||
Deleter: file.NewDeleter(),
|
||||
Paths: manager.GetInstance().Paths,
|
||||
}
|
||||
|
||||
deleteGenerated := utils.IsTrue(input.DeleteGenerated)
|
||||
deleteFile := utils.IsTrue(input.DeleteFile)
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Gallery()
|
||||
iqb := repo.Image()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Gallery
|
||||
|
||||
for _, id := range galleryIDs {
|
||||
gallery, err := qb.Find(id)
|
||||
gallery, err := qb.Find(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -424,55 +368,14 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
return fmt.Errorf("gallery with id %d not found", id)
|
||||
}
|
||||
|
||||
galleries = append(galleries, gallery)
|
||||
|
||||
// if this is a zip-based gallery, delete the images as well first
|
||||
if gallery.Zip {
|
||||
imgs, err := iqb.FindByGalleryID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, img := range imgs {
|
||||
if err := image.Destroy(img, iqb, fileDeleter, deleteGenerated, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imgsDestroyed = append(imgsDestroyed, img)
|
||||
}
|
||||
|
||||
if deleteFile {
|
||||
if err := fileDeleter.Files([]string{gallery.Path.String}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if deleteFile {
|
||||
// Delete image if it is only attached to this gallery
|
||||
imgs, err := iqb.FindByGalleryID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, img := range imgs {
|
||||
imgGalleries, err := qb.FindByImageID(img.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(imgGalleries) == 1 {
|
||||
if err := image.Destroy(img, iqb, fileDeleter, deleteGenerated, deleteFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imgsDestroyed = append(imgsDestroyed, img)
|
||||
}
|
||||
}
|
||||
|
||||
// we only want to delete a folder-based gallery if it is empty.
|
||||
// don't do this with the file deleter
|
||||
if err := gallery.LoadFiles(ctx, qb); err != nil {
|
||||
return fmt.Errorf("loading files for gallery %d", id)
|
||||
}
|
||||
|
||||
if err := qb.Destroy(id); err != nil {
|
||||
galleries = append(galleries, gallery)
|
||||
|
||||
imgsDestroyed, err = r.galleryService.Destroy(ctx, gallery, fileDeleter, deleteGenerated, deleteFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -488,10 +391,11 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
|
||||
for _, gallery := range galleries {
|
||||
// don't delete stash library paths
|
||||
if utils.IsTrue(input.DeleteFile) && !gallery.Zip && gallery.Path.Valid && !isStashPath(gallery.Path.String) {
|
||||
path := gallery.Path
|
||||
if deleteFile && path != "" && !isStashPath(path) {
|
||||
// try to remove the folder - it is possible that it is not empty
|
||||
// so swallow the error if present
|
||||
_ = os.Remove(gallery.Path.String)
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,8 +403,8 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
for _, gallery := range galleries {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
GalleryDestroyInput: input,
|
||||
Checksum: gallery.Checksum,
|
||||
Path: gallery.Path.String,
|
||||
Checksum: gallery.PrimaryChecksum(),
|
||||
Path: gallery.Path,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
@@ -526,7 +430,7 @@ func isStashPath(path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.GalleryAddInput) (bool, error) {
|
||||
func (r *mutationResolver) AddGalleryImages(ctx context.Context, input GalleryAddInput) (bool, error) {
|
||||
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -537,9 +441,9 @@ func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.Ga
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Gallery()
|
||||
gallery, err := qb.Find(galleryID)
|
||||
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
|
||||
}
|
||||
@@ -548,17 +452,7 @@ func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.Ga
|
||||
return errors.New("gallery not found")
|
||||
}
|
||||
|
||||
if gallery.Zip {
|
||||
return errors.New("cannot modify zip gallery images")
|
||||
}
|
||||
|
||||
newIDs, err := qb.GetImageIDs(galleryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newIDs = intslice.IntAppendUniques(newIDs, imageIDs)
|
||||
return qb.UpdateImages(galleryID, newIDs)
|
||||
return r.galleryService.AddImages(ctx, gallery, imageIDs...)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -566,7 +460,7 @@ func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.Ga
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input models.GalleryRemoveInput) (bool, error) {
|
||||
func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input GalleryRemoveInput) (bool, error) {
|
||||
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -577,9 +471,9 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input models
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Gallery()
|
||||
gallery, err := qb.Find(galleryID)
|
||||
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
|
||||
}
|
||||
@@ -588,17 +482,7 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input models
|
||||
return errors.New("gallery not found")
|
||||
}
|
||||
|
||||
if gallery.Zip {
|
||||
return errors.New("cannot modify zip gallery images")
|
||||
}
|
||||
|
||||
newIDs, err := qb.GetImageIDs(galleryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newIDs = intslice.IntExclude(newIDs, imageIDs)
|
||||
return qb.UpdateImages(galleryID, newIDs)
|
||||
return r.galleryService.RemoveImages(ctx, gallery, imageIDs...)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
@@ -16,8 +15,8 @@ import (
|
||||
)
|
||||
|
||||
func (r *mutationResolver) getImage(ctx context.Context, id int) (ret *models.Image, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Image().Find(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Image.Find(ctx, id)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -26,14 +25,14 @@ 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),
|
||||
}
|
||||
|
||||
// Start the transaction and save the image
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
ret, err = r.imageUpdate(input, translator, repo)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.imageUpdate(ctx, input, translator)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -44,17 +43,17 @@ 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
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
for i, image := range input {
|
||||
translator := changesetTranslator{
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
thisImage, err := r.imageUpdate(*image, translator, repo)
|
||||
thisImage, err := r.imageUpdate(ctx, *image, translator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -86,148 +85,169 @@ func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*models.Ima
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) imageUpdate(input models.ImageUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Image, error) {
|
||||
func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInput, translator changesetTranslator) (*models.Image, error) {
|
||||
// Populate image from the input
|
||||
imageID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedTime := time.Now()
|
||||
updatedImage := models.ImagePartial{
|
||||
ID: imageID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
|
||||
updatedImage.Title = translator.nullString(input.Title, "title")
|
||||
updatedImage.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedImage.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedImage.Organized = input.Organized
|
||||
|
||||
qb := repo.Image()
|
||||
image, err := qb.Update(updatedImage)
|
||||
i, err := r.repository.Image.Find(ctx, imageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if i == nil {
|
||||
return nil, fmt.Errorf("image not found %d", imageID)
|
||||
}
|
||||
|
||||
updatedImage := models.NewImagePartial()
|
||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
if input.PrimaryFileID != nil {
|
||||
primaryFileID, err := strconv.Atoi(*input.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||
}
|
||||
|
||||
converted := file.ID(primaryFileID)
|
||||
updatedImage.PrimaryFileID = &converted
|
||||
|
||||
if err := i.LoadFiles(ctx, r.repository.Image); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that new primary file is associated with scene
|
||||
var f *file.ImageFile
|
||||
for _, ff := range i.Files.List() {
|
||||
if ff.ID == converted {
|
||||
f = ff
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, fmt.Errorf("file with id %d not associated with image", converted)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("gallery_ids") {
|
||||
if err := r.updateImageGalleries(qb, imageID, input.GalleryIds); err != nil {
|
||||
updatedImage.GalleryIDs, err = translateUpdateIDs(input.GalleryIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
|
||||
// ensure gallery IDs are loaded
|
||||
if err := i.LoadGalleryIDs(ctx, r.repository.Image); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.galleryService.ValidateImageGalleryChange(ctx, i, *updatedImage.GalleryIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
if translator.hasField("performer_ids") {
|
||||
if err := r.updateImagePerformers(qb, imageID, input.PerformerIds); err != nil {
|
||||
return nil, err
|
||||
updatedImage.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
if err := r.updateImageTags(qb, imageID, input.TagIds); err != nil {
|
||||
return nil, err
|
||||
updatedImage.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
qb := r.repository.Image
|
||||
image, err := qb.UpdatePartial(ctx, imageID, updatedImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateImageGalleries(qb models.ImageReaderWriter, imageID int, galleryIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(galleryIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateGalleries(imageID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateImagePerformers(qb models.ImageReaderWriter, imageID int, performerIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(performerIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdatePerformers(imageID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateImageTags(qb models.ImageReaderWriter, imageID int, tagsIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(tagsIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateTags(imageID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input models.BulkImageUpdateInput) (ret []*models.Image, err error) {
|
||||
func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageUpdateInput) (ret []*models.Image, err error) {
|
||||
imageIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Populate image from the input
|
||||
updatedTime := time.Now()
|
||||
|
||||
updatedImage := models.ImagePartial{
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
updatedImage := models.NewImagePartial()
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
updatedImage.Title = translator.nullString(input.Title, "title")
|
||||
updatedImage.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedImage.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedImage.Organized = input.Organized
|
||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
if translator.hasField("gallery_ids") {
|
||||
updatedImage.GalleryIDs, err = translateUpdateIDs(input.GalleryIds.Ids, input.GalleryIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("performer_ids") {
|
||||
updatedImage.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedImage.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the transaction and save the image marker
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
|
||||
for _, imageID := range imageIDs {
|
||||
updatedImage.ID = imageID
|
||||
i, err := r.repository.Image.Find(ctx, imageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
image, err := qb.Update(updatedImage)
|
||||
if i == nil {
|
||||
return fmt.Errorf("image not found %d", imageID)
|
||||
}
|
||||
|
||||
if updatedImage.GalleryIDs != nil {
|
||||
// ensure gallery IDs are loaded
|
||||
if err := i.LoadGalleryIDs(ctx, r.repository.Image); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.galleryService.ValidateImageGalleryChange(ctx, i, *updatedImage.GalleryIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
image, err := qb.UpdatePartial(ctx, imageID, updatedImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, image)
|
||||
|
||||
// Save the galleries
|
||||
if translator.hasField("gallery_ids") {
|
||||
galleryIDs, err := adjustImageGalleryIDs(qb, imageID, *input.GalleryIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateGalleries(imageID, galleryIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
if translator.hasField("performer_ids") {
|
||||
performerIDs, err := adjustImagePerformerIDs(qb, imageID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdatePerformers(imageID, performerIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
tagIDs, err := adjustImageTagIDs(qb, imageID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateTags(imageID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -251,33 +271,6 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input models.Bul
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func adjustImageGalleryIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetGalleryIDs(imageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustImagePerformerIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetPerformerIDs(imageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustImageTagIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetTagIDs(imageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageDestroyInput) (ret bool, err error) {
|
||||
imageID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -286,13 +279,11 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD
|
||||
|
||||
var i *models.Image
|
||||
fileDeleter := &image.FileDeleter{
|
||||
Deleter: *file.NewDeleter(),
|
||||
Deleter: file.NewDeleter(),
|
||||
Paths: manager.GetInstance().Paths,
|
||||
}
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
|
||||
i, err = qb.Find(imageID)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
i, err = r.repository.Image.Find(ctx, imageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -301,7 +292,7 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD
|
||||
return fmt.Errorf("image with id %d not found", imageID)
|
||||
}
|
||||
|
||||
return image.Destroy(i, qb, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile))
|
||||
return r.imageService.Destroy(ctx, i, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile))
|
||||
}); err != nil {
|
||||
fileDeleter.Rollback()
|
||||
return false, err
|
||||
@@ -328,15 +319,14 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image
|
||||
|
||||
var images []*models.Image
|
||||
fileDeleter := &image.FileDeleter{
|
||||
Deleter: *file.NewDeleter(),
|
||||
Deleter: file.NewDeleter(),
|
||||
Paths: manager.GetInstance().Paths,
|
||||
}
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
|
||||
for _, imageID := range imageIDs {
|
||||
|
||||
i, err := qb.Find(imageID)
|
||||
i, err := qb.Find(ctx, imageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -347,7 +337,7 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image
|
||||
|
||||
images = append(images, i)
|
||||
|
||||
if err := image.Destroy(i, qb, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile)); err != nil {
|
||||
if err := r.imageService.Destroy(ctx, i, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -379,10 +369,10 @@ func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (ret
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
|
||||
ret, err = qb.IncrementOCounter(imageID)
|
||||
ret, err = qb.IncrementOCounter(ctx, imageID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -397,10 +387,10 @@ func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (ret
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
|
||||
ret, err = qb.DecrementOCounter(imageID)
|
||||
ret, err = qb.DecrementOCounter(ctx, imageID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -415,10 +405,10 @@ func (r *mutationResolver) ImageResetO(ctx context.Context, id string) (ret int,
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
|
||||
ret, err = qb.ResetOCounter(imageID)
|
||||
ret, err = qb.ResetOCounter(ctx, imageID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -9,15 +9,14 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/identify"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
|
||||
func (r *mutationResolver) MetadataScan(ctx context.Context, input manager.ScanMetadataInput) (string, error) {
|
||||
jobID, err := manager.GetInstance().Scan(ctx, input)
|
||||
|
||||
if err != nil {
|
||||
@@ -36,7 +35,7 @@ func (r *mutationResolver) MetadataImport(ctx context.Context) (string, error) {
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImportObjects(ctx context.Context, input models.ImportObjectsInput) (string, error) {
|
||||
func (r *mutationResolver) ImportObjects(ctx context.Context, input manager.ImportObjectsInput) (string, error) {
|
||||
t, err := manager.CreateImportTask(config.GetInstance().GetVideoFileNamingAlgorithm(), input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -56,12 +55,12 @@ func (r *mutationResolver) MetadataExport(ctx context.Context) (string, error) {
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ExportObjects(ctx context.Context, input models.ExportObjectsInput) (*string, error) {
|
||||
func (r *mutationResolver) ExportObjects(ctx context.Context, input manager.ExportObjectsInput) (*string, error) {
|
||||
t := manager.CreateExportTask(config.GetInstance().GetVideoFileNamingAlgorithm(), input)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
t.Start(&wg)
|
||||
t.Start(ctx, &wg)
|
||||
|
||||
if t.DownloadHash != "" {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
@@ -75,7 +74,7 @@ func (r *mutationResolver) ExportObjects(ctx context.Context, input models.Expor
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
|
||||
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input manager.GenerateMetadataInput) (string, error) {
|
||||
jobID, err := manager.GetInstance().Generate(ctx, input)
|
||||
|
||||
if err != nil {
|
||||
@@ -85,19 +84,19 @@ func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.Ge
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
|
||||
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input manager.AutoTagMetadataInput) (string, error) {
|
||||
jobID := manager.GetInstance().AutoTag(ctx, input)
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataIdentify(ctx context.Context, input models.IdentifyMetadataInput) (string, error) {
|
||||
func (r *mutationResolver) MetadataIdentify(ctx context.Context, input identify.Options) (string, error) {
|
||||
t := manager.CreateIdentifyJob(input)
|
||||
jobID := manager.GetInstance().JobManager.Add(ctx, "Identifying...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataClean(ctx context.Context, input models.CleanMetadataInput) (string, error) {
|
||||
func (r *mutationResolver) MetadataClean(ctx context.Context, input manager.CleanMetadataInput) (string, error) {
|
||||
jobID := manager.GetInstance().Clean(ctx, input)
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
@@ -107,10 +106,11 @@ func (r *mutationResolver) MigrateHashNaming(ctx context.Context) (string, error
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.BackupDatabaseInput) (*string, error) {
|
||||
func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatabaseInput) (*string, error) {
|
||||
// if download is true, then backup to temporary file and return a link
|
||||
download := input.Download != nil && *input.Download
|
||||
mgr := manager.GetInstance()
|
||||
database := mgr.Database
|
||||
var backupPath string
|
||||
if download {
|
||||
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
|
||||
@@ -124,10 +124,16 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.Back
|
||||
backupPath = f.Name()
|
||||
f.Close()
|
||||
} else {
|
||||
backupPath = database.DatabaseBackupPath()
|
||||
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
|
||||
if backupDirectoryPath != "" {
|
||||
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
|
||||
}
|
||||
}
|
||||
backupPath = database.DatabaseBackupPath(backupDirectoryPath)
|
||||
}
|
||||
|
||||
err := database.Backup(database.DB, backupPath)
|
||||
err := database.Backup(backupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -141,7 +147,7 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.Back
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
fn := filepath.Base(database.DatabaseBackupPath())
|
||||
fn := filepath.Base(database.DatabaseBackupPath(""))
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
||||
return &ret, nil
|
||||
} else {
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
)
|
||||
|
||||
func (r *mutationResolver) getMovie(ctx context.Context, id int) (ret *models.Movie, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().Find(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.Find(ctx, id)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -25,7 +25,7 @@ func (r *mutationResolver) getMovie(ctx context.Context, id int) (ret *models.Mo
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCreateInput) (*models.Movie, error) {
|
||||
func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInput) (*models.Movie, error) {
|
||||
// generate checksum from movie name rather than image
|
||||
checksum := md5.FromString(input.Name)
|
||||
|
||||
@@ -76,9 +76,11 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
||||
newMovie.Date = models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
rating := int64(*input.Rating)
|
||||
newMovie.Rating = sql.NullInt64{Int64: rating, Valid: true}
|
||||
if input.Rating100 != nil {
|
||||
newMovie.Rating = sql.NullInt64{Int64: int64(*input.Rating100), Valid: true}
|
||||
} else if input.Rating != nil {
|
||||
rating := models.Rating5To100(*input.Rating)
|
||||
newMovie.Rating = sql.NullInt64{Int64: int64(rating), Valid: true}
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
@@ -100,16 +102,16 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
||||
|
||||
// Start the transaction and save the movie
|
||||
var movie *models.Movie
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Movie()
|
||||
movie, err = qb.Create(newMovie)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Movie
|
||||
movie, err = qb.Create(ctx, newMovie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(frontimageData) > 0 {
|
||||
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {
|
||||
if err := qb.UpdateImages(ctx, movie.ID, frontimageData, backimageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -123,7 +125,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
||||
return r.getMovie(ctx, movie.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUpdateInput) (*models.Movie, error) {
|
||||
func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInput) (*models.Movie, error) {
|
||||
// Populate movie from the input
|
||||
movieID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -166,7 +168,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
updatedMovie.Aliases = translator.nullString(input.Aliases, "aliases")
|
||||
updatedMovie.Duration = translator.nullInt64(input.Duration, "duration")
|
||||
updatedMovie.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedMovie.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedMovie.Director = translator.nullString(input.Director, "director")
|
||||
updatedMovie.Synopsis = translator.nullString(input.Synopsis, "synopsis")
|
||||
@@ -174,9 +176,9 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
|
||||
// Start the transaction and save the movie
|
||||
var movie *models.Movie
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Movie()
|
||||
movie, err = qb.Update(updatedMovie)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Movie
|
||||
movie, err = qb.Update(ctx, updatedMovie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -184,13 +186,13 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
// update image table
|
||||
if frontImageIncluded || backImageIncluded {
|
||||
if !frontImageIncluded {
|
||||
frontimageData, err = qb.GetFrontImage(updatedMovie.ID)
|
||||
frontimageData, err = qb.GetFrontImage(ctx, updatedMovie.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !backImageIncluded {
|
||||
backimageData, err = qb.GetBackImage(updatedMovie.ID)
|
||||
backimageData, err = qb.GetBackImage(ctx, updatedMovie.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -198,7 +200,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
|
||||
if len(frontimageData) == 0 && len(backimageData) == 0 {
|
||||
// both images are being nulled. Destroy them.
|
||||
if err := qb.DestroyImages(movie.ID); err != nil {
|
||||
if err := qb.DestroyImages(ctx, movie.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -208,7 +210,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
frontimageData, _ = utils.ProcessImageInput(ctx, models.DefaultMovieImage)
|
||||
}
|
||||
|
||||
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {
|
||||
if err := qb.UpdateImages(ctx, movie.ID, frontimageData, backimageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -223,7 +225,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
return r.getMovie(ctx, movie.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input models.BulkMovieUpdateInput) ([]*models.Movie, error) {
|
||||
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieUpdateInput) ([]*models.Movie, error) {
|
||||
movieIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -239,19 +241,19 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input models.Bul
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
|
||||
updatedMovie.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedMovie.Director = translator.nullString(input.Director, "director")
|
||||
|
||||
ret := []*models.Movie{}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Movie()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Movie
|
||||
|
||||
for _, movieID := range movieIDs {
|
||||
updatedMovie.ID = movieID
|
||||
|
||||
existing, err := qb.Find(movieID)
|
||||
existing, err := qb.Find(ctx, movieID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -260,7 +262,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input models.Bul
|
||||
return fmt.Errorf("movie with id %d not found", movieID)
|
||||
}
|
||||
|
||||
movie, err := qb.Update(updatedMovie)
|
||||
movie, err := qb.Update(ctx, updatedMovie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -288,14 +290,14 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input models.Bul
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MovieDestroy(ctx context.Context, input models.MovieDestroyInput) (bool, error) {
|
||||
func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyInput) (bool, error) {
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
return repo.Movie().Destroy(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.repository.Movie.Destroy(ctx, id)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -311,10 +313,10 @@ func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Movie()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Movie
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(id); err != nil {
|
||||
if err := qb.Destroy(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -16,8 +15,8 @@ import (
|
||||
)
|
||||
|
||||
func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *models.Performer, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().Find(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Performer.Find(ctx, id)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -26,7 +25,17 @@ func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *model
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.PerformerCreateInput) (*models.Performer, error) {
|
||||
func stashIDPtrSliceToSlice(v []*models.StashID) []models.StashID {
|
||||
ret := make([]models.StashID, len(v))
|
||||
for i, vv := range v {
|
||||
c := vv
|
||||
ret[i] = *c
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerCreateInput) (*models.Performer, error) {
|
||||
// generate checksum from performer name rather than image
|
||||
checksum := md5.FromString(input.Name)
|
||||
|
||||
@@ -44,78 +53,85 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
// Populate a new performer from the input
|
||||
currentTime := time.Now()
|
||||
newPerformer := models.Performer{
|
||||
Name: input.Name,
|
||||
Checksum: checksum,
|
||||
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
CreatedAt: currentTime,
|
||||
UpdatedAt: currentTime,
|
||||
}
|
||||
newPerformer.Name = sql.NullString{String: input.Name, Valid: true}
|
||||
if input.URL != nil {
|
||||
newPerformer.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
newPerformer.URL = *input.URL
|
||||
}
|
||||
if input.Gender != nil {
|
||||
newPerformer.Gender = sql.NullString{String: input.Gender.String(), Valid: true}
|
||||
newPerformer.Gender = *input.Gender
|
||||
}
|
||||
if input.Birthdate != nil {
|
||||
newPerformer.Birthdate = models.SQLiteDate{String: *input.Birthdate, Valid: true}
|
||||
d := models.NewDate(*input.Birthdate)
|
||||
newPerformer.Birthdate = &d
|
||||
}
|
||||
if input.Ethnicity != nil {
|
||||
newPerformer.Ethnicity = sql.NullString{String: *input.Ethnicity, Valid: true}
|
||||
newPerformer.Ethnicity = *input.Ethnicity
|
||||
}
|
||||
if input.Country != nil {
|
||||
newPerformer.Country = sql.NullString{String: *input.Country, Valid: true}
|
||||
newPerformer.Country = *input.Country
|
||||
}
|
||||
if input.EyeColor != nil {
|
||||
newPerformer.EyeColor = sql.NullString{String: *input.EyeColor, Valid: true}
|
||||
newPerformer.EyeColor = *input.EyeColor
|
||||
}
|
||||
if input.Height != nil {
|
||||
newPerformer.Height = sql.NullString{String: *input.Height, Valid: true}
|
||||
// prefer height_cm over height
|
||||
if input.HeightCm != nil {
|
||||
newPerformer.Height = input.HeightCm
|
||||
} else if input.Height != nil {
|
||||
h, err := strconv.Atoi(*input.Height)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid height: %s", *input.Height)
|
||||
}
|
||||
newPerformer.Height = &h
|
||||
}
|
||||
if input.Measurements != nil {
|
||||
newPerformer.Measurements = sql.NullString{String: *input.Measurements, Valid: true}
|
||||
newPerformer.Measurements = *input.Measurements
|
||||
}
|
||||
if input.FakeTits != nil {
|
||||
newPerformer.FakeTits = sql.NullString{String: *input.FakeTits, Valid: true}
|
||||
newPerformer.FakeTits = *input.FakeTits
|
||||
}
|
||||
if input.CareerLength != nil {
|
||||
newPerformer.CareerLength = sql.NullString{String: *input.CareerLength, Valid: true}
|
||||
newPerformer.CareerLength = *input.CareerLength
|
||||
}
|
||||
if input.Tattoos != nil {
|
||||
newPerformer.Tattoos = sql.NullString{String: *input.Tattoos, Valid: true}
|
||||
newPerformer.Tattoos = *input.Tattoos
|
||||
}
|
||||
if input.Piercings != nil {
|
||||
newPerformer.Piercings = sql.NullString{String: *input.Piercings, Valid: true}
|
||||
newPerformer.Piercings = *input.Piercings
|
||||
}
|
||||
if input.Aliases != nil {
|
||||
newPerformer.Aliases = sql.NullString{String: *input.Aliases, Valid: true}
|
||||
newPerformer.Aliases = *input.Aliases
|
||||
}
|
||||
if input.Twitter != nil {
|
||||
newPerformer.Twitter = sql.NullString{String: *input.Twitter, Valid: true}
|
||||
newPerformer.Twitter = *input.Twitter
|
||||
}
|
||||
if input.Instagram != nil {
|
||||
newPerformer.Instagram = sql.NullString{String: *input.Instagram, Valid: true}
|
||||
newPerformer.Instagram = *input.Instagram
|
||||
}
|
||||
if input.Favorite != nil {
|
||||
newPerformer.Favorite = sql.NullBool{Bool: *input.Favorite, Valid: true}
|
||||
} else {
|
||||
newPerformer.Favorite = sql.NullBool{Bool: false, Valid: true}
|
||||
newPerformer.Favorite = *input.Favorite
|
||||
}
|
||||
if input.Rating != nil {
|
||||
newPerformer.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
newPerformer.Rating = sql.NullInt64{Valid: false}
|
||||
if input.Rating100 != nil {
|
||||
newPerformer.Rating = input.Rating100
|
||||
} else if input.Rating != nil {
|
||||
rating := models.Rating5To100(*input.Rating)
|
||||
newPerformer.Rating = &rating
|
||||
}
|
||||
if input.Details != nil {
|
||||
newPerformer.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||
newPerformer.Details = *input.Details
|
||||
}
|
||||
if input.DeathDate != nil {
|
||||
newPerformer.DeathDate = models.SQLiteDate{String: *input.DeathDate, Valid: true}
|
||||
d := models.NewDate(*input.DeathDate)
|
||||
newPerformer.DeathDate = &d
|
||||
}
|
||||
if input.HairColor != nil {
|
||||
newPerformer.HairColor = sql.NullString{String: *input.HairColor, Valid: true}
|
||||
newPerformer.HairColor = *input.HairColor
|
||||
}
|
||||
if input.Weight != nil {
|
||||
weight := int64(*input.Weight)
|
||||
newPerformer.Weight = sql.NullInt64{Int64: weight, Valid: true}
|
||||
newPerformer.Weight = input.Weight
|
||||
}
|
||||
if input.IgnoreAutoTag != nil {
|
||||
newPerformer.IgnoreAutoTag = *input.IgnoreAutoTag
|
||||
@@ -128,32 +144,31 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
}
|
||||
|
||||
// Start the transaction and save the performer
|
||||
var performer *models.Performer
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Performer()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
|
||||
performer, err = qb.Create(newPerformer)
|
||||
err = qb.Create(ctx, &newPerformer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(input.TagIds) > 0 {
|
||||
if err := r.updatePerformerTags(qb, performer.ID, input.TagIds); err != nil {
|
||||
if err := r.updatePerformerTags(ctx, newPerformer.ID, input.TagIds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(performer.ID, imageData); err != nil {
|
||||
if err := qb.UpdateImage(ctx, newPerformer.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(performer.ID, stashIDJoins); err != nil {
|
||||
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(ctx, newPerformer.ID, stashIDJoins); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -163,17 +178,14 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, performer.ID, plugin.PerformerCreatePost, input, nil)
|
||||
return r.getPerformer(ctx, performer.ID)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newPerformer.ID, plugin.PerformerCreatePost, input, nil)
|
||||
return r.getPerformer(ctx, newPerformer.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) {
|
||||
func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerUpdateInput) (*models.Performer, error) {
|
||||
// Populate performer from the input
|
||||
performerID, _ := strconv.Atoi(input.ID)
|
||||
updatedPerformer := models.PerformerPartial{
|
||||
ID: performerID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
|
||||
}
|
||||
updatedPerformer := models.NewPerformerPartial()
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
@@ -193,54 +205,62 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
// generate checksum from performer name rather than image
|
||||
checksum := md5.FromString(*input.Name)
|
||||
|
||||
updatedPerformer.Name = &sql.NullString{String: *input.Name, Valid: true}
|
||||
updatedPerformer.Checksum = &checksum
|
||||
updatedPerformer.Name = models.NewOptionalString(*input.Name)
|
||||
updatedPerformer.Checksum = models.NewOptionalString(checksum)
|
||||
}
|
||||
|
||||
updatedPerformer.URL = translator.nullString(input.URL, "url")
|
||||
updatedPerformer.URL = translator.optionalString(input.URL, "url")
|
||||
|
||||
if translator.hasField("gender") {
|
||||
if input.Gender != nil {
|
||||
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
|
||||
updatedPerformer.Gender = models.NewOptionalString(input.Gender.String())
|
||||
} else {
|
||||
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
|
||||
updatedPerformer.Gender = models.NewOptionalStringPtr(nil)
|
||||
}
|
||||
}
|
||||
|
||||
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
|
||||
updatedPerformer.Country = translator.nullString(input.Country, "country")
|
||||
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
|
||||
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
|
||||
updatedPerformer.Height = translator.nullString(input.Height, "height")
|
||||
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
|
||||
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
|
||||
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
|
||||
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
|
||||
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedPerformer.Details = translator.nullString(input.Details, "details")
|
||||
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
|
||||
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
|
||||
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
|
||||
updatedPerformer.IgnoreAutoTag = input.IgnoreAutoTag
|
||||
updatedPerformer.Birthdate = translator.optionalDate(input.Birthdate, "birthdate")
|
||||
updatedPerformer.Country = translator.optionalString(input.Country, "country")
|
||||
updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color")
|
||||
updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements")
|
||||
// prefer height_cm over height
|
||||
if translator.hasField("height_cm") {
|
||||
updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm")
|
||||
} else if translator.hasField("height") {
|
||||
updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
|
||||
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
|
||||
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
|
||||
updatedPerformer.Aliases = translator.optionalString(input.Aliases, "aliases")
|
||||
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedPerformer.Details = translator.optionalString(input.Details, "details")
|
||||
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
|
||||
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
|
||||
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
|
||||
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
|
||||
// Start the transaction and save the p
|
||||
var p *models.Performer
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Performer()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
|
||||
// need to get existing performer
|
||||
existing, err := qb.Find(updatedPerformer.ID)
|
||||
existing, err := qb.Find(ctx, performerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
return fmt.Errorf("performer with id %d not found", updatedPerformer.ID)
|
||||
return fmt.Errorf("performer with id %d not found", performerID)
|
||||
}
|
||||
|
||||
if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil {
|
||||
@@ -249,34 +269,34 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
}
|
||||
}
|
||||
|
||||
p, err = qb.Update(updatedPerformer)
|
||||
_, err = qb.UpdatePartial(ctx, performerID, updatedPerformer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
if err := r.updatePerformerTags(qb, p.ID, input.TagIds); err != nil {
|
||||
if err := r.updatePerformerTags(ctx, performerID, input.TagIds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(p.ID, imageData); err != nil {
|
||||
if err := qb.UpdateImage(ctx, performerID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if imageIncluded {
|
||||
// must be unsetting
|
||||
if err := qb.DestroyImage(p.ID); err != nil {
|
||||
if err := qb.DestroyImage(ctx, performerID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if translator.hasField("stash_ids") {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(performerID, stashIDJoins); err != nil {
|
||||
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(ctx, performerID, stashIDJoins); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -286,76 +306,81 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, p.ID, plugin.PerformerUpdatePost, input, translator.getFields())
|
||||
return r.getPerformer(ctx, p.ID)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, performerID, plugin.PerformerUpdatePost, input, translator.getFields())
|
||||
return r.getPerformer(ctx, performerID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updatePerformerTags(qb models.PerformerReaderWriter, performerID int, tagsIDs []string) error {
|
||||
func (r *mutationResolver) updatePerformerTags(ctx context.Context, performerID int, tagsIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(tagsIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateTags(performerID, ids)
|
||||
return r.repository.Performer.UpdateTags(ctx, performerID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models.BulkPerformerUpdateInput) ([]*models.Performer, error) {
|
||||
func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPerformerUpdateInput) ([]*models.Performer, error) {
|
||||
performerIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Populate performer from the input
|
||||
updatedTime := time.Now()
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
updatedPerformer := models.PerformerPartial{
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
updatedPerformer := models.NewPerformerPartial()
|
||||
|
||||
updatedPerformer.URL = translator.optionalString(input.URL, "url")
|
||||
updatedPerformer.Birthdate = translator.optionalDate(input.Birthdate, "birthdate")
|
||||
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
|
||||
updatedPerformer.Country = translator.optionalString(input.Country, "country")
|
||||
updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color")
|
||||
// prefer height_cm over height
|
||||
if translator.hasField("height_cm") {
|
||||
updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm")
|
||||
} else if translator.hasField("height") {
|
||||
updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
updatedPerformer.URL = translator.nullString(input.URL, "url")
|
||||
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
|
||||
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
|
||||
updatedPerformer.Country = translator.nullString(input.Country, "country")
|
||||
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
|
||||
updatedPerformer.Height = translator.nullString(input.Height, "height")
|
||||
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
|
||||
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
|
||||
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
|
||||
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
|
||||
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedPerformer.Details = translator.nullString(input.Details, "details")
|
||||
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
|
||||
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
|
||||
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
|
||||
updatedPerformer.IgnoreAutoTag = input.IgnoreAutoTag
|
||||
updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements")
|
||||
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
|
||||
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
|
||||
updatedPerformer.Aliases = translator.optionalString(input.Aliases, "aliases")
|
||||
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedPerformer.Details = translator.optionalString(input.Details, "details")
|
||||
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
|
||||
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
|
||||
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
|
||||
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
|
||||
if translator.hasField("gender") {
|
||||
if input.Gender != nil {
|
||||
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
|
||||
updatedPerformer.Gender = models.NewOptionalString(input.Gender.String())
|
||||
} else {
|
||||
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
|
||||
updatedPerformer.Gender = models.NewOptionalStringPtr(nil)
|
||||
}
|
||||
}
|
||||
|
||||
ret := []*models.Performer{}
|
||||
|
||||
// Start the transaction and save the scene marker
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Performer()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
|
||||
for _, performerID := range performerIDs {
|
||||
updatedPerformer.ID = performerID
|
||||
|
||||
// need to get existing performer
|
||||
existing, err := qb.Find(performerID)
|
||||
existing, err := qb.Find(ctx, performerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -368,7 +393,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models
|
||||
return err
|
||||
}
|
||||
|
||||
performer, err := qb.Update(updatedPerformer)
|
||||
performer, err := qb.UpdatePartial(ctx, performerID, updatedPerformer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -377,12 +402,12 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
tagIDs, err := adjustTagIDs(qb, performerID, *input.TagIds)
|
||||
tagIDs, err := adjustTagIDs(ctx, qb, performerID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateTags(performerID, tagIDs); err != nil {
|
||||
if err := qb.UpdateTags(ctx, performerID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -409,14 +434,14 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) PerformerDestroy(ctx context.Context, input models.PerformerDestroyInput) (bool, error) {
|
||||
func (r *mutationResolver) PerformerDestroy(ctx context.Context, input PerformerDestroyInput) (bool, error) {
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
return repo.Performer().Destroy(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.repository.Performer.Destroy(ctx, id)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -432,10 +457,10 @@ func (r *mutationResolver) PerformersDestroy(ctx context.Context, performerIDs [
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Performer()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(id); err != nil {
|
||||
if err := qb.Destroy(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, taskName string, args []*models.PluginArgInput) (string, error) {
|
||||
func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, taskName string, args []*plugin.PluginArgInput) (string, error) {
|
||||
m := manager.GetInstance()
|
||||
m.RunPluginTask(ctx, pluginID, taskName, args)
|
||||
return "todo", nil
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) SaveFilter(ctx context.Context, input models.SaveFilterInput) (ret *models.SavedFilter, err error) {
|
||||
func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput) (ret *models.SavedFilter, err error) {
|
||||
if strings.TrimSpace(input.Name) == "" {
|
||||
return nil, errors.New("name must be non-empty")
|
||||
}
|
||||
@@ -23,17 +23,17 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input models.SaveFilt
|
||||
id = &idv
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
f := models.SavedFilter{
|
||||
Mode: input.Mode,
|
||||
Name: input.Name,
|
||||
Filter: input.Filter,
|
||||
}
|
||||
if id == nil {
|
||||
ret, err = repo.SavedFilter().Create(f)
|
||||
ret, err = r.repository.SavedFilter.Create(ctx, f)
|
||||
} else {
|
||||
f.ID = *id
|
||||
ret, err = repo.SavedFilter().Update(f)
|
||||
ret, err = r.repository.SavedFilter.Update(ctx, f)
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -42,14 +42,14 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input models.SaveFilt
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input models.DestroyFilterInput) (bool, error) {
|
||||
func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input DestroyFilterInput) (bool, error) {
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
return repo.SavedFilter().Destroy(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.repository.SavedFilter.Destroy(ctx, id)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -57,25 +57,25 @@ func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input models.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input models.SetDefaultFilterInput) (bool, error) {
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.SavedFilter()
|
||||
func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaultFilterInput) (bool, error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.SavedFilter
|
||||
|
||||
if input.Filter == nil {
|
||||
// clearing
|
||||
def, err := qb.FindDefault(input.Mode)
|
||||
def, err := qb.FindDefault(ctx, input.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if def != nil {
|
||||
return qb.Destroy(def.ID)
|
||||
return qb.Destroy(ctx, def.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := qb.SetDefault(models.SavedFilter{
|
||||
_, err := qb.SetDefault(ctx, models.SavedFilter{
|
||||
Mode: input.Mode,
|
||||
Filter: *input.Filter,
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -15,12 +16,86 @@ import (
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) getScene(ctx context.Context, id int) (ret *models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Scene().Find(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Scene.Find(ctx, id)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneCreate(ctx context.Context, input SceneCreateInput) (ret *models.Scene, err error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
performerIDs, err := stringslice.StringSliceToIntSlice(input.PerformerIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
galleryIDs, err := stringslice.StringSliceToIntSlice(input.GalleryIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
|
||||
moviesScenes, err := models.MoviesScenesFromInput(input.Movies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting movies scenes: %w", err)
|
||||
}
|
||||
|
||||
fileIDsInt, err := stringslice.StringSliceToIntSlice(input.FileIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting file ids: %w", err)
|
||||
}
|
||||
|
||||
fileIDs := make([]file.ID, len(fileIDsInt))
|
||||
for i, v := range fileIDsInt {
|
||||
fileIDs[i] = file.ID(v)
|
||||
}
|
||||
|
||||
newScene := models.Scene{
|
||||
Title: translator.string(input.Title, "title"),
|
||||
Code: translator.string(input.Code, "code"),
|
||||
Details: translator.string(input.Details, "details"),
|
||||
Director: translator.string(input.Director, "director"),
|
||||
URL: translator.string(input.URL, "url"),
|
||||
Date: translator.datePtr(input.Date, "date"),
|
||||
Rating: translator.ratingConversionInt(input.Rating, input.Rating100),
|
||||
Organized: translator.bool(input.Organized, "organized"),
|
||||
PerformerIDs: models.NewRelatedIDs(performerIDs),
|
||||
TagIDs: models.NewRelatedIDs(tagIDs),
|
||||
GalleryIDs: models.NewRelatedIDs(galleryIDs),
|
||||
Movies: models.NewRelatedMovies(moviesScenes),
|
||||
StashIDs: models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)),
|
||||
}
|
||||
|
||||
newScene.StudioID, err = translator.intPtrFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
var coverImageData []byte
|
||||
if input.CoverImage != nil && *input.CoverImage != "" {
|
||||
var err error
|
||||
coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.Resolver.sceneService.Create(ctx, &newScene, fileIDs, coverImageData)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -35,8 +110,8 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
|
||||
}
|
||||
|
||||
// Start the transaction and save the scene
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
ret, err = r.sceneUpdate(ctx, input, translator, repo)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.sceneUpdate(ctx, input, translator)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -50,13 +125,13 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
|
||||
inputMaps := getUpdateInputMaps(ctx)
|
||||
|
||||
// Start the transaction and save the scene
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
for i, scene := range input {
|
||||
translator := changesetTranslator{
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
thisScene, err := r.sceneUpdate(ctx, *scene, translator, repo)
|
||||
thisScene, err := r.sceneUpdate(ctx, *scene, translator)
|
||||
ret = append(ret, thisScene)
|
||||
|
||||
if err != nil {
|
||||
@@ -89,28 +164,133 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Scene, error) {
|
||||
func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTranslator) (*models.ScenePartial, error) {
|
||||
updatedScene := models.NewScenePartial()
|
||||
updatedScene.Title = translator.optionalString(input.Title, "title")
|
||||
updatedScene.Code = translator.optionalString(input.Code, "code")
|
||||
updatedScene.Details = translator.optionalString(input.Details, "details")
|
||||
updatedScene.Director = translator.optionalString(input.Director, "director")
|
||||
updatedScene.URL = translator.optionalString(input.URL, "url")
|
||||
updatedScene.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedScene.OCounter = translator.optionalInt(input.OCounter, "o_counter")
|
||||
updatedScene.PlayCount = translator.optionalInt(input.PlayCount, "play_count")
|
||||
updatedScene.PlayDuration = translator.optionalFloat64(input.PlayDuration, "play_duration")
|
||||
var err error
|
||||
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
updatedScene.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
if input.PrimaryFileID != nil {
|
||||
primaryFileID, err := strconv.Atoi(*input.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||
}
|
||||
|
||||
converted := file.ID(primaryFileID)
|
||||
updatedScene.PrimaryFileID = &converted
|
||||
}
|
||||
|
||||
if translator.hasField("performer_ids") {
|
||||
updatedScene.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedScene.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("gallery_ids") {
|
||||
updatedScene.GalleryIDs, err = translateUpdateIDs(input.GalleryIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the movies
|
||||
if translator.hasField("movies") {
|
||||
updatedScene.MovieIDs, err = models.UpdateMovieIDsFromInput(input.Movies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting movie ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if translator.hasField("stash_ids") {
|
||||
updatedScene.StashIDs = &models.UpdateStashIDs{
|
||||
StashIDs: input.StashIds,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
}
|
||||
|
||||
return &updatedScene, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUpdateInput, translator changesetTranslator) (*models.Scene, error) {
|
||||
// Populate scene from the input
|
||||
sceneID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var coverImageData []byte
|
||||
qb := r.repository.Scene
|
||||
|
||||
updatedTime := time.Now()
|
||||
updatedScene := models.ScenePartial{
|
||||
ID: sceneID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
s, err := qb.Find(ctx, sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedScene.Title = translator.nullString(input.Title, "title")
|
||||
updatedScene.Details = translator.nullString(input.Details, "details")
|
||||
updatedScene.URL = translator.nullString(input.URL, "url")
|
||||
updatedScene.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedScene.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedScene.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedScene.Organized = input.Organized
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("scene with id %d not found", sceneID)
|
||||
}
|
||||
|
||||
var coverImageData []byte
|
||||
|
||||
updatedScene, err := scenePartialFromInput(input, translator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that title is set where scene has no file
|
||||
if updatedScene.Title.Set && updatedScene.Title.Value == "" {
|
||||
if err := s.LoadFiles(ctx, r.repository.Scene); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(s.Files.List()) == 0 {
|
||||
return nil, errors.New("title must be set if scene has no files")
|
||||
}
|
||||
}
|
||||
|
||||
if updatedScene.PrimaryFileID != nil {
|
||||
newPrimaryFileID := *updatedScene.PrimaryFileID
|
||||
|
||||
// if file hash has changed, we should migrate generated files
|
||||
// after commit
|
||||
if err := s.LoadFiles(ctx, r.repository.Scene); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that new primary file is associated with scene
|
||||
var f *file.VideoFile
|
||||
for _, ff := range s.Files.List() {
|
||||
if ff.ID == newPrimaryFileID {
|
||||
f = ff
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, fmt.Errorf("file with id %d not associated with scene", newPrimaryFileID)
|
||||
}
|
||||
}
|
||||
|
||||
if input.CoverImage != nil && *input.CoverImage != "" {
|
||||
var err error
|
||||
@@ -118,208 +298,108 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update the cover after updating the scene
|
||||
}
|
||||
|
||||
qb := repo.Scene()
|
||||
s, err := qb.Update(updatedScene)
|
||||
s, err = qb.UpdatePartial(ctx, sceneID, *updatedScene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update cover table
|
||||
if len(coverImageData) > 0 {
|
||||
if err := qb.UpdateCover(sceneID, coverImageData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
if translator.hasField("performer_ids") {
|
||||
if err := r.updateScenePerformers(qb, sceneID, input.PerformerIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the movies
|
||||
if translator.hasField("movies") {
|
||||
if err := r.updateSceneMovies(qb, sceneID, input.Movies); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
if err := r.updateSceneTags(qb, sceneID, input.TagIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the galleries
|
||||
if translator.hasField("gallery_ids") {
|
||||
if err := r.updateSceneGalleries(qb, sceneID, input.GalleryIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if translator.hasField("stash_ids") {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(sceneID, stashIDJoins); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// only update the cover image if provided and everything else was successful
|
||||
if coverImageData != nil {
|
||||
err = scene.SetScreenshot(manager.GetInstance().Paths, s.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.sceneUpdateCoverImage(ctx, s, coverImageData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateScenePerformers(qb models.SceneReaderWriter, sceneID int, performerIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(performerIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdatePerformers(sceneID, ids)
|
||||
}
|
||||
func (r *mutationResolver) sceneUpdateCoverImage(ctx context.Context, s *models.Scene, coverImageData []byte) error {
|
||||
if len(coverImageData) > 0 {
|
||||
qb := r.repository.Scene
|
||||
|
||||
func (r *mutationResolver) updateSceneMovies(qb models.SceneReaderWriter, sceneID int, movies []*models.SceneMovieInput) error {
|
||||
var movieJoins []models.MoviesScenes
|
||||
|
||||
for _, movie := range movies {
|
||||
movieID, err := strconv.Atoi(movie.MovieID)
|
||||
if err != nil {
|
||||
// update cover table
|
||||
if err := qb.UpdateCover(ctx, s.ID, coverImageData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
movieJoin := models.MoviesScenes{
|
||||
MovieID: movieID,
|
||||
if s.Path != "" {
|
||||
// update the file-based screenshot after commit
|
||||
txn.AddPostCommitHook(ctx, func(ctx context.Context) error {
|
||||
return scene.SetScreenshot(manager.GetInstance().Paths, s.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
|
||||
})
|
||||
}
|
||||
|
||||
if movie.SceneIndex != nil {
|
||||
movieJoin.SceneIndex = sql.NullInt64{
|
||||
Int64: int64(*movie.SceneIndex),
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
movieJoins = append(movieJoins, movieJoin)
|
||||
}
|
||||
|
||||
return qb.UpdateMovies(sceneID, movieJoins)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateSceneTags(qb models.SceneReaderWriter, sceneID int, tagsIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(tagsIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateTags(sceneID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateSceneGalleries(qb models.SceneReaderWriter, sceneID int, galleryIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(galleryIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateGalleries(sceneID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.BulkSceneUpdateInput) ([]*models.Scene, error) {
|
||||
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneUpdateInput) ([]*models.Scene, error) {
|
||||
sceneIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Populate scene from the input
|
||||
updatedTime := time.Now()
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
updatedScene := models.ScenePartial{
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
updatedScene := models.NewScenePartial()
|
||||
updatedScene.Title = translator.optionalString(input.Title, "title")
|
||||
updatedScene.Code = translator.optionalString(input.Code, "code")
|
||||
updatedScene.Details = translator.optionalString(input.Details, "details")
|
||||
updatedScene.Director = translator.optionalString(input.Director, "director")
|
||||
updatedScene.URL = translator.optionalString(input.URL, "url")
|
||||
updatedScene.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
updatedScene.Title = translator.nullString(input.Title, "title")
|
||||
updatedScene.Details = translator.nullString(input.Details, "details")
|
||||
updatedScene.URL = translator.nullString(input.URL, "url")
|
||||
updatedScene.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedScene.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedScene.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedScene.Organized = input.Organized
|
||||
updatedScene.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
if translator.hasField("performer_ids") {
|
||||
updatedScene.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedScene.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("gallery_ids") {
|
||||
updatedScene.GalleryIDs, err = translateUpdateIDs(input.GalleryIds.Ids, input.GalleryIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the movies
|
||||
if translator.hasField("movie_ids") {
|
||||
updatedScene.MovieIDs, err = translateSceneMovieIDs(*input.MovieIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting movie ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
ret := []*models.Scene{}
|
||||
|
||||
// Start the transaction and save the scene marker
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
for _, sceneID := range sceneIDs {
|
||||
updatedScene.ID = sceneID
|
||||
|
||||
scene, err := qb.Update(updatedScene)
|
||||
scene, err := qb.UpdatePartial(ctx, sceneID, updatedScene)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, scene)
|
||||
|
||||
// Save the performers
|
||||
if translator.hasField("performer_ids") {
|
||||
performerIDs, err := adjustScenePerformerIDs(qb, sceneID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdatePerformers(sceneID, performerIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
tagIDs, err := adjustTagIDs(qb, sceneID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateTags(sceneID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the galleries
|
||||
if translator.hasField("gallery_ids") {
|
||||
galleryIDs, err := adjustSceneGalleryIDs(qb, sceneID, *input.GalleryIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateGalleries(sceneID, galleryIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the movies
|
||||
if translator.hasField("movie_ids") {
|
||||
movies, err := adjustSceneMovieIDs(qb, sceneID, *input.MovieIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateMovies(sceneID, movies); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -343,9 +423,9 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
|
||||
func adjustIDs(existingIDs []int, updateIDs BulkUpdateIds) []int {
|
||||
// if we are setting the ids, just return the ids
|
||||
if updateIDs.Mode == models.BulkUpdateIDModeSet {
|
||||
if updateIDs.Mode == models.RelationshipUpdateModeSet {
|
||||
existingIDs = []int{}
|
||||
for _, idStr := range updateIDs.Ids {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
@@ -362,7 +442,7 @@ func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
|
||||
foundExisting := false
|
||||
for idx, existingID := range existingIDs {
|
||||
if existingID == id {
|
||||
if updateIDs.Mode == models.BulkUpdateIDModeRemove {
|
||||
if updateIDs.Mode == models.RelationshipUpdateModeRemove {
|
||||
// remove from the list
|
||||
existingIDs = append(existingIDs[:idx], existingIDs[idx+1:]...)
|
||||
}
|
||||
@@ -372,7 +452,7 @@ func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
|
||||
}
|
||||
}
|
||||
|
||||
if !foundExisting && updateIDs.Mode != models.BulkUpdateIDModeRemove {
|
||||
if !foundExisting && updateIDs.Mode != models.RelationshipUpdateModeRemove {
|
||||
existingIDs = append(existingIDs, id)
|
||||
}
|
||||
}
|
||||
@@ -380,21 +460,12 @@ func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
|
||||
return existingIDs
|
||||
}
|
||||
|
||||
func adjustScenePerformerIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetPerformerIDs(sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
type tagIDsGetter interface {
|
||||
GetTagIDs(id int) ([]int, error)
|
||||
GetTagIDs(ctx context.Context, id int) ([]int, error)
|
||||
}
|
||||
|
||||
func adjustTagIDs(qb tagIDsGetter, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetTagIDs(sceneID)
|
||||
func adjustTagIDs(ctx context.Context, qb tagIDsGetter, sceneID int, ids BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetTagIDs(ctx, sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -402,57 +473,6 @@ func adjustTagIDs(qb tagIDsGetter, sceneID int, ids models.BulkUpdateIds) (ret [
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustSceneGalleryIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetGalleryIDs(sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustSceneMovieIDs(qb models.SceneReader, sceneID int, updateIDs models.BulkUpdateIds) ([]models.MoviesScenes, error) {
|
||||
existingMovies, err := qb.GetMovies(sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if we are setting the ids, just return the ids
|
||||
if updateIDs.Mode == models.BulkUpdateIDModeSet {
|
||||
existingMovies = []models.MoviesScenes{}
|
||||
for _, idStr := range updateIDs.Ids {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
existingMovies = append(existingMovies, models.MoviesScenes{MovieID: id})
|
||||
}
|
||||
|
||||
return existingMovies, nil
|
||||
}
|
||||
|
||||
for _, idStr := range updateIDs.Ids {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
|
||||
// look for the id in the list
|
||||
foundExisting := false
|
||||
for idx, existingMovie := range existingMovies {
|
||||
if existingMovie.MovieID == id {
|
||||
if updateIDs.Mode == models.BulkUpdateIDModeRemove {
|
||||
// remove from the list
|
||||
existingMovies = append(existingMovies[:idx], existingMovies[idx+1:]...)
|
||||
}
|
||||
|
||||
foundExisting = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundExisting && updateIDs.Mode != models.BulkUpdateIDModeRemove {
|
||||
existingMovies = append(existingMovies, models.MoviesScenes{MovieID: id})
|
||||
}
|
||||
}
|
||||
|
||||
return existingMovies, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
|
||||
sceneID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -463,7 +483,7 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
|
||||
|
||||
var s *models.Scene
|
||||
fileDeleter := &scene.FileDeleter{
|
||||
Deleter: *file.NewDeleter(),
|
||||
Deleter: file.NewDeleter(),
|
||||
FileNamingAlgo: fileNamingAlgo,
|
||||
Paths: manager.GetInstance().Paths,
|
||||
}
|
||||
@@ -471,10 +491,10 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
|
||||
deleteGenerated := utils.IsTrue(input.DeleteGenerated)
|
||||
deleteFile := utils.IsTrue(input.DeleteFile)
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
var err error
|
||||
s, err = qb.Find(sceneID)
|
||||
s, err = qb.Find(ctx, sceneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -486,7 +506,7 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
|
||||
// kill any running encoders
|
||||
manager.KillRunningStreams(s, fileNamingAlgo)
|
||||
|
||||
return scene.Destroy(s, repo, fileDeleter, deleteGenerated, deleteFile)
|
||||
return r.sceneService.Destroy(ctx, s, fileDeleter, deleteGenerated, deleteFile)
|
||||
}); err != nil {
|
||||
fileDeleter.Rollback()
|
||||
return false, err
|
||||
@@ -498,8 +518,8 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
|
||||
// call post hook after performing the other actions
|
||||
r.hookExecutor.ExecutePostHooks(ctx, s.ID, plugin.SceneDestroyPost, plugin.SceneDestroyInput{
|
||||
SceneDestroyInput: input,
|
||||
Checksum: s.Checksum.String,
|
||||
OSHash: s.OSHash.String,
|
||||
Checksum: s.Checksum,
|
||||
OSHash: s.OSHash,
|
||||
Path: s.Path,
|
||||
}, nil)
|
||||
|
||||
@@ -511,7 +531,7 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
|
||||
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
|
||||
|
||||
fileDeleter := &scene.FileDeleter{
|
||||
Deleter: *file.NewDeleter(),
|
||||
Deleter: file.NewDeleter(),
|
||||
FileNamingAlgo: fileNamingAlgo,
|
||||
Paths: manager.GetInstance().Paths,
|
||||
}
|
||||
@@ -519,13 +539,13 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
|
||||
deleteGenerated := utils.IsTrue(input.DeleteGenerated)
|
||||
deleteFile := utils.IsTrue(input.DeleteFile)
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
for _, id := range input.Ids {
|
||||
sceneID, _ := strconv.Atoi(id)
|
||||
|
||||
s, err := qb.Find(sceneID)
|
||||
s, err := qb.Find(ctx, sceneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -536,7 +556,7 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
|
||||
// kill any running encoders
|
||||
manager.KillRunningStreams(s, fileNamingAlgo)
|
||||
|
||||
if err := scene.Destroy(s, repo, fileDeleter, deleteGenerated, deleteFile); err != nil {
|
||||
if err := r.sceneService.Destroy(ctx, s, fileDeleter, deleteGenerated, deleteFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -554,8 +574,8 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
|
||||
// call post hook after performing the other actions
|
||||
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, plugin.SceneDestroyPost, plugin.ScenesDestroyInput{
|
||||
ScenesDestroyInput: input,
|
||||
Checksum: scene.Checksum.String,
|
||||
OSHash: scene.OSHash.String,
|
||||
Checksum: scene.Checksum,
|
||||
OSHash: scene.OSHash,
|
||||
Path: scene.Path,
|
||||
}, nil)
|
||||
}
|
||||
@@ -563,9 +583,76 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) getSceneMarker(ctx context.Context, id int) (ret *models.SceneMarker, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.SceneMarker().Find(id)
|
||||
func (r *mutationResolver) SceneAssignFile(ctx context.Context, input AssignSceneFileInput) (bool, error) {
|
||||
sceneID, err := strconv.Atoi(input.SceneID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting scene ID: %w", err)
|
||||
}
|
||||
|
||||
fileIDInt, err := strconv.Atoi(input.FileID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting file ID: %w", err)
|
||||
}
|
||||
|
||||
fileID := file.ID(fileIDInt)
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.Resolver.sceneService.AssignFile(ctx, sceneID, fileID)
|
||||
}); err != nil {
|
||||
return false, fmt.Errorf("assigning file to scene: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput) (*models.Scene, error) {
|
||||
srcIDs, err := stringslice.StringSliceToIntSlice(input.Source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting source IDs: %w", err)
|
||||
}
|
||||
|
||||
destID, err := strconv.Atoi(input.Destination)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting destination ID %s: %w", input.Destination, err)
|
||||
}
|
||||
|
||||
var values *models.ScenePartial
|
||||
if input.Values != nil {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getNamedUpdateInputMap(ctx, "input.values"),
|
||||
}
|
||||
|
||||
values, err = scenePartialFromInput(*input.Values, translator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
v := models.NewScenePartial()
|
||||
values = &v
|
||||
}
|
||||
|
||||
var coverImageData []byte
|
||||
|
||||
if input.Values.CoverImage != nil && *input.Values.CoverImage != "" {
|
||||
var err error
|
||||
coverImageData, err = utils.ProcessImageInput(ctx, *input.Values.CoverImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var ret *models.Scene
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.Resolver.sceneService.Merge(ctx, srcIDs, destID, *values); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret, err = r.Resolver.repository.Scene.Find(ctx, destID)
|
||||
|
||||
if err == nil && ret != nil {
|
||||
err = r.sceneUpdateCoverImage(ctx, ret, coverImageData)
|
||||
}
|
||||
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -574,7 +661,18 @@ func (r *mutationResolver) getSceneMarker(ctx context.Context, id int) (ret *mod
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.SceneMarkerCreateInput) (*models.SceneMarker, error) {
|
||||
func (r *mutationResolver) getSceneMarker(ctx context.Context, id int) (ret *models.SceneMarker, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.Find(ctx, id)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMarkerCreateInput) (*models.SceneMarker, error) {
|
||||
primaryTagID, err := strconv.Atoi(input.PrimaryTagID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -609,7 +707,7 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.S
|
||||
return r.getSceneMarker(ctx, ret.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input models.SceneMarkerUpdateInput) (*models.SceneMarker, error) {
|
||||
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) {
|
||||
// Populate scene marker from the input
|
||||
sceneMarkerID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -661,16 +759,16 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
|
||||
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
|
||||
|
||||
fileDeleter := &scene.FileDeleter{
|
||||
Deleter: *file.NewDeleter(),
|
||||
Deleter: file.NewDeleter(),
|
||||
FileNamingAlgo: fileNamingAlgo,
|
||||
Paths: manager.GetInstance().Paths,
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.SceneMarker()
|
||||
sqb := repo.Scene()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.SceneMarker
|
||||
sqb := r.repository.Scene
|
||||
|
||||
marker, err := qb.Find(markerID)
|
||||
marker, err := qb.Find(ctx, markerID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -680,12 +778,12 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
|
||||
return fmt.Errorf("scene marker with id %d not found", markerID)
|
||||
}
|
||||
|
||||
s, err := sqb.Find(int(marker.SceneID.Int64))
|
||||
s, err := sqb.Find(ctx, int(marker.SceneID.Int64))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return scene.DestroyMarker(s, marker, qb, fileDeleter)
|
||||
return scene.DestroyMarker(ctx, s, marker, qb, fileDeleter)
|
||||
}); err != nil {
|
||||
fileDeleter.Rollback()
|
||||
return false, err
|
||||
@@ -707,32 +805,32 @@ func (r *mutationResolver) changeMarker(ctx context.Context, changeType int, cha
|
||||
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
|
||||
|
||||
fileDeleter := &scene.FileDeleter{
|
||||
Deleter: *file.NewDeleter(),
|
||||
Deleter: file.NewDeleter(),
|
||||
FileNamingAlgo: fileNamingAlgo,
|
||||
Paths: manager.GetInstance().Paths,
|
||||
}
|
||||
|
||||
// Start the transaction and save the scene marker
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.SceneMarker()
|
||||
sqb := repo.Scene()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.SceneMarker
|
||||
sqb := r.repository.Scene
|
||||
|
||||
var err error
|
||||
switch changeType {
|
||||
case create:
|
||||
sceneMarker, err = qb.Create(changedMarker)
|
||||
sceneMarker, err = qb.Create(ctx, changedMarker)
|
||||
case update:
|
||||
// check to see if timestamp was changed
|
||||
existingMarker, err = qb.Find(changedMarker.ID)
|
||||
existingMarker, err = qb.Find(ctx, changedMarker.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sceneMarker, err = qb.Update(changedMarker)
|
||||
sceneMarker, err = qb.Update(ctx, changedMarker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err = sqb.Find(int(existingMarker.SceneID.Int64))
|
||||
s, err = sqb.Find(ctx, int(existingMarker.SceneID.Int64))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -749,7 +847,7 @@ func (r *mutationResolver) changeMarker(ctx context.Context, changeType int, cha
|
||||
// Save the marker tags
|
||||
// If this tag is the primary tag, then let's not add it.
|
||||
tagIDs = intslice.IntExclude(tagIDs, []int{changedMarker.PrimaryTagID})
|
||||
return qb.UpdateTags(sceneMarker.ID, tagIDs)
|
||||
return qb.UpdateTags(ctx, sceneMarker.ID, tagIDs)
|
||||
}); err != nil {
|
||||
fileDeleter.Rollback()
|
||||
return nil, err
|
||||
@@ -760,16 +858,52 @@ func (r *mutationResolver) changeMarker(ctx context.Context, changeType int, cha
|
||||
return sceneMarker, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, resumeTime *float64, playDuration *float64) (ret bool, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.SaveActivity(ctx, sceneID, resumeTime, playDuration)
|
||||
return err
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.IncrementWatchCount(ctx, sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.IncrementOCounter(sceneID)
|
||||
ret, err = qb.IncrementOCounter(ctx, sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -784,10 +918,10 @@ func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (ret
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.DecrementOCounter(sceneID)
|
||||
ret, err = qb.DecrementOCounter(ctx, sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -802,10 +936,10 @@ func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int,
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.ResetOCounter(sceneID)
|
||||
ret, err = qb.ResetOCounter(ctx, sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -7,35 +7,43 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input models.StashBoxFingerprintSubmissionInput) (bool, error) {
|
||||
func (r *Resolver) stashboxRepository() stashbox.Repository {
|
||||
return stashbox.Repository{
|
||||
Scene: r.repository.Scene,
|
||||
Performer: r.repository.Performer,
|
||||
Tag: r.repository.Tag,
|
||||
Studio: r.repository.Studio,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input StashBoxFingerprintSubmissionInput) (bool, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
||||
return false, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
|
||||
|
||||
return client.SubmitStashBoxFingerprints(ctx, input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input models.StashBoxBatchPerformerTagInput) (string, error) {
|
||||
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input manager.StashBoxBatchPerformerTagInput) (string, error) {
|
||||
jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, input)
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input models.StashBoxDraftSubmissionInput) (*string, error) {
|
||||
func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
||||
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
|
||||
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -43,29 +51,30 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input m
|
||||
}
|
||||
|
||||
var res *string
|
||||
err = r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Scene()
|
||||
scene, err := qb.Find(id)
|
||||
err = r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
scene, err := qb.Find(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filepath := manager.GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
|
||||
|
||||
res, err = client.SubmitSceneDraft(ctx, id, boxes[input.StashBoxIndex].Endpoint, filepath)
|
||||
res, err = client.SubmitSceneDraft(ctx, scene, boxes[input.StashBoxIndex].Endpoint, filepath)
|
||||
return err
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, input models.StashBoxDraftSubmissionInput) (*string, error) {
|
||||
func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
||||
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
|
||||
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -73,9 +82,9 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
|
||||
}
|
||||
|
||||
var res *string
|
||||
err = r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Performer()
|
||||
performer, err := qb.Find(id)
|
||||
err = r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
performer, err := qb.Find(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
)
|
||||
|
||||
func (r *mutationResolver) getStudio(ctx context.Context, id int) (ret *models.Studio, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().Find(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Studio.Find(ctx, id)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -27,7 +27,7 @@ func (r *mutationResolver) getStudio(ctx context.Context, id int) (ret *models.S
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StudioCreate(ctx context.Context, input models.StudioCreateInput) (*models.Studio, error) {
|
||||
func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateInput) (*models.Studio, error) {
|
||||
// generate checksum from studio name rather than image
|
||||
checksum := md5.FromString(input.Name)
|
||||
|
||||
@@ -58,11 +58,18 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
newStudio.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
newStudio.Rating = sql.NullInt64{Valid: false}
|
||||
if input.Rating100 != nil {
|
||||
newStudio.Rating = sql.NullInt64{
|
||||
Int64: int64(*input.Rating100),
|
||||
Valid: true,
|
||||
}
|
||||
} else if input.Rating != nil {
|
||||
newStudio.Rating = sql.NullInt64{
|
||||
Int64: int64(models.Rating5To100(*input.Rating)),
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
if input.Details != nil {
|
||||
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
@@ -72,36 +79,36 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
|
||||
// Start the transaction and save the studio
|
||||
var s *models.Studio
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Studio()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Studio
|
||||
|
||||
var err error
|
||||
s, err = qb.Create(newStudio)
|
||||
s, err = qb.Create(ctx, newStudio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(s.ID, imageData); err != nil {
|
||||
if err := qb.UpdateImage(ctx, s.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(s.ID, stashIDJoins); err != nil {
|
||||
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(ctx, s.ID, stashIDJoins); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(input.Aliases) > 0 {
|
||||
if err := studio.EnsureAliasesUnique(s.ID, input.Aliases, qb); err != nil {
|
||||
if err := studio.EnsureAliasesUnique(ctx, s.ID, input.Aliases, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateAliases(s.ID, input.Aliases); err != nil {
|
||||
if err := qb.UpdateAliases(ctx, s.ID, input.Aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -115,7 +122,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
return r.getStudio(ctx, s.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.StudioUpdateInput) (*models.Studio, error) {
|
||||
func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateInput) (*models.Studio, error) {
|
||||
// Populate studio from the input
|
||||
studioID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -150,50 +157,50 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
updatedStudio.URL = translator.nullString(input.URL, "url")
|
||||
updatedStudio.Details = translator.nullString(input.Details, "details")
|
||||
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
|
||||
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedStudio.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
updatedStudio.IgnoreAutoTag = input.IgnoreAutoTag
|
||||
|
||||
// Start the transaction and save the studio
|
||||
var s *models.Studio
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Studio()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Studio
|
||||
|
||||
if err := manager.ValidateModifyStudio(updatedStudio, qb); err != nil {
|
||||
if err := manager.ValidateModifyStudio(ctx, updatedStudio, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
s, err = qb.Update(updatedStudio)
|
||||
s, err = qb.Update(ctx, updatedStudio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(s.ID, imageData); err != nil {
|
||||
if err := qb.UpdateImage(ctx, s.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if imageIncluded {
|
||||
// must be unsetting
|
||||
if err := qb.DestroyImage(s.ID); err != nil {
|
||||
if err := qb.DestroyImage(ctx, s.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if translator.hasField("stash_ids") {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(studioID, stashIDJoins); err != nil {
|
||||
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(ctx, studioID, stashIDJoins); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("aliases") {
|
||||
if err := studio.EnsureAliasesUnique(studioID, input.Aliases, qb); err != nil {
|
||||
if err := studio.EnsureAliasesUnique(ctx, studioID, input.Aliases, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateAliases(studioID, input.Aliases); err != nil {
|
||||
if err := qb.UpdateAliases(ctx, studioID, input.Aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -207,14 +214,14 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
return r.getStudio(ctx, s.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StudioDestroy(ctx context.Context, input models.StudioDestroyInput) (bool, error) {
|
||||
func (r *mutationResolver) StudioDestroy(ctx context.Context, input StudioDestroyInput) (bool, error) {
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
return repo.Studio().Destroy(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.repository.Studio.Destroy(ctx, id)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -230,10 +237,10 @@ func (r *mutationResolver) StudiosDestroy(ctx context.Context, studioIDs []strin
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Studio()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Studio
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(id); err != nil {
|
||||
if err := qb.Destroy(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -15,8 +16,8 @@ import (
|
||||
)
|
||||
|
||||
func (r *mutationResolver) getTag(ctx context.Context, id int) (ret *models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().Find(id)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.Find(ctx, id)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -25,7 +26,7 @@ func (r *mutationResolver) getTag(ctx context.Context, id int) (ret *models.Tag,
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreateInput) (*models.Tag, error) {
|
||||
func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput) (*models.Tag, error) {
|
||||
// Populate a new tag from the input
|
||||
currentTime := time.Now()
|
||||
newTag := models.Tag{
|
||||
@@ -34,6 +35,10 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
}
|
||||
|
||||
if input.Description != nil {
|
||||
newTag.Description = sql.NullString{String: *input.Description, Valid: true}
|
||||
}
|
||||
|
||||
if input.IgnoreAutoTag != nil {
|
||||
newTag.IgnoreAutoTag = *input.IgnoreAutoTag
|
||||
}
|
||||
@@ -68,44 +73,44 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
|
||||
|
||||
// Start the transaction and save the tag
|
||||
var t *models.Tag
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Tag()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Tag
|
||||
|
||||
// ensure name is unique
|
||||
if err := tag.EnsureTagNameUnique(0, newTag.Name, qb); err != nil {
|
||||
if err := tag.EnsureTagNameUnique(ctx, 0, newTag.Name, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, err = qb.Create(newTag)
|
||||
t, err = qb.Create(ctx, newTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(t.ID, imageData); err != nil {
|
||||
if err := qb.UpdateImage(ctx, t.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(input.Aliases) > 0 {
|
||||
if err := tag.EnsureAliasesUnique(t.ID, input.Aliases, qb); err != nil {
|
||||
if err := tag.EnsureAliasesUnique(ctx, t.ID, input.Aliases, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateAliases(t.ID, input.Aliases); err != nil {
|
||||
if err := qb.UpdateAliases(ctx, t.ID, input.Aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(parentIDs) > 0 {
|
||||
if err := qb.UpdateParentTags(t.ID, parentIDs); err != nil {
|
||||
if err := qb.UpdateParentTags(ctx, t.ID, parentIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(childIDs) > 0 {
|
||||
if err := qb.UpdateChildTags(t.ID, childIDs); err != nil {
|
||||
if err := qb.UpdateChildTags(ctx, t.ID, childIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -113,7 +118,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
|
||||
// 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(t, parentIDs, childIDs, qb); err != nil {
|
||||
if err := tag.ValidateHierarchy(ctx, t, parentIDs, childIDs, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -127,7 +132,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
|
||||
return r.getTag(ctx, t.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdateInput) (*models.Tag, error) {
|
||||
func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) (*models.Tag, error) {
|
||||
// Populate tag from the input
|
||||
tagID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -168,11 +173,11 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
||||
|
||||
// Start the transaction and save the tag
|
||||
var t *models.Tag
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Tag()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Tag
|
||||
|
||||
// ensure name is unique
|
||||
t, err = qb.Find(tagID)
|
||||
t, err = qb.Find(ctx, tagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -188,48 +193,50 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
||||
}
|
||||
|
||||
if input.Name != nil && t.Name != *input.Name {
|
||||
if err := tag.EnsureTagNameUnique(tagID, *input.Name, qb); err != nil {
|
||||
if err := tag.EnsureTagNameUnique(ctx, tagID, *input.Name, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updatedTag.Name = input.Name
|
||||
}
|
||||
|
||||
t, err = qb.Update(updatedTag)
|
||||
updatedTag.Description = translator.nullString(input.Description, "description")
|
||||
|
||||
t, err = qb.Update(ctx, updatedTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(tagID, imageData); err != nil {
|
||||
if err := qb.UpdateImage(ctx, tagID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if imageIncluded {
|
||||
// must be unsetting
|
||||
if err := qb.DestroyImage(tagID); err != nil {
|
||||
if err := qb.DestroyImage(ctx, tagID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("aliases") {
|
||||
if err := tag.EnsureAliasesUnique(tagID, input.Aliases, qb); err != nil {
|
||||
if err := tag.EnsureAliasesUnique(ctx, tagID, input.Aliases, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateAliases(tagID, input.Aliases); err != nil {
|
||||
if err := qb.UpdateAliases(ctx, tagID, input.Aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if parentIDs != nil {
|
||||
if err := qb.UpdateParentTags(tagID, parentIDs); err != nil {
|
||||
if err := qb.UpdateParentTags(ctx, tagID, parentIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if childIDs != nil {
|
||||
if err := qb.UpdateChildTags(tagID, childIDs); err != nil {
|
||||
if err := qb.UpdateChildTags(ctx, tagID, childIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -237,7 +244,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
||||
// 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(t, parentIDs, childIDs, qb); err != nil {
|
||||
if err := tag.ValidateHierarchy(ctx, t, parentIDs, childIDs, qb); err != nil {
|
||||
logger.Errorf("Error saving tag: %s", err)
|
||||
return err
|
||||
}
|
||||
@@ -252,14 +259,14 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
||||
return r.getTag(ctx, t.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) TagDestroy(ctx context.Context, input models.TagDestroyInput) (bool, error) {
|
||||
func (r *mutationResolver) TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error) {
|
||||
tagID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
return repo.Tag().Destroy(tagID)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.repository.Tag.Destroy(ctx, tagID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -275,10 +282,10 @@ func (r *mutationResolver) TagsDestroy(ctx context.Context, tagIDs []string) (bo
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Tag()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Tag
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(id); err != nil {
|
||||
if err := qb.Destroy(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -295,7 +302,7 @@ func (r *mutationResolver) TagsDestroy(ctx context.Context, tagIDs []string) (bo
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) TagsMerge(ctx context.Context, input models.TagsMergeInput) (*models.Tag, error) {
|
||||
func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput) (*models.Tag, error) {
|
||||
source, err := stringslice.StringSliceToIntSlice(input.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -311,11 +318,11 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input models.TagsMerge
|
||||
}
|
||||
|
||||
var t *models.Tag
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Tag()
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Tag
|
||||
|
||||
var err error
|
||||
t, err = qb.Find(destination)
|
||||
t, err = qb.Find(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -324,25 +331,25 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input models.TagsMerge
|
||||
return fmt.Errorf("Tag with ID %d not found", destination)
|
||||
}
|
||||
|
||||
parents, children, err := tag.MergeHierarchy(destination, source, qb)
|
||||
parents, children, err := tag.MergeHierarchy(ctx, destination, source, qb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = qb.Merge(source, destination); err != nil {
|
||||
if err = qb.Merge(ctx, source, destination); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = qb.UpdateParentTags(destination, parents)
|
||||
err = qb.UpdateParentTags(ctx, destination, parents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = qb.UpdateChildTags(destination, children)
|
||||
err = qb.UpdateChildTags(ctx, destination, children)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tag.ValidateHierarchy(t, parents, children, qb)
|
||||
err = tag.ValidateHierarchy(ctx, t, parents, children, qb)
|
||||
if err != nil {
|
||||
logger.Errorf("Error merging tag: %s", err)
|
||||
return err
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
@@ -15,18 +16,28 @@ import (
|
||||
|
||||
// TODO - move this into a common area
|
||||
func newResolver() *Resolver {
|
||||
txnMgr := &mocks.TxnManager{}
|
||||
return &Resolver{
|
||||
txnManager: mocks.NewTransactionManager(),
|
||||
txnManager: txnMgr,
|
||||
repository: manager.Repository{
|
||||
TxnManager: txnMgr,
|
||||
Tag: &mocks.TagReaderWriter{},
|
||||
},
|
||||
hookExecutor: &mockHookExecutor{},
|
||||
}
|
||||
}
|
||||
|
||||
const tagName = "tagName"
|
||||
const errTagName = "errTagName"
|
||||
const (
|
||||
tagName = "tagName"
|
||||
errTagName = "errTagName"
|
||||
|
||||
const existingTagID = 1
|
||||
const existingTagName = "existingTagName"
|
||||
const newTagID = 2
|
||||
existingTagID = 1
|
||||
existingTagName = "existingTagName"
|
||||
|
||||
newTagID = 2
|
||||
)
|
||||
|
||||
var testCtx = context.Background()
|
||||
|
||||
type mockHookExecutor struct{}
|
||||
|
||||
@@ -36,7 +47,7 @@ func (*mockHookExecutor) ExecutePostHooks(ctx context.Context, id int, hookType
|
||||
func TestTagCreate(t *testing.T) {
|
||||
r := newResolver()
|
||||
|
||||
tagRW := r.txnManager.(*mocks.TransactionManager).Tag().(*mocks.TagReaderWriter)
|
||||
tagRW := r.repository.Tag.(*mocks.TagReaderWriter)
|
||||
|
||||
pp := 1
|
||||
findFilter := &models.FindFilterType{
|
||||
@@ -61,25 +72,25 @@ func TestTagCreate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
tagRW.On("Query", tagFilterForName(existingTagName), findFilter).Return([]*models.Tag{
|
||||
tagRW.On("Query", mock.Anything, tagFilterForName(existingTagName), findFilter).Return([]*models.Tag{
|
||||
{
|
||||
ID: existingTagID,
|
||||
Name: existingTagName,
|
||||
},
|
||||
}, 1, nil).Once()
|
||||
tagRW.On("Query", tagFilterForName(errTagName), findFilter).Return(nil, 0, nil).Once()
|
||||
tagRW.On("Query", tagFilterForAlias(errTagName), findFilter).Return(nil, 0, nil).Once()
|
||||
tagRW.On("Query", mock.Anything, tagFilterForName(errTagName), findFilter).Return(nil, 0, nil).Once()
|
||||
tagRW.On("Query", mock.Anything, tagFilterForAlias(errTagName), findFilter).Return(nil, 0, nil).Once()
|
||||
|
||||
expectedErr := errors.New("TagCreate error")
|
||||
tagRW.On("Create", mock.AnythingOfType("models.Tag")).Return(nil, expectedErr)
|
||||
tagRW.On("Create", mock.Anything, mock.AnythingOfType("models.Tag")).Return(nil, expectedErr)
|
||||
|
||||
_, err := r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
|
||||
_, err := r.Mutation().TagCreate(testCtx, TagCreateInput{
|
||||
Name: existingTagName,
|
||||
})
|
||||
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, err = r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
|
||||
_, err = r.Mutation().TagCreate(testCtx, TagCreateInput{
|
||||
Name: errTagName,
|
||||
})
|
||||
|
||||
@@ -87,18 +98,18 @@ func TestTagCreate(t *testing.T) {
|
||||
tagRW.AssertExpectations(t)
|
||||
|
||||
r = newResolver()
|
||||
tagRW = r.txnManager.(*mocks.TransactionManager).Tag().(*mocks.TagReaderWriter)
|
||||
tagRW = r.repository.Tag.(*mocks.TagReaderWriter)
|
||||
|
||||
tagRW.On("Query", tagFilterForName(tagName), findFilter).Return(nil, 0, nil).Once()
|
||||
tagRW.On("Query", tagFilterForAlias(tagName), findFilter).Return(nil, 0, nil).Once()
|
||||
tagRW.On("Query", mock.Anything, tagFilterForName(tagName), findFilter).Return(nil, 0, nil).Once()
|
||||
tagRW.On("Query", mock.Anything, tagFilterForAlias(tagName), findFilter).Return(nil, 0, nil).Once()
|
||||
newTag := &models.Tag{
|
||||
ID: newTagID,
|
||||
Name: tagName,
|
||||
}
|
||||
tagRW.On("Create", mock.AnythingOfType("models.Tag")).Return(newTag, nil)
|
||||
tagRW.On("Find", newTagID).Return(newTag, nil)
|
||||
tagRW.On("Create", mock.Anything, mock.AnythingOfType("models.Tag")).Return(newTag, nil)
|
||||
tagRW.On("Find", mock.Anything, newTagID).Return(newTag, nil)
|
||||
|
||||
tag, err := r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
|
||||
tag, err := r.Mutation().TagCreate(testCtx, TagCreateInput{
|
||||
Name: tagName,
|
||||
})
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ import (
|
||||
"golang.org/x/text/collate"
|
||||
)
|
||||
|
||||
func (r *queryResolver) Configuration(ctx context.Context) (*models.ConfigResult, error) {
|
||||
func (r *queryResolver) Configuration(ctx context.Context) (*ConfigResult, error) {
|
||||
return makeConfigResult(), nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Directory(ctx context.Context, path, locale *string) (*models.Directory, error) {
|
||||
func (r *queryResolver) Directory(ctx context.Context, path, locale *string) (*Directory, error) {
|
||||
|
||||
directory := &models.Directory{}
|
||||
directory := &Directory{}
|
||||
var err error
|
||||
|
||||
col := newCollator(locale, collate.IgnoreCase, collate.Numeric)
|
||||
@@ -59,17 +59,18 @@ func getParent(path string) *string {
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigResult() *models.ConfigResult {
|
||||
return &models.ConfigResult{
|
||||
func makeConfigResult() *ConfigResult {
|
||||
return &ConfigResult{
|
||||
General: makeConfigGeneralResult(),
|
||||
Interface: makeConfigInterfaceResult(),
|
||||
Dlna: makeConfigDLNAResult(),
|
||||
Scraping: makeConfigScrapingResult(),
|
||||
Defaults: makeConfigDefaultsResult(),
|
||||
UI: makeConfigUIResult(),
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
func makeConfigGeneralResult() *ConfigGeneralResult {
|
||||
config := config.GetInstance()
|
||||
logFile := config.GetLogFile()
|
||||
|
||||
@@ -81,9 +82,10 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
scraperUserAgent := config.GetScraperUserAgent()
|
||||
scraperCDPPath := config.GetScraperCDPPath()
|
||||
|
||||
return &models.ConfigGeneralResult{
|
||||
return &ConfigGeneralResult{
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
BackupDirectoryPath: config.GetBackupDirectoryPath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
MetadataPath: config.GetMetadataPath(),
|
||||
ConfigFilePath: config.GetConfigFile(),
|
||||
@@ -124,7 +126,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
func makeConfigInterfaceResult() *ConfigInterfaceResult {
|
||||
config := config.GetInstance()
|
||||
menuItems := config.GetMenuItems()
|
||||
soundOnPreview := config.GetSoundOnPreview()
|
||||
@@ -140,15 +142,18 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
showStudioAsText := config.GetShowStudioAsText()
|
||||
css := config.GetCSS()
|
||||
cssEnabled := config.GetCSSEnabled()
|
||||
javascript := config.GetJavascript()
|
||||
javascriptEnabled := config.GetJavascriptEnabled()
|
||||
customLocales := config.GetCustomLocales()
|
||||
customLocalesEnabled := config.GetCustomLocalesEnabled()
|
||||
language := config.GetLanguage()
|
||||
handyKey := config.GetHandyKey()
|
||||
scriptOffset := config.GetFunscriptOffset()
|
||||
imageLightboxOptions := config.GetImageLightboxOptions()
|
||||
|
||||
// FIXME - misnamed output field means we have redundant fields
|
||||
disableDropdownCreate := config.GetDisableDropdownCreate()
|
||||
|
||||
return &models.ConfigInterfaceResult{
|
||||
return &ConfigInterfaceResult{
|
||||
MenuItems: menuItems,
|
||||
SoundOnPreview: &soundOnPreview,
|
||||
WallShowTitle: &wallShowTitle,
|
||||
@@ -163,6 +168,10 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
ContinuePlaylistDefault: &continuePlaylistDefault,
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
Javascript: &javascript,
|
||||
JavascriptEnabled: &javascriptEnabled,
|
||||
CustomLocales: &customLocales,
|
||||
CustomLocalesEnabled: &customLocalesEnabled,
|
||||
Language: &language,
|
||||
|
||||
ImageLightbox: &imageLightboxOptions,
|
||||
@@ -176,10 +185,10 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigDLNAResult() *models.ConfigDLNAResult {
|
||||
func makeConfigDLNAResult() *ConfigDLNAResult {
|
||||
config := config.GetInstance()
|
||||
|
||||
return &models.ConfigDLNAResult{
|
||||
return &ConfigDLNAResult{
|
||||
ServerName: config.GetDLNAServerName(),
|
||||
Enabled: config.GetDLNADefaultEnabled(),
|
||||
WhitelistedIPs: config.GetDLNADefaultIPWhitelist(),
|
||||
@@ -187,13 +196,13 @@ func makeConfigDLNAResult() *models.ConfigDLNAResult {
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigScrapingResult() *models.ConfigScrapingResult {
|
||||
func makeConfigScrapingResult() *ConfigScrapingResult {
|
||||
config := config.GetInstance()
|
||||
|
||||
scraperUserAgent := config.GetScraperUserAgent()
|
||||
scraperCDPPath := config.GetScraperCDPPath()
|
||||
|
||||
return &models.ConfigScrapingResult{
|
||||
return &ConfigScrapingResult{
|
||||
ScraperUserAgent: &scraperUserAgent,
|
||||
ScraperCertCheck: config.GetScraperCertCheck(),
|
||||
ScraperCDPPath: &scraperCDPPath,
|
||||
@@ -201,12 +210,12 @@ func makeConfigScrapingResult() *models.ConfigScrapingResult {
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult {
|
||||
func makeConfigDefaultsResult() *ConfigDefaultSettingsResult {
|
||||
config := config.GetInstance()
|
||||
deleteFileDefault := config.GetDeleteFileDefault()
|
||||
deleteGeneratedDefault := config.GetDeleteGeneratedDefault()
|
||||
|
||||
return &models.ConfigDefaultSettingsResult{
|
||||
return &ConfigDefaultSettingsResult{
|
||||
Identify: config.GetDefaultIdentifySettings(),
|
||||
Scan: config.GetDefaultScanSettings(),
|
||||
AutoTag: config.GetDefaultAutoTagSettings(),
|
||||
@@ -216,8 +225,12 @@ func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input models.StashBoxInput) (*models.StashBoxValidationResult, error) {
|
||||
client := stashbox.NewClient(models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}, r.txnManager)
|
||||
func makeConfigUIResult() map[string]interface{} {
|
||||
return config.GetInstance().GetUIConfiguration()
|
||||
}
|
||||
|
||||
func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input config.StashBoxInput) (*StashBoxValidationResult, error) {
|
||||
client := stashbox.NewClient(models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}, r.txnManager, r.stashboxRepository())
|
||||
user, err := client.GetUser(ctx)
|
||||
|
||||
valid := user != nil && user.Me != nil
|
||||
@@ -243,7 +256,7 @@ func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input m
|
||||
}
|
||||
}
|
||||
|
||||
result := models.StashBoxValidationResult{
|
||||
result := StashBoxValidationResult{
|
||||
Valid: valid,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package api
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/internal/dlna"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) DlnaStatus(ctx context.Context) (*models.DLNAStatus, error) {
|
||||
func (r *queryResolver) DlnaStatus(ctx context.Context) (*dlna.Status, error) {
|
||||
return manager.GetInstance().DLNAService.Status(), nil
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Gallery().Find(idInt)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Gallery.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -23,14 +23,14 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *models.FindGalleriesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
galleries, total, err := repo.Gallery().Query(galleryFilter, filter)
|
||||
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *FindGalleriesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
galleries, total, err := r.repository.Gallery.Query(ctx, galleryFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindGalleriesResultType{
|
||||
ret = &FindGalleriesResultType{
|
||||
Count: total,
|
||||
Galleries: galleries,
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
|
||||
var image *models.Image
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Image()
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
var err error
|
||||
|
||||
if id != nil {
|
||||
@@ -22,12 +22,20 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
|
||||
return err
|
||||
}
|
||||
|
||||
image, err = qb.Find(idInt)
|
||||
image, err = qb.Find(ctx, idInt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if checksum != nil {
|
||||
image, err = qb.FindByChecksum(*checksum)
|
||||
var images []*models.Image
|
||||
images, err = qb.FindByChecksum(ctx, *checksum)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(images) > 0 {
|
||||
image = images[0]
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -38,13 +46,13 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *models.FindImagesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Image()
|
||||
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *FindImagesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
|
||||
fields := graphql.CollectAllFields(ctx)
|
||||
|
||||
result, err := qb.Query(models.ImageQueryOptions{
|
||||
result, err := qb.Query(ctx, models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: stringslice.StrInclude(fields, "count"),
|
||||
@@ -57,12 +65,12 @@ func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.Imag
|
||||
return err
|
||||
}
|
||||
|
||||
images, err := result.Resolve()
|
||||
images, err := result.Resolve(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindImagesResultType{
|
||||
ret = &FindImagesResultType{
|
||||
Count: result.Count,
|
||||
Images: images,
|
||||
Megapixels: result.Megapixels,
|
||||
|
||||
@@ -13,8 +13,8 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().Find(idInt)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -23,14 +23,14 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *models.FindMoviesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
movies, total, err := repo.Movie().Query(movieFilter, filter)
|
||||
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *FindMoviesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
movies, total, err := r.repository.Movie.Query(ctx, movieFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindMoviesResultType{
|
||||
ret = &FindMoviesResultType{
|
||||
Count: total,
|
||||
Movies: movies,
|
||||
}
|
||||
@@ -44,8 +44,8 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Movie, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().All()
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -13,8 +13,8 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().Find(idInt)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Performer.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -23,14 +23,14 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *models.FindPerformersResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
performers, total, err := repo.Performer().Query(performerFilter, filter)
|
||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *FindPerformersResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
performers, total, err := r.repository.Performer.Query(ctx, performerFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindPerformersResultType{
|
||||
ret = &FindPerformersResultType{
|
||||
Count: total,
|
||||
Performers: performers,
|
||||
}
|
||||
@@ -43,8 +43,8 @@ func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *mod
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllPerformers(ctx context.Context) (ret []*models.Performer, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().All()
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Performer.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -2,13 +2,33 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindSavedFilters(ctx context.Context, mode models.FilterMode) (ret []*models.SavedFilter, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.SavedFilter().FindByMode(mode)
|
||||
func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *models.SavedFilter, 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.SavedFilter.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.FilterMode) (ret []*models.SavedFilter, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
if mode != nil {
|
||||
ret, err = r.repository.SavedFilter.FindByMode(ctx, *mode)
|
||||
} else {
|
||||
ret, err = r.repository.SavedFilter.All(ctx)
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -17,8 +37,8 @@ func (r *queryResolver) FindSavedFilters(ctx context.Context, mode models.Filter
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.SavedFilter().FindDefault(mode)
|
||||
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
|
||||
|
||||
@@ -12,20 +12,24 @@ import (
|
||||
|
||||
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
|
||||
var scene *models.Scene
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Scene()
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
var err error
|
||||
if id != nil {
|
||||
idInt, err := strconv.Atoi(*id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scene, err = qb.Find(idInt)
|
||||
scene, err = qb.Find(ctx, idInt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if checksum != nil {
|
||||
scene, err = qb.FindByChecksum(*checksum)
|
||||
var scenes []*models.Scene
|
||||
scenes, err = qb.FindByChecksum(ctx, *checksum)
|
||||
if len(scenes) > 0 {
|
||||
scene = scenes[0]
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -36,24 +40,29 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str
|
||||
return scene, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindSceneByHash(ctx context.Context, input models.SceneHashInput) (*models.Scene, error) {
|
||||
func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInput) (*models.Scene, error) {
|
||||
var scene *models.Scene
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Scene()
|
||||
var err error
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
if input.Checksum != nil {
|
||||
scene, err = qb.FindByChecksum(*input.Checksum)
|
||||
scenes, err := qb.FindByChecksum(ctx, *input.Checksum)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(scenes) > 0 {
|
||||
scene = scenes[0]
|
||||
}
|
||||
}
|
||||
|
||||
if scene == nil && input.Oshash != nil {
|
||||
scene, err = qb.FindByOSHash(*input.Oshash)
|
||||
scenes, err := qb.FindByOSHash(ctx, *input.Oshash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(scenes) > 0 {
|
||||
scene = scenes[0]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -64,8 +73,8 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input models.SceneH
|
||||
return scene, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var scenes []*models.Scene
|
||||
var err error
|
||||
|
||||
@@ -73,17 +82,26 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
|
||||
result := &models.SceneQueryResult{}
|
||||
|
||||
if len(sceneIDs) > 0 {
|
||||
scenes, err = repo.Scene().FindMany(sceneIDs)
|
||||
scenes, err = r.repository.Scene.FindMany(ctx, sceneIDs)
|
||||
if err == nil {
|
||||
result.Count = len(scenes)
|
||||
for _, s := range scenes {
|
||||
result.TotalDuration += s.Duration.Float64
|
||||
size, _ := strconv.ParseFloat(s.Size.String, 64)
|
||||
result.TotalSize += size
|
||||
if err = s.LoadPrimaryFile(ctx, r.repository.File); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
f := s.Files.Primary()
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result.TotalDuration += f.Duration
|
||||
|
||||
result.TotalSize += float64(f.Size)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result, err = repo.Scene().Query(models.SceneQueryOptions{
|
||||
result, err = r.repository.Scene.Query(ctx, models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: stringslice.StrInclude(fields, "count"),
|
||||
@@ -93,7 +111,7 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
|
||||
TotalSize: stringslice.StrInclude(fields, "filesize"),
|
||||
})
|
||||
if err == nil {
|
||||
scenes, err = result.Resolve()
|
||||
scenes, err = result.Resolve(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +119,7 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindScenesResultType{
|
||||
ret = &FindScenesResultType{
|
||||
Count: result.Count,
|
||||
Scenes: scenes,
|
||||
Duration: result.TotalDuration,
|
||||
@@ -116,8 +134,8 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
|
||||
sceneFilter := &models.SceneFilterType{}
|
||||
|
||||
@@ -138,7 +156,7 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
|
||||
|
||||
fields := graphql.CollectAllFields(ctx)
|
||||
|
||||
result, err := repo.Scene().Query(models.SceneQueryOptions{
|
||||
result, err := r.repository.Scene.Query(ctx, models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: queryFilter,
|
||||
Count: stringslice.StrInclude(fields, "count"),
|
||||
@@ -151,12 +169,12 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
|
||||
return err
|
||||
}
|
||||
|
||||
scenes, err := result.Resolve()
|
||||
scenes, err := result.Resolve(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindScenesResultType{
|
||||
ret = &FindScenesResultType{
|
||||
Count: result.Count,
|
||||
Scenes: scenes,
|
||||
Duration: result.TotalDuration,
|
||||
@@ -171,17 +189,23 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config models.SceneParserInput) (ret *models.SceneParserResultType, err error) {
|
||||
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config manager.SceneParserInput) (ret *SceneParserResultType, err error) {
|
||||
parser := manager.NewSceneFilenameParser(filter, config)
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
result, count, err := parser.Parse(repo)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
result, count, err := parser.Parse(ctx, manager.SceneFilenameParserRepository{
|
||||
Scene: r.repository.Scene,
|
||||
Performer: r.repository.Performer,
|
||||
Studio: r.repository.Studio,
|
||||
Movie: r.repository.Movie,
|
||||
Tag: r.repository.Tag,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.SceneParserResultType{
|
||||
ret = &SceneParserResultType{
|
||||
Count: count,
|
||||
Results: result,
|
||||
}
|
||||
@@ -199,8 +223,8 @@ func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int)
|
||||
if distance != nil {
|
||||
dist = *distance
|
||||
}
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Scene().FindDuplicates(dist)
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Scene.FindDuplicates(ctx, dist)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user