mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
260 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bd5f91e58 | ||
|
|
83cb51ec47 | ||
|
|
ca38a355d2 | ||
|
|
d2865b0796 | ||
|
|
3cf97f6e27 | ||
|
|
f0988817c8 | ||
|
|
692c1e55ac | ||
|
|
18b44e9381 | ||
|
|
7761ac19de | ||
|
|
deb5110623 | ||
|
|
9875a21674 | ||
|
|
901a7e59ec | ||
|
|
cb808c7be2 | ||
|
|
cb3545a303 | ||
|
|
6f2057a51e | ||
|
|
53f9530524 | ||
|
|
65d1353f2c | ||
|
|
1cba910435 | ||
|
|
ef2af977d3 | ||
|
|
d80ec1d7a1 | ||
|
|
7ccfa07843 | ||
|
|
32e8496314 | ||
|
|
c52d8c9314 | ||
|
|
71a751d997 | ||
|
|
f5ff1139b0 | ||
|
|
cf0ce6cb08 | ||
|
|
08560923d2 | ||
|
|
0e2bd125a8 | ||
|
|
b67abb89ff | ||
|
|
a36b895e4b | ||
|
|
7df26e2f56 | ||
|
|
515202d28e | ||
|
|
639a9da65b | ||
|
|
01d40c1b9e | ||
|
|
ef622659ff | ||
|
|
8bac413d74 | ||
|
|
9351a0b2a4 | ||
|
|
0b4b100ecc | ||
|
|
cc4b0f7b11 | ||
|
|
150c496949 | ||
|
|
74506bc5e8 | ||
|
|
05b0fb23f4 | ||
|
|
8629a0713d | ||
|
|
0aab2c382f | ||
|
|
74b585a05f | ||
|
|
7c58305b05 | ||
|
|
ce080c1b07 | ||
|
|
a67eee8f4c | ||
|
|
2715dcb72a | ||
|
|
eb7956a05a | ||
|
|
78bb2d8425 | ||
|
|
2064ea27b0 | ||
|
|
782bed9df8 | ||
|
|
c9b0841184 | ||
|
|
38d6af8b66 | ||
|
|
dc875ed5d7 | ||
|
|
b5b9023b3e | ||
|
|
87cea80e7b | ||
|
|
e614ca8d26 | ||
|
|
4daf0a14a2 | ||
|
|
d2395e579c | ||
|
|
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 |
36
.github/workflows/build.yml
vendored
36
.github/workflows/build.yml
vendored
@@ -2,7 +2,7 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop, master, files-refactor ]
|
||||
branches: [ develop, master ]
|
||||
pull_request:
|
||||
release:
|
||||
types: [ published ]
|
||||
@@ -12,7 +12,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:6
|
||||
COMPILER_IMAGE: stashapp/compiler:7
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -27,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:
|
||||
@@ -35,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
|
||||
@@ -44,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
|
||||
@@ -130,10 +130,6 @@ jobs:
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
run : git tag -f latest_develop; git push -f --tags
|
||||
|
||||
- name: Update files-refactor-release tag
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/files-refactor' }}
|
||||
run : git tag -f files-refactor-release; git push -f --tags
|
||||
|
||||
- name: Development Release
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
uses: marvinpinto/action-automatic-releases@v1.1.2
|
||||
@@ -155,7 +151,7 @@ jobs:
|
||||
- name: Master release
|
||||
# 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') }}
|
||||
if: ${{ github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
uses: WithoutPants/github-release@v2.0.4
|
||||
with:
|
||||
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -171,24 +167,6 @@ jobs:
|
||||
CHECKSUMS_SHA1
|
||||
gzip: false
|
||||
|
||||
- name: Files refactor Release
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/files-refactor' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
prerelease: true
|
||||
tag_name: files-refactor-release
|
||||
target_commitish: refs/heads/files-refactor
|
||||
files: |
|
||||
dist/stash-macos-intel
|
||||
dist/stash-macos-applesilicon
|
||||
dist/stash-win.exe
|
||||
dist/stash-linux
|
||||
dist/stash-linux-arm64v8
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-linux-arm32v6
|
||||
CHECKSUMS_SHA1
|
||||
|
||||
- name: Development Docker
|
||||
if: ${{ github.repository == 'stashapp/stash' && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
env:
|
||||
@@ -206,7 +184,7 @@ jobs:
|
||||
- name: Release Docker
|
||||
# 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') }}
|
||||
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,22 +1,19 @@
|
||||
# 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
|
||||
|
||||
38
Makefile
38
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,14 +47,21 @@ ifndef STASH_VERSION
|
||||
endif
|
||||
|
||||
ifndef OFFICIAL_BUILD
|
||||
$(eval OFFICIAL_BUILD := false)
|
||||
$(eval OFFICIAL_BUILD := false)
|
||||
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
|
||||
@@ -71,6 +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: GO_BUILD_TAGS := $(GO_BUILD_TAGS_WINDOWS)
|
||||
cross-compile-windows: build-release-static
|
||||
|
||||
cross-compile-macos-intel: export GOOS := darwin
|
||||
@@ -78,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
|
||||
|
||||
@@ -86,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
|
||||
|
||||
@@ -106,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
|
||||
@@ -124,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
|
||||
@@ -131,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
|
||||
@@ -162,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:
|
||||
|
||||
23
README.md
23
README.md
@@ -26,7 +26,7 @@ For further information you can [read the in-app manual](ui/v2.5/src/docs/en).
|
||||
|
||||
## First Run
|
||||
#### Windows Users: Security Prompt
|
||||
Running the app might present a security prompt since the binary isn't yet signed. Bypass this by clicking "more info" and then the "run anyway" button.
|
||||
Running the app might present a security prompt since the binary isn't yet signed. Bypass this by clicking "more info" and then the "run anyway" button.
|
||||
#### FFMPEG
|
||||
Stash requires ffmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
|
||||
|
||||
@@ -39,32 +39,31 @@ On first run, Stash will prompt you for some configuration options and media dir
|
||||
|
||||
Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en/Scraping.md), which integrate directly into Stash.
|
||||
|
||||
Many community-maintained scrapers are available for download at the [Community Scrapers Collection](https://github.com/stashapp/CommunityScrapers). The community also maintains StashDB, a crowd-sourced repository of scene, studio, and performer information, that can automatically identify much of a typical media collection. Inquire in the Discord for details. Identifying an entire collection will typically require a mix of multiple sources.
|
||||
Many community-maintained scrapers are available for download from [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers). The community also maintains StashDB, a crowd-sourced repository of scene, studio, and performer information, that can automatically identify much of a typical media collection. Inquire in the Discord for details. Identifying an entire collection will typically require a mix of multiple sources.
|
||||
|
||||
<sub>StashDB is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box).</sub>
|
||||
<sub>[StashDB](http://stashdb.org) is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box).</sub>
|
||||
|
||||
# Translation
|
||||
[](https://translate.stashapp.cc/engage/stash/)
|
||||
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇪🇸 🇸🇪 🇹🇼 🇹🇷
|
||||
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇪🇪 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇷🇺 🇪🇸 🇸🇪 🇹🇼 🇹🇷
|
||||
|
||||
Stash is available in 16 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 25 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)
|
||||
|
||||
Answers to other Frequently Asked Questions can be found [on our Wiki](https://github.com/stashapp/stash/wiki/FAQ)
|
||||
Check out our documentation on [Stash-Docs](https://docs.stashapp.cc) for information about the software, questions, guides, add-ons and more.
|
||||
|
||||
For issues not addressed there, there are a few options.
|
||||
|
||||
* Read the [Wiki](https://github.com/stashapp/stash/wiki)
|
||||
* Check the in-app documentation, in the top right corner of the app (also available [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en)
|
||||
For more help you can:
|
||||
* Check the in-app documentation, in the top right corner of the app (it's also mirrored on [Stash-Docs](https://docs.stashapp.cc/in-app-manual))
|
||||
* Join the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support.
|
||||
* Start a [discussion on GitHub](https://github.com/stashapp/stash/discussions)
|
||||
|
||||
# Customization
|
||||
|
||||
## Themes and CSS Customization
|
||||
There is a [directory of community-created themes](https://github.com/stashapp/stash/wiki/Themes) on our Wiki, along with instructions on how to install them.
|
||||
There is a [directory of community-created themes](https://docs.stashapp.cc/user-interface-ui/themes) on Stash-Docs, along with instructions on how to install them.
|
||||
|
||||
You can also make Stash interface fit your desired style with [Custom CSS snippets](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets).
|
||||
You can also change the Stash interface to fit your desired style with various snippets from [Custom CSS snippets](https://docs.stashapp.cc/user-interface-ui/custom-css-snippets).
|
||||
|
||||
# For Developers
|
||||
|
||||
|
||||
@@ -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,11 @@ 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 --virtual .build-deps gcc python3-dev musl-dev \
|
||||
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg vips-tools ruby \
|
||||
&& pip install mechanicalsoup cloudscraper bencoder.pyx \
|
||||
&& gem install faraday \
|
||||
&& apk del .build-deps
|
||||
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 .
|
||||
|
||||
@@ -43,9 +43,10 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW. For examp
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
16
go.mod
16
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,17 +36,18 @@ 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/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
|
||||
@@ -55,7 +56,9 @@ require (
|
||||
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 (
|
||||
@@ -98,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
|
||||
|
||||
35
go.sum
35
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=
|
||||
@@ -206,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=
|
||||
@@ -248,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=
|
||||
@@ -535,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=
|
||||
@@ -570,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=
|
||||
@@ -693,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=
|
||||
@@ -746,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=
|
||||
@@ -856,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=
|
||||
@@ -913,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=
|
||||
@@ -1037,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=
|
||||
@@ -1071,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=
|
||||
@@ -1128,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=
|
||||
@@ -1300,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=
|
||||
|
||||
134
gqlgen.yml
134
gqlgen.yml
@@ -4,46 +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
|
||||
SceneCaption:
|
||||
model: github.com/stashapp/stash/pkg/models.SceneCaption
|
||||
|
||||
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
|
||||
@@ -45,6 +46,10 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
api_key
|
||||
}
|
||||
pythonPath
|
||||
transcodeInputArgs
|
||||
transcodeOutputArgs
|
||||
liveTranscodeInputArgs
|
||||
liveTranscodeOutputArgs
|
||||
}
|
||||
|
||||
fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
@@ -62,6 +67,10 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
showStudioAsText
|
||||
css
|
||||
cssEnabled
|
||||
javascript
|
||||
javascriptEnabled
|
||||
customLocales
|
||||
customLocalesEnabled
|
||||
language
|
||||
imageLightbox {
|
||||
slideshowDelay
|
||||
|
||||
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,14 @@
|
||||
fragment SlimImageData on Image {
|
||||
id
|
||||
checksum
|
||||
title
|
||||
rating
|
||||
date
|
||||
url
|
||||
rating100
|
||||
organized
|
||||
o_counter
|
||||
path
|
||||
|
||||
file {
|
||||
size
|
||||
width
|
||||
height
|
||||
files {
|
||||
...ImageFileData
|
||||
}
|
||||
|
||||
paths {
|
||||
@@ -20,8 +18,13 @@ fragment SlimImageData on Image {
|
||||
|
||||
galleries {
|
||||
id
|
||||
path
|
||||
title
|
||||
files {
|
||||
path
|
||||
}
|
||||
folder {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
studio {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
fragment ImageData on Image {
|
||||
id
|
||||
checksum
|
||||
title
|
||||
rating
|
||||
rating100
|
||||
date
|
||||
url
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
fragment SlimPerformerData on Performer {
|
||||
id
|
||||
name
|
||||
disambiguation
|
||||
gender
|
||||
url
|
||||
twitter
|
||||
@@ -13,11 +14,12 @@ fragment SlimPerformerData on Performer {
|
||||
ethnicity
|
||||
hair_color
|
||||
eye_color
|
||||
height
|
||||
height_cm
|
||||
fake_tits
|
||||
career_length
|
||||
tattoos
|
||||
piercings
|
||||
alias_list
|
||||
tags {
|
||||
id
|
||||
name
|
||||
@@ -26,7 +28,7 @@ fragment SlimPerformerData on Performer {
|
||||
endpoint
|
||||
stash_id
|
||||
}
|
||||
rating
|
||||
rating100
|
||||
death_date
|
||||
weight
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ fragment PerformerData on Performer {
|
||||
id
|
||||
checksum
|
||||
name
|
||||
disambiguation
|
||||
url
|
||||
gender
|
||||
twitter
|
||||
@@ -10,13 +11,13 @@ fragment PerformerData on Performer {
|
||||
ethnicity
|
||||
country
|
||||
eye_color
|
||||
height
|
||||
height_cm
|
||||
measurements
|
||||
fake_tits
|
||||
career_length
|
||||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
alias_list
|
||||
favorite
|
||||
ignore_auto_tag
|
||||
image_path
|
||||
@@ -33,7 +34,7 @@ fragment PerformerData on Performer {
|
||||
stash_id
|
||||
endpoint
|
||||
}
|
||||
rating
|
||||
rating100
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
|
||||
@@ -1,32 +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
|
||||
captions {
|
||||
language_code
|
||||
caption_type
|
||||
}
|
||||
resume_time
|
||||
play_duration
|
||||
play_count
|
||||
|
||||
file {
|
||||
size
|
||||
duration
|
||||
video_codec
|
||||
audio_codec
|
||||
width
|
||||
height
|
||||
framerate
|
||||
bitrate
|
||||
files {
|
||||
...VideoFileData
|
||||
}
|
||||
|
||||
paths {
|
||||
@@ -35,7 +25,6 @@ fragment SlimSceneData on Scene {
|
||||
stream
|
||||
webp
|
||||
vtt
|
||||
chapters_vtt
|
||||
sprite
|
||||
funscript
|
||||
interactive_heatmap
|
||||
@@ -46,11 +35,17 @@ fragment SlimSceneData on Scene {
|
||||
id
|
||||
title
|
||||
seconds
|
||||
primary_tag {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
galleries {
|
||||
id
|
||||
path
|
||||
files {
|
||||
path
|
||||
}
|
||||
title
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
fragment SceneData on Scene {
|
||||
id
|
||||
checksum
|
||||
oshash
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
url
|
||||
date
|
||||
rating
|
||||
rating100
|
||||
o_counter
|
||||
organized
|
||||
path
|
||||
phash
|
||||
interactive
|
||||
interactive_speed
|
||||
captions {
|
||||
@@ -19,16 +17,13 @@ fragment SceneData on Scene {
|
||||
}
|
||||
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 {
|
||||
@@ -37,7 +32,6 @@ fragment SceneData on Scene {
|
||||
stream
|
||||
webp
|
||||
vtt
|
||||
chapters_vtt
|
||||
sprite
|
||||
funscript
|
||||
interactive_heatmap
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
fragment ScrapedPerformerData on ScrapedPerformer {
|
||||
stored_id
|
||||
name
|
||||
disambiguation
|
||||
gender
|
||||
url
|
||||
twitter
|
||||
@@ -30,6 +31,7 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
||||
fragment ScrapedScenePerformerData on ScrapedPerformer {
|
||||
stored_id
|
||||
name
|
||||
disambiguation
|
||||
gender
|
||||
url
|
||||
twitter
|
||||
@@ -105,7 +107,9 @@ fragment ScrapedSceneTagData on ScrapedTag {
|
||||
|
||||
fragment ScrapedSceneData on ScrapedScene {
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
url
|
||||
date
|
||||
image
|
||||
@@ -166,7 +170,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
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ fragment StudioData on Studio {
|
||||
scene_count
|
||||
image_count
|
||||
gallery_count
|
||||
performer_count
|
||||
movie_count
|
||||
stash_ids {
|
||||
stash_id
|
||||
endpoint
|
||||
}
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
aliases
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
fragment TagData on Tag {
|
||||
id
|
||||
name
|
||||
description
|
||||
aliases
|
||||
ignore_auto_tag
|
||||
image_path
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -41,3 +41,7 @@ mutation MigrateHashNaming {
|
||||
mutation BackupDatabase($input: BackupDatabaseInput!) {
|
||||
backupDatabase(input: $input)
|
||||
}
|
||||
|
||||
mutation AnonymiseDatabase($input: AnonymiseDatabaseInput!) {
|
||||
anonymiseDatabase(input: $input)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,9 @@ query Version {
|
||||
|
||||
query LatestVersion {
|
||||
latestversion {
|
||||
version
|
||||
shorthash
|
||||
release_date
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -96,7 +96,7 @@ type Query {
|
||||
|
||||
"""Scrapes a complete performer record based on a URL"""
|
||||
scrapePerformerURL(url: String!): ScrapedPerformer
|
||||
"""Scrapes a complete performer record based on a URL"""
|
||||
"""Scrapes a complete scene record based on a URL"""
|
||||
scrapeSceneURL(url: String!): ScrapedScene
|
||||
"""Scrapes a complete gallery record based on a URL"""
|
||||
scrapeGalleryURL(url: String!): ScrapedGallery
|
||||
@@ -155,14 +155,16 @@ type Query {
|
||||
version: Version!
|
||||
|
||||
# LatestVersion
|
||||
latestversion: ShortVersion!
|
||||
latestversion: LatestVersion!
|
||||
}
|
||||
|
||||
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!
|
||||
@@ -175,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!
|
||||
|
||||
@@ -182,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!
|
||||
@@ -227,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!
|
||||
@@ -269,6 +281,9 @@ type Mutation {
|
||||
metadataIdentify(input: IdentifyMetadataInput!): ID!
|
||||
"""Migrate generated files for the current hash naming"""
|
||||
migrateHashNaming: ID!
|
||||
|
||||
"""Anonymise the database in a separate file. Optionally returns a link to download the database file"""
|
||||
anonymiseDatabase(input: AnonymiseDatabaseInput!): String
|
||||
|
||||
"""Reload scrapers"""
|
||||
reloadScrapers: Boolean!
|
||||
|
||||
@@ -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"""
|
||||
@@ -67,6 +69,21 @@ input ConfigGeneralInput {
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"""Max streaming transcode size"""
|
||||
maxStreamingTranscodeSize: StreamingResolutionEnum
|
||||
|
||||
"""ffmpeg transcode input args - injected before input file
|
||||
These are applied to generated transcodes (previews and transcodes)"""
|
||||
transcodeInputArgs: [String!]
|
||||
"""ffmpeg transcode output args - injected before output file
|
||||
These are applied to generated transcodes (previews and transcodes)"""
|
||||
transcodeOutputArgs: [String!]
|
||||
|
||||
"""ffmpeg stream input args - injected before input file
|
||||
These are applied when live transcoding"""
|
||||
liveTranscodeInputArgs: [String!]
|
||||
"""ffmpeg stream output args - injected before output file
|
||||
These are applied when live transcoding"""
|
||||
liveTranscodeOutputArgs: [String!]
|
||||
|
||||
"""Write image thumbnails to disk when generating on the fly"""
|
||||
writeImageThumbnails: Boolean
|
||||
"""Username"""
|
||||
@@ -116,6 +133,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"""
|
||||
@@ -148,6 +167,21 @@ type ConfigGeneralResult {
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"""Max streaming transcode size"""
|
||||
maxStreamingTranscodeSize: StreamingResolutionEnum
|
||||
|
||||
"""ffmpeg transcode input args - injected before input file
|
||||
These are applied to generated transcodes (previews and transcodes)"""
|
||||
transcodeInputArgs: [String!]!
|
||||
"""ffmpeg transcode output args - injected before output file
|
||||
These are applied to generated transcodes (previews and transcodes)"""
|
||||
transcodeOutputArgs: [String!]!
|
||||
|
||||
"""ffmpeg stream input args - injected before input file
|
||||
These are applied when live transcoding"""
|
||||
liveTranscodeInputArgs: [String!]!
|
||||
"""ffmpeg stream output args - injected before output file
|
||||
These are applied when live transcoding"""
|
||||
liveTranscodeOutputArgs: [String!]!
|
||||
|
||||
"""Write image thumbnails to disk when generating on the fly"""
|
||||
writeImageThumbnails: Boolean!
|
||||
"""API Key"""
|
||||
@@ -259,6 +293,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
|
||||
@@ -322,6 +364,14 @@ type ConfigInterfaceResult {
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
|
||||
"""Custom Javascript"""
|
||||
javascript: String
|
||||
javascriptEnabled: Boolean
|
||||
|
||||
"""Custom Locales"""
|
||||
customLocales: String
|
||||
customLocalesEnabled: Boolean
|
||||
|
||||
"""Interface language"""
|
||||
language: String
|
||||
|
||||
|
||||
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,12 +39,21 @@ 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
|
||||
NOT: PerformerFilterType
|
||||
|
||||
name: StringCriterionInput
|
||||
disambiguation: StringCriterionInput
|
||||
details: StringCriterionInput
|
||||
|
||||
"""Filter by favorite"""
|
||||
@@ -60,7 +69,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 +99,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 +118,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 +137,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 +154,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 +168,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,7 +207,9 @@ 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"""
|
||||
@@ -176,6 +218,18 @@ input SceneFilterType {
|
||||
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 {
|
||||
@@ -187,7 +241,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"""
|
||||
@@ -196,6 +252,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 {
|
||||
@@ -208,11 +270,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"""
|
||||
@@ -225,6 +291,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 {
|
||||
@@ -232,6 +302,7 @@ input GalleryFilterType {
|
||||
OR: GalleryFilterType
|
||||
NOT: GalleryFilterType
|
||||
|
||||
id: IntCriterionInput
|
||||
title: StringCriterionInput
|
||||
details: StringCriterionInput
|
||||
|
||||
@@ -239,12 +310,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"""
|
||||
@@ -269,6 +344,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 {
|
||||
@@ -282,6 +363,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
|
||||
|
||||
@@ -314,6 +398,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 {
|
||||
@@ -323,12 +413,22 @@ 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 date"""
|
||||
date: DateCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
"""Filter by organized"""
|
||||
organized: Boolean
|
||||
"""Filter by o-counter"""
|
||||
@@ -353,6 +453,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 {
|
||||
@@ -409,6 +513,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,23 @@
|
||||
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
|
||||
url: String
|
||||
date: String
|
||||
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 +27,10 @@ type Image {
|
||||
}
|
||||
|
||||
type ImageFileType {
|
||||
size: Int
|
||||
width: Int
|
||||
height: Int
|
||||
mod_time: Time!
|
||||
size: Int!
|
||||
width: Int!
|
||||
height: Int!
|
||||
}
|
||||
|
||||
type ImagePathsType {
|
||||
@@ -34,21 +42,33 @@ 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
|
||||
url: String
|
||||
date: String
|
||||
|
||||
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
|
||||
url: String
|
||||
date: String
|
||||
|
||||
studio_id: ID
|
||||
performer_ids: BulkUpdateIds
|
||||
|
||||
@@ -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"""
|
||||
@@ -254,6 +263,10 @@ input BackupDatabaseInput {
|
||||
download: Boolean
|
||||
}
|
||||
|
||||
input AnonymiseDatabaseInput {
|
||||
download: Boolean
|
||||
}
|
||||
|
||||
enum SystemStatusEnum {
|
||||
SETUP
|
||||
NEEDS_MIGRATION
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ enum GenderEnum {
|
||||
|
||||
type Performer {
|
||||
id: ID!
|
||||
checksum: String!
|
||||
name: String
|
||||
checksum: String @deprecated(reason: "Not used")
|
||||
name: String!
|
||||
disambiguation: String
|
||||
url: String
|
||||
gender: GenderEnum
|
||||
twitter: String
|
||||
@@ -19,13 +20,15 @@ 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
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: [String!]!
|
||||
favorite: Boolean!
|
||||
tags: [Tag!]!
|
||||
ignore_auto_tag: Boolean!
|
||||
@@ -36,7 +39,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
|
||||
@@ -49,19 +55,23 @@ type Performer {
|
||||
|
||||
input PerformerCreateInput {
|
||||
name: String!
|
||||
disambiguation: String
|
||||
url: String
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
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
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: [String!]
|
||||
twitter: String
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
@@ -69,7 +79,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
|
||||
@@ -80,19 +93,23 @@ input PerformerCreateInput {
|
||||
input PerformerUpdateInput {
|
||||
id: ID!
|
||||
name: String
|
||||
disambiguation: String
|
||||
url: String
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
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
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: [String!]
|
||||
twitter: String
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
@@ -100,7 +117,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
|
||||
@@ -108,27 +128,39 @@ input PerformerUpdateInput {
|
||||
ignore_auto_tag: Boolean
|
||||
}
|
||||
|
||||
input BulkUpdateStrings {
|
||||
values: [String!]
|
||||
mode: BulkUpdateIdMode!
|
||||
}
|
||||
|
||||
input BulkPerformerUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
disambiguation: String
|
||||
url: String
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
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
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: BulkUpdateStrings
|
||||
twitter: String
|
||||
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
|
||||
|
||||
@@ -9,4 +9,6 @@ scalar Timestamp
|
||||
# generic JSON object
|
||||
scalar Map
|
||||
|
||||
scalar Any
|
||||
scalar Any
|
||||
|
||||
scalar Int64
|
||||
@@ -15,7 +15,7 @@ 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
|
||||
@@ -27,32 +27,46 @@ type SceneMovie {
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
type SceneCaption {
|
||||
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: [SceneCaption!]
|
||||
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!]!
|
||||
@@ -72,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!]
|
||||
@@ -89,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 {
|
||||
@@ -106,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
|
||||
@@ -154,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!]
|
||||
@@ -180,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
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ type ScrapedPerformer {
|
||||
"""Set if performer matched"""
|
||||
stored_id: ID
|
||||
name: String
|
||||
disambiguation: String
|
||||
gender: String
|
||||
url: String
|
||||
twitter: String
|
||||
@@ -17,6 +18,7 @@ type ScrapedPerformer {
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
# aliases must be comma-delimited to be parsed correctly
|
||||
aliases: String
|
||||
tags: [ScrapedTag!]
|
||||
|
||||
@@ -34,6 +36,7 @@ input ScrapedPerformerInput {
|
||||
"""Set if performer matched"""
|
||||
stored_id: ID
|
||||
name: String
|
||||
disambiguation: String
|
||||
gender: String
|
||||
url: String
|
||||
twitter: String
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -12,8 +12,12 @@ type Studio {
|
||||
scene_count: Int # Resolver
|
||||
image_count: Int # Resolver
|
||||
gallery_count: Int # Resolver
|
||||
performer_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 +32,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 +49,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
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ type Version {
|
||||
build_time: String!
|
||||
}
|
||||
|
||||
type ShortVersion {
|
||||
type LatestVersion {
|
||||
version: String!
|
||||
shorthash: String!
|
||||
release_date: String!
|
||||
url: String!
|
||||
}
|
||||
|
||||
@@ -94,7 +94,9 @@ fragment FingerprintFragment on Fingerprint {
|
||||
fragment SceneFragment on Scene {
|
||||
id
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
duration
|
||||
date
|
||||
urls {
|
||||
|
||||
@@ -17,12 +17,12 @@ const loginEndPoint = "/login"
|
||||
|
||||
const (
|
||||
tripwireActivatedErrMsg = "Stash is exposed to the public internet without authentication, and is not serving any more content to protect your privacy. " +
|
||||
"More information and fixes are available at https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet"
|
||||
"More information and fixes are available at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
|
||||
|
||||
externalAccessErrMsg = "You have attempted to access Stash over the internet, and authentication is not enabled. " +
|
||||
"This is extremely dangerous! The whole world can see your your stash page and browse your files! " +
|
||||
"Stash is not answering any other requests to protect your privacy. " +
|
||||
"Please read the log entry or visit https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet"
|
||||
"Please read the log entry or visit https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
|
||||
)
|
||||
|
||||
func allowUnauthenticated(r *http.Request) bool {
|
||||
@@ -85,12 +85,16 @@ func authenticateHandler() func(http.Handler) http.Handler {
|
||||
prefix := getProxyPrefix(r.Header)
|
||||
|
||||
// otherwise redirect to the login page
|
||||
u := url.URL{
|
||||
Path: prefix + "/login",
|
||||
returnURL := url.URL{
|
||||
Path: prefix + r.URL.Path,
|
||||
RawQuery: r.URL.RawQuery,
|
||||
}
|
||||
q := make(url.Values)
|
||||
q.Set(returnURLParam, returnURL.String())
|
||||
u := url.URL{
|
||||
Path: prefix + "/login",
|
||||
RawQuery: q.Encode(),
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set(returnURLParam, prefix+r.URL.Path)
|
||||
u.RawQuery = q.Encode()
|
||||
http.Redirect(w, r, u.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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 || *value == "" {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -20,12 +20,7 @@ import (
|
||||
const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases"
|
||||
const apiTags string = "https://api.github.com/repos/stashapp/stash/tags"
|
||||
const apiAcceptHeader string = "application/vnd.github.v3+json"
|
||||
const developmentTag string = "latest_develop"
|
||||
const defaultSHLength int = 7 // default length of SHA short hash returned by <git rev-parse --short HEAD>
|
||||
|
||||
// ErrNoVersion indicates that no version information has been embedded in the
|
||||
// stash binary
|
||||
var ErrNoVersion = errors.New("no stash version")
|
||||
const defaultSHLength int = 8 // default length of SHA short hash returned by <git rev-parse --short HEAD>
|
||||
|
||||
var stashReleases = func() map[string]string {
|
||||
return map[string]string{
|
||||
@@ -108,9 +103,21 @@ type githubTagResponse struct {
|
||||
Node_id string
|
||||
}
|
||||
|
||||
type LatestRelease struct {
|
||||
Version string
|
||||
Hash string
|
||||
ShortHash string
|
||||
Date string
|
||||
Url string
|
||||
}
|
||||
|
||||
func makeGithubRequest(ctx context.Context, url string, output interface{}) error {
|
||||
|
||||
transport := &http.Transport{Proxy: http.ProxyFromEnvironment}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
Timeout: 3 * time.Second,
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
@@ -144,14 +151,16 @@ func makeGithubRequest(ctx context.Context, url string, output interface{}) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLatestVersion gets latest version (git commit hash) from github API
|
||||
// GetLatestRelease gets latest release information from github API
|
||||
// If running a build from the "master" branch, then the latest full release
|
||||
// is used, otherwise it uses the release that is tagged with "latest_develop"
|
||||
// which is the latest pre-release build.
|
||||
func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string, latestRelease string, err error) {
|
||||
func GetLatestRelease(ctx context.Context) (*LatestRelease, error) {
|
||||
arch := runtime.GOARCH
|
||||
|
||||
arch := runtime.GOARCH // https://en.wikipedia.org/wiki/Comparison_of_ARM_cores
|
||||
isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4 // armv6 doesn't support any of these features
|
||||
// https://en.wikipedia.org/wiki/Comparison_of_ARM_cores
|
||||
// armv6 doesn't support any of these features
|
||||
isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4
|
||||
if arch == "arm" && isARMv7 {
|
||||
arch = "armv7"
|
||||
}
|
||||
@@ -159,125 +168,100 @@ func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string
|
||||
platform := fmt.Sprintf("%s/%s", runtime.GOOS, arch)
|
||||
wantedRelease := stashReleases()[platform]
|
||||
|
||||
version, _, _ := GetVersion()
|
||||
if version == "" {
|
||||
return "", "", ErrNoVersion
|
||||
}
|
||||
|
||||
// if the version is suffixed with -x-xxxx, then we are running a development build
|
||||
usePreRelease := false
|
||||
re := regexp.MustCompile(`-\d+-g\w+$`)
|
||||
if re.MatchString(version) {
|
||||
usePreRelease = true
|
||||
}
|
||||
|
||||
url := apiReleases
|
||||
if !usePreRelease {
|
||||
// just get the latest full release
|
||||
url += "/latest"
|
||||
var release githubReleasesResponse
|
||||
if IsDevelop() {
|
||||
// get the latest release, prerelease or not
|
||||
releases := []githubReleasesResponse{}
|
||||
err := makeGithubRequest(ctx, apiReleases+"?per_page=1", &releases)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
release = releases[0]
|
||||
} else {
|
||||
// get the release tagged with the development tag
|
||||
url += "/tags/" + developmentTag
|
||||
// just get the latest full release
|
||||
err := makeGithubRequest(ctx, apiReleases+"/latest", &release)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
release := githubReleasesResponse{}
|
||||
err = makeGithubRequest(ctx, url, &release)
|
||||
version := release.Name
|
||||
if release.Prerelease {
|
||||
// find version in prerelease name
|
||||
re := regexp.MustCompile(`v[\w-\.]+-\d+-g[0-9a-f]+`)
|
||||
if match := re.FindString(version); match != "" {
|
||||
version = match
|
||||
}
|
||||
}
|
||||
|
||||
latestHash, err := getReleaseHash(ctx, release.Tag_name)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if release.Prerelease == usePreRelease {
|
||||
latestVersion = getReleaseHash(ctx, release, shortHash, usePreRelease)
|
||||
var releaseDate string
|
||||
if publishedAt, err := time.Parse(time.RFC3339, release.Published_at); err == nil {
|
||||
releaseDate = publishedAt.Format("2006-01-02")
|
||||
}
|
||||
|
||||
if wantedRelease != "" {
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == wantedRelease {
|
||||
latestRelease = asset.Browser_download_url
|
||||
break
|
||||
}
|
||||
var releaseUrl string
|
||||
if wantedRelease != "" {
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == wantedRelease {
|
||||
releaseUrl = asset.Browser_download_url
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if latestVersion == "" {
|
||||
return "", "", fmt.Errorf("no version found for \"%s\"", version)
|
||||
_, githash, _ := GetVersion()
|
||||
shLength := len(githash)
|
||||
if shLength == 0 {
|
||||
shLength = defaultSHLength
|
||||
}
|
||||
return latestVersion, latestRelease, nil
|
||||
|
||||
return &LatestRelease{
|
||||
Version: version,
|
||||
Hash: latestHash,
|
||||
ShortHash: latestHash[:shLength],
|
||||
Date: releaseDate,
|
||||
Url: releaseUrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getReleaseHash(ctx context.Context, release githubReleasesResponse, shortHash bool, usePreRelease bool) string {
|
||||
shaLength := len(release.Target_commitish)
|
||||
// the /latest API call doesn't return the hash in target_commitish
|
||||
// also add sanity check in case Target_commitish is not 40 characters
|
||||
if !usePreRelease || shaLength != 40 {
|
||||
return getShaFromTags(ctx, shortHash, release.Tag_name)
|
||||
}
|
||||
|
||||
if shortHash {
|
||||
last := defaultSHLength // default length of git short hash
|
||||
_, gitShort, _ := GetVersion() // retrieve it to check actual length
|
||||
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
|
||||
last = len(gitShort)
|
||||
}
|
||||
return release.Target_commitish[0:last]
|
||||
}
|
||||
|
||||
return release.Target_commitish
|
||||
}
|
||||
|
||||
func printLatestVersion(ctx context.Context) {
|
||||
_, githash, _ = GetVersion()
|
||||
latest, _, err := GetLatestVersion(ctx, true)
|
||||
if err != nil {
|
||||
logger.Errorf("Couldn't find latest version: %s", err)
|
||||
} else {
|
||||
if githash == latest {
|
||||
logger.Infof("Version: (%s) is already the latest released.", latest)
|
||||
} else {
|
||||
logger.Infof("New version: (%s) available.", latest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get sha from the github api tags endpoint
|
||||
// returns the sha1 hash/shorthash or "" if something's wrong
|
||||
func getShaFromTags(ctx context.Context, shortHash bool, name string) string {
|
||||
func getReleaseHash(ctx context.Context, tagName string) (string, error) {
|
||||
url := apiTags
|
||||
tags := []githubTagResponse{}
|
||||
err := makeGithubRequest(ctx, url, &tags)
|
||||
|
||||
if err != nil {
|
||||
// If the context is canceled, we don't want to log this as an error
|
||||
// in the path. The function here just gives up and returns "" if
|
||||
// something goes wrong. Hence, log the error at the info-level so
|
||||
// it's still present, but don't treat this as an error.
|
||||
if errors.Is(err, context.Canceled) {
|
||||
logger.Infof("aborting sha request due to context cancellation")
|
||||
} else {
|
||||
logger.Errorf("Github Tags Api: %v", err)
|
||||
}
|
||||
return ""
|
||||
return "", err
|
||||
}
|
||||
_, gitShort, _ := GetVersion() // retrieve short hash to check actual length
|
||||
|
||||
for _, tag := range tags {
|
||||
if tag.Name == name {
|
||||
shaLength := len(tag.Commit.Sha)
|
||||
if shaLength != 40 {
|
||||
return ""
|
||||
if tag.Name == tagName {
|
||||
if len(tag.Commit.Sha) != 40 {
|
||||
return "", errors.New("invalid Github API response")
|
||||
}
|
||||
if shortHash {
|
||||
last := defaultSHLength // default length of git short hash
|
||||
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
|
||||
last = len(gitShort)
|
||||
}
|
||||
return tag.Commit.Sha[0:last]
|
||||
}
|
||||
|
||||
return tag.Commit.Sha
|
||||
return tag.Commit.Sha, nil
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return "", errors.New("invalid Github API response")
|
||||
}
|
||||
|
||||
func printLatestVersion(ctx context.Context) {
|
||||
latestRelease, err := GetLatestRelease(ctx)
|
||||
if err != nil {
|
||||
logger.Errorf("Couldn't retrieve latest version: %v", err)
|
||||
} else {
|
||||
_, githash, _ = GetVersion()
|
||||
switch {
|
||||
case githash == "":
|
||||
logger.Infof("Latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
|
||||
case githash == latestRelease.ShortHash:
|
||||
logger.Infof("Version %s (%s) is already the latest released", latestRelease.Version, latestRelease.ShortHash)
|
||||
default:
|
||||
logger.Infof("New version available: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -26,6 +26,14 @@ var matcher = language.NewMatcher([]language.Tag{
|
||||
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,56 +174,59 @@ 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,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Latestversion returns the latest git shorthash commit.
|
||||
func (r *queryResolver) Latestversion(ctx context.Context) (*models.ShortVersion, error) {
|
||||
ver, url, err := GetLatestVersion(ctx, true)
|
||||
if err == nil {
|
||||
logger.Infof("Retrieved latest hash: %s", ver)
|
||||
} else {
|
||||
logger.Errorf("Error while retrieving latest hash: %s", err)
|
||||
func (r *queryResolver) Latestversion(ctx context.Context) (*LatestVersion, error) {
|
||||
latestRelease, err := GetLatestRelease(ctx)
|
||||
if err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
logger.Errorf("Error while retrieving latest version: %v", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
logger.Infof("Retrieved latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
|
||||
|
||||
return &models.ShortVersion{
|
||||
Shorthash: ver,
|
||||
URL: url,
|
||||
}, err
|
||||
return &LatestVersion{
|
||||
Version: latestRelease.Version,
|
||||
Shorthash: latestRelease.ShortHash,
|
||||
ReleaseDate: latestRelease.Date,
|
||||
URL: latestRelease.Url,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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 +245,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,200 @@ 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) 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
|
||||
}
|
||||
|
||||
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) 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 := image.GetTitle(obj)
|
||||
ret := obj.GetTitle()
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, 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) Date(ctx context.Context, obj *models.Image) (*string, error) {
|
||||
if obj.Date != nil {
|
||||
result := obj.Date.String()
|
||||
return &result, 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,
|
||||
}, nil
|
||||
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) Paths(ctx context.Context, obj *models.Image) (*models.ImagePathsType, error) {
|
||||
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,137 +2,64 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"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
|
||||
}
|
||||
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) Birthdate(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Birthdate.Valid {
|
||||
return &obj.Birthdate.String, 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
|
||||
}
|
||||
// Checksum is deprecated
|
||||
func (r *performerResolver) Checksum(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Aliases(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Aliases.Valid {
|
||||
return &obj.Aliases.String, nil
|
||||
if !obj.Aliases.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadAliases(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ret := strings.Join(obj.Aliases.List(), ", ")
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer) ([]string, error) {
|
||||
if !obj.Aliases.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadAliases(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return obj.Aliases.List(), 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) Favorite(ctx context.Context, obj *models.Performer) (bool, error) {
|
||||
if obj.Favorite.Valid {
|
||||
return obj.Favorite.Bool, 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 != nil {
|
||||
ret := obj.Birthdate.String()
|
||||
return &ret, nil
|
||||
}
|
||||
return false, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
@@ -142,20 +69,23 @@ 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)
|
||||
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.Performer)
|
||||
}); 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 *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 +96,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 +108,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 +119,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 +129,39 @@ 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)
|
||||
return err
|
||||
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadStashIDs(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), 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 +172,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,97 +2,187 @@ 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()
|
||||
@@ -101,7 +191,7 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.S
|
||||
captionBasePath := builder.GetCaptionURL()
|
||||
interactiveHeatmap := builder.GetInteractiveHeatmapURL()
|
||||
|
||||
return &models.ScenePathsType{
|
||||
return &ScenePathsType{
|
||||
Screenshot: &screenshotPath,
|
||||
Preview: &previewPath,
|
||||
Stream: &streamPath,
|
||||
@@ -116,8 +206,8 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.S
|
||||
}
|
||||
|
||||
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
|
||||
@@ -126,9 +216,17 @@ func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (re
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []*models.SceneCaption, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Scene().GetCaptions(obj.ID)
|
||||
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
|
||||
}
|
||||
if primaryFile == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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
|
||||
@@ -138,125 +236,166 @@ func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
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.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 *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()
|
||||
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
|
||||
|
||||
sceneMovies, err := qb.GetMovies(obj.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return obj.LoadMovies(ctx, qb)
|
||||
}); 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
|
||||
}
|
||||
|
||||
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,10 +4,12 @@ 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"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
)
|
||||
|
||||
func (r *studioResolver) Name(ctx context.Context, obj *models.Studio) (string, error) {
|
||||
@@ -29,9 +31,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 +48,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 +60,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 +72,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 +84,20 @@ 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
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) PerformerCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = performer.CountByStudioID(ctx, r.repository.Performer, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -97,19 +111,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 +125,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 +170,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 +182,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
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
@@ -15,21 +16,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 +59,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 +85,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 +142,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
|
||||
}
|
||||
|
||||
@@ -216,10 +228,22 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
}
|
||||
|
||||
if input.Excludes != nil {
|
||||
for _, r := range input.Excludes {
|
||||
_, err := regexp.Compile(r)
|
||||
if err != nil {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("video exclusion pattern '%v' invalid: %w", r, err)
|
||||
}
|
||||
}
|
||||
c.Set(config.Exclude, input.Excludes)
|
||||
}
|
||||
|
||||
if input.ImageExcludes != nil {
|
||||
for _, r := range input.ImageExcludes {
|
||||
_, err := regexp.Compile(r)
|
||||
if err != nil {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("image/gallery exclusion pattern '%v' invalid: %w", r, err)
|
||||
}
|
||||
}
|
||||
c.Set(config.ImageExclude, input.ImageExcludes)
|
||||
}
|
||||
|
||||
@@ -269,6 +293,19 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
c.Set(config.PythonPath, input.PythonPath)
|
||||
}
|
||||
|
||||
if input.TranscodeInputArgs != nil {
|
||||
c.Set(config.TranscodeInputArgs, input.TranscodeInputArgs)
|
||||
}
|
||||
if input.TranscodeOutputArgs != nil {
|
||||
c.Set(config.TranscodeOutputArgs, input.TranscodeOutputArgs)
|
||||
}
|
||||
if input.LiveTranscodeInputArgs != nil {
|
||||
c.Set(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs)
|
||||
}
|
||||
if input.LiveTranscodeOutputArgs != nil {
|
||||
c.Set(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs)
|
||||
}
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
@@ -281,7 +318,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 +375,10 @@ 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)
|
||||
@@ -354,6 +391,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)
|
||||
@@ -376,7 +425,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 {
|
||||
@@ -413,7 +462,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
|
||||
@@ -428,6 +477,12 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input models.C
|
||||
}
|
||||
|
||||
if input.ExcludeTagPatterns != nil {
|
||||
for _, r := range input.ExcludeTagPatterns {
|
||||
_, err := regexp.Compile(r)
|
||||
if err != nil {
|
||||
return makeConfigScrapingResult(), fmt.Errorf("tag exclusion pattern '%v' invalid: %w", r, err)
|
||||
}
|
||||
}
|
||||
c.Set(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns)
|
||||
}
|
||||
|
||||
@@ -445,7 +500,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 {
|
||||
@@ -453,7 +508,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 {
|
||||
@@ -479,7 +534,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
|
||||
|
||||
@@ -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,173 @@ 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.URL = translator.optionalString(input.URL, "url")
|
||||
updatedImage.Date = translator.optionalDate(input.Date, "date")
|
||||
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.URL = translator.optionalString(input.URL, "url")
|
||||
updatedImage.Date = translator.optionalDate(input.Date, "date")
|
||||
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 +275,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 +283,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 +296,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 +323,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 +341,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 +373,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 +391,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 +409,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,7 +55,7 @@ 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
|
||||
@@ -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 {
|
||||
@@ -150,3 +156,55 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.Back
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input AnonymiseDatabaseInput) (*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 outPath string
|
||||
if download {
|
||||
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
|
||||
}
|
||||
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "anonymous*.sqlite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outPath = f.Name()
|
||||
f.Close()
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
outPath = database.AnonymousDatabasePath(backupDirectoryPath)
|
||||
}
|
||||
|
||||
err := database.Anonymise(outPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Error anonymising database: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if download {
|
||||
downloadHash, err := mgr.DownloadStore.RegisterFile(outPath, "", false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error registering file for download: %w", err)
|
||||
}
|
||||
logger.Debugf("Generated anonymised file %s with hash %s", outPath, downloadHash)
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
fn := filepath.Base(database.DatabaseBackupPath(""))
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
||||
return &ret, nil
|
||||
} else {
|
||||
logger.Infof("Successfully anonymised database to: %s", outPath)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -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,12 +2,10 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/hash/md5"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
@@ -16,8 +14,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,10 +24,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) {
|
||||
// generate checksum from performer name rather than image
|
||||
checksum := md5.FromString(input.Name)
|
||||
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) {
|
||||
var imageData []byte
|
||||
var err error
|
||||
|
||||
@@ -41,81 +46,99 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
// Populate a new performer from the input
|
||||
currentTime := time.Now()
|
||||
newPerformer := models.Performer{
|
||||
Checksum: checksum,
|
||||
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
Name: input.Name,
|
||||
TagIDs: models.NewRelatedIDs(tagIDs),
|
||||
StashIDs: models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)),
|
||||
CreatedAt: currentTime,
|
||||
UpdatedAt: currentTime,
|
||||
}
|
||||
if input.Disambiguation != nil {
|
||||
newPerformer.Disambiguation = *input.Disambiguation
|
||||
}
|
||||
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}
|
||||
if input.AliasList != nil {
|
||||
newPerformer.Aliases = models.NewRelatedStrings(input.AliasList)
|
||||
} else if input.Aliases != nil {
|
||||
newPerformer.Aliases = models.NewRelatedStrings(stringslice.FromString(*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 +151,17 @@ 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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(performer.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 {
|
||||
if err := qb.UpdateImage(ctx, newPerformer.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -163,17 +171,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),
|
||||
@@ -189,58 +194,86 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
}
|
||||
}
|
||||
|
||||
if input.Name != nil {
|
||||
// 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.URL = translator.nullString(input.URL, "url")
|
||||
updatedPerformer.Name = translator.optionalString(input.Name, "name")
|
||||
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
|
||||
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.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("alias_list") {
|
||||
updatedPerformer.Aliases = &models.UpdateStrings{
|
||||
Values: input.AliasList,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
} else if translator.hasField("aliases") {
|
||||
updatedPerformer.Aliases = &models.UpdateStrings{
|
||||
Values: stringslice.FromString(*input.Aliases, ","),
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if translator.hasField("stash_ids") {
|
||||
updatedPerformer.StashIDs = &models.UpdateStashIDs{
|
||||
StashIDs: stashIDPtrSliceToSlice(input.StashIds),
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +282,19 @@ 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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if translator.hasField("stash_ids") {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(performerID, stashIDJoins); err != nil {
|
||||
if err := qb.DestroyImage(ctx, performerID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -286,76 +304,92 @@ 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 {
|
||||
ids, err := stringslice.StringSliceToIntSlice(tagsIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateTags(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.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
|
||||
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.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("alias_list") {
|
||||
updatedPerformer.Aliases = &models.UpdateStrings{
|
||||
Values: input.AliasList.Values,
|
||||
Mode: input.AliasList.Mode,
|
||||
}
|
||||
} else if translator.hasField("aliases") {
|
||||
updatedPerformer.Aliases = &models.UpdateStrings{
|
||||
Values: stringslice.FromString(*input.Aliases, ","),
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
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,24 +402,12 @@ 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
|
||||
}
|
||||
|
||||
ret = append(ret, performer)
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
tagIDs, err := adjustTagIDs(qb, performerID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateTags(performerID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -409,14 +431,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 +454,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,
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,8 +59,8 @@ func getParent(path string) *string {
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigResult() *models.ConfigResult {
|
||||
return &models.ConfigResult{
|
||||
func makeConfigResult() *ConfigResult {
|
||||
return &ConfigResult{
|
||||
General: makeConfigGeneralResult(),
|
||||
Interface: makeConfigInterfaceResult(),
|
||||
Dlna: makeConfigDLNAResult(),
|
||||
@@ -70,7 +70,7 @@ func makeConfigResult() *models.ConfigResult {
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
func makeConfigGeneralResult() *ConfigGeneralResult {
|
||||
config := config.GetInstance()
|
||||
logFile := config.GetLogFile()
|
||||
|
||||
@@ -82,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(),
|
||||
@@ -122,10 +123,14 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
ScraperCDPPath: &scraperCDPPath,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
PythonPath: config.GetPythonPath(),
|
||||
TranscodeInputArgs: config.GetTranscodeInputArgs(),
|
||||
TranscodeOutputArgs: config.GetTranscodeOutputArgs(),
|
||||
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
|
||||
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
func makeConfigInterfaceResult() *ConfigInterfaceResult {
|
||||
config := config.GetInstance()
|
||||
menuItems := config.GetMenuItems()
|
||||
soundOnPreview := config.GetSoundOnPreview()
|
||||
@@ -141,15 +146,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,
|
||||
@@ -164,6 +172,10 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
ContinuePlaylistDefault: &continuePlaylistDefault,
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
Javascript: &javascript,
|
||||
JavascriptEnabled: &javascriptEnabled,
|
||||
CustomLocales: &customLocales,
|
||||
CustomLocalesEnabled: &customLocalesEnabled,
|
||||
Language: &language,
|
||||
|
||||
ImageLightbox: &imageLightboxOptions,
|
||||
@@ -177,10 +189,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(),
|
||||
@@ -188,13 +200,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,
|
||||
@@ -202,12 +214,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(),
|
||||
@@ -221,8 +233,8 @@ func makeConfigUIResult() map[string]interface{} {
|
||||
return config.GetInstance().GetUIConfiguration()
|
||||
}
|
||||
|
||||
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 (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
|
||||
@@ -248,7 +260,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
|
||||
|
||||
@@ -13,8 +13,8 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.SavedFilter().Find(idInt)
|
||||
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
|
||||
@@ -23,11 +23,11 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
if mode != nil {
|
||||
ret, err = repo.SavedFilter().FindByMode(*mode)
|
||||
ret, err = r.repository.SavedFilter.FindByMode(ctx, *mode)
|
||||
} else {
|
||||
ret, err = repo.SavedFilter().All()
|
||||
ret, err = r.repository.SavedFilter.All(ctx)
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -37,8 +37,8 @@ func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.Filte
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) {
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user