mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9fa7d071e | ||
|
|
dd5cff2aec | ||
|
|
c7c4d5b126 | ||
|
|
ccbe3c4e92 | ||
|
|
96ce260a40 | ||
|
|
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 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/golangci-lint.yml
vendored
2
.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:
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -23,6 +23,12 @@ 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
|
||||
34
Makefile
34
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 sqlite_stat4 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
|
||||
|
||||
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 ruby && pip install --no-cache-dir mechanicalsoup cloudscraper && gem install faraday
|
||||
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
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -108,4 +108,4 @@ require (
|
||||
|
||||
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
|
||||
|
||||
go 1.17
|
||||
go 1.19
|
||||
|
||||
6
go.sum
6
go.sum
@@ -700,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=
|
||||
@@ -773,7 +772,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
||||
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
@@ -958,7 +956,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1049,11 +1046,8 @@ 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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-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=
|
||||
|
||||
@@ -46,6 +46,10 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
api_key
|
||||
}
|
||||
pythonPath
|
||||
transcodeInputArgs
|
||||
transcodeOutputArgs
|
||||
liveTranscodeInputArgs
|
||||
liveTranscodeOutputArgs
|
||||
}
|
||||
|
||||
fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
@@ -63,6 +67,8 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
showStudioAsText
|
||||
css
|
||||
cssEnabled
|
||||
javascript
|
||||
javascriptEnabled
|
||||
customLocales
|
||||
customLocalesEnabled
|
||||
language
|
||||
|
||||
@@ -7,6 +7,7 @@ fragment VideoFileData on VideoFile {
|
||||
id
|
||||
path
|
||||
size
|
||||
mod_time
|
||||
duration
|
||||
video_codec
|
||||
audio_codec
|
||||
@@ -24,6 +25,7 @@ fragment ImageFileData on ImageFile {
|
||||
id
|
||||
path
|
||||
size
|
||||
mod_time
|
||||
width
|
||||
height
|
||||
fingerprints {
|
||||
@@ -36,6 +38,7 @@ fragment GalleryFileData on GalleryFile {
|
||||
id
|
||||
path
|
||||
size
|
||||
mod_time
|
||||
fingerprints {
|
||||
type
|
||||
value
|
||||
|
||||
@@ -4,7 +4,7 @@ fragment SlimGalleryData on Gallery {
|
||||
date
|
||||
url
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
organized
|
||||
files {
|
||||
...GalleryFileData
|
||||
|
||||
@@ -6,7 +6,7 @@ fragment GalleryData on Gallery {
|
||||
date
|
||||
url
|
||||
details
|
||||
rating
|
||||
rating100
|
||||
organized
|
||||
|
||||
files {
|
||||
@@ -16,9 +16,6 @@ fragment GalleryData on Gallery {
|
||||
...FolderData
|
||||
}
|
||||
|
||||
images {
|
||||
...SlimImageData
|
||||
}
|
||||
cover {
|
||||
...SlimImageData
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
fragment SlimImageData on Image {
|
||||
id
|
||||
title
|
||||
rating
|
||||
date
|
||||
url
|
||||
rating100
|
||||
organized
|
||||
o_counter
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
fragment ImageData on Image {
|
||||
id
|
||||
title
|
||||
rating
|
||||
rating100
|
||||
date
|
||||
url
|
||||
organized
|
||||
o_counter
|
||||
created_at
|
||||
|
||||
@@ -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,14 +1,19 @@
|
||||
fragment SlimSceneData on Scene {
|
||||
id
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
url
|
||||
date
|
||||
rating
|
||||
rating100
|
||||
o_counter
|
||||
organized
|
||||
interactive
|
||||
interactive_speed
|
||||
resume_time
|
||||
play_duration
|
||||
play_count
|
||||
|
||||
files {
|
||||
...VideoFileData
|
||||
@@ -20,7 +25,6 @@ fragment SlimSceneData on Scene {
|
||||
stream
|
||||
webp
|
||||
vtt
|
||||
chapters_vtt
|
||||
sprite
|
||||
funscript
|
||||
interactive_heatmap
|
||||
@@ -42,6 +46,9 @@ fragment SlimSceneData on Scene {
|
||||
files {
|
||||
path
|
||||
}
|
||||
folder {
|
||||
path
|
||||
}
|
||||
title
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
fragment SceneData on Scene {
|
||||
id
|
||||
title
|
||||
code
|
||||
details
|
||||
director
|
||||
url
|
||||
date
|
||||
rating
|
||||
rating100
|
||||
o_counter
|
||||
organized
|
||||
interactive
|
||||
@@ -15,6 +17,10 @@ fragment SceneData on Scene {
|
||||
}
|
||||
created_at
|
||||
updated_at
|
||||
resume_time
|
||||
last_played_at
|
||||
play_duration
|
||||
play_count
|
||||
|
||||
files {
|
||||
...VideoFileData
|
||||
@@ -26,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -6,26 +6,27 @@ query MarkerStrings($q: String, $sort: String) {
|
||||
}
|
||||
}
|
||||
|
||||
query AllTags {
|
||||
allTags {
|
||||
...TagData
|
||||
}
|
||||
}
|
||||
|
||||
query AllPerformersForFilter {
|
||||
allPerformers {
|
||||
...SlimPerformerData
|
||||
id
|
||||
name
|
||||
disambiguation
|
||||
alias_list
|
||||
}
|
||||
}
|
||||
|
||||
query AllStudiosForFilter {
|
||||
allStudios {
|
||||
...SlimStudioData
|
||||
id
|
||||
name
|
||||
aliases
|
||||
}
|
||||
}
|
||||
|
||||
query AllMoviesForFilter {
|
||||
allMovies {
|
||||
...SlimMovieData
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +68,9 @@ query Version {
|
||||
|
||||
query LatestVersion {
|
||||
latestversion {
|
||||
version
|
||||
shorthash
|
||||
release_date
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -144,6 +144,10 @@ type Query {
|
||||
|
||||
# Get everything
|
||||
|
||||
allScenes: [Scene!]!
|
||||
allSceneMarkers: [SceneMarker!]!
|
||||
allImages: [Image!]!
|
||||
allGalleries: [Gallery!]!
|
||||
allPerformers: [Performer!]!
|
||||
allStudios: [Studio!]!
|
||||
allMovies: [Movie!]!
|
||||
@@ -155,14 +159,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 +181,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 +194,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!
|
||||
@@ -271,6 +285,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!
|
||||
|
||||
@@ -69,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"""
|
||||
@@ -152,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"""
|
||||
@@ -264,6 +294,10 @@ input ConfigInterfaceInput {
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
|
||||
"""Custom Javascript"""
|
||||
javascript: String
|
||||
javascriptEnabled: Boolean
|
||||
|
||||
"""Custom Locales"""
|
||||
customLocales: String
|
||||
customLocalesEnabled: Boolean
|
||||
@@ -330,6 +364,10 @@ type ConfigInterfaceResult {
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
|
||||
"""Custom Javascript"""
|
||||
javascript: String
|
||||
javascriptEnabled: Boolean
|
||||
|
||||
"""Custom Locales"""
|
||||
customLocales: String
|
||||
customLocalesEnabled: Boolean
|
||||
|
||||
@@ -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
|
||||
@@ -135,7 +171,9 @@ input SceneFilterType {
|
||||
"""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"""
|
||||
@@ -169,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"""
|
||||
@@ -178,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 {
|
||||
@@ -189,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"""
|
||||
@@ -198,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 {
|
||||
@@ -210,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"""
|
||||
@@ -227,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 {
|
||||
@@ -234,6 +302,7 @@ input GalleryFilterType {
|
||||
OR: GalleryFilterType
|
||||
NOT: GalleryFilterType
|
||||
|
||||
id: IntCriterionInput
|
||||
title: StringCriterionInput
|
||||
details: StringCriterionInput
|
||||
|
||||
@@ -248,7 +317,9 @@ input GalleryFilterType {
|
||||
"""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"""
|
||||
@@ -273,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 {
|
||||
@@ -286,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
|
||||
|
||||
@@ -318,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 {
|
||||
@@ -327,6 +413,8 @@ input ImageFilterType {
|
||||
|
||||
title: StringCriterionInput
|
||||
|
||||
""" Filter by image id"""
|
||||
id: IntCriterionInput
|
||||
"""Filter by file checksum"""
|
||||
checksum: StringCriterionInput
|
||||
"""Filter by path"""
|
||||
@@ -334,7 +422,13 @@ input ImageFilterType {
|
||||
"""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"""
|
||||
@@ -359,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 {
|
||||
@@ -415,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,
|
||||
|
||||
@@ -7,7 +7,10 @@ type Gallery {
|
||||
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!
|
||||
@@ -23,7 +26,7 @@ type Gallery {
|
||||
performers: [Performer!]!
|
||||
|
||||
"""The images in the gallery"""
|
||||
images: [Image!]! # Resolver
|
||||
images: [Image!]! @deprecated(reason: "Use findImages")
|
||||
cover: Image
|
||||
}
|
||||
|
||||
@@ -32,7 +35,10 @@ input GalleryCreateInput {
|
||||
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
|
||||
@@ -47,7 +53,10 @@ 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
|
||||
@@ -63,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
|
||||
|
||||
@@ -2,7 +2,12 @@ type Image {
|
||||
id: ID!
|
||||
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! @deprecated(reason: "Use files.path")
|
||||
@@ -37,8 +42,13 @@ 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!]
|
||||
@@ -52,8 +62,13 @@ 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
|
||||
|
||||
@@ -263,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
|
||||
|
||||
@@ -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
|
||||
@@ -37,10 +37,15 @@ type Scene {
|
||||
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! @deprecated(reason: "Use files.path")
|
||||
@@ -51,6 +56,14 @@ type Scene {
|
||||
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! @deprecated(reason: "Use files")
|
||||
files: [VideoFile!]!
|
||||
@@ -73,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!]
|
||||
@@ -91,6 +107,42 @@ input SceneUpdateInput {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -109,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
|
||||
@@ -157,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!]
|
||||
@@ -183,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -19,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{} {
|
||||
@@ -90,6 +107,14 @@ 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{}
|
||||
@@ -118,7 +143,7 @@ func (t changesetTranslator) optionalDate(value *string, field string) models.Op
|
||||
return models.OptionalDate{}
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
if value == nil || *value == "" {
|
||||
return models.OptionalDate{
|
||||
Set: true,
|
||||
Null: true,
|
||||
@@ -128,6 +153,27 @@ func (t changesetTranslator) optionalDate(value *string, field string) models.Op
|
||||
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
|
||||
@@ -143,6 +189,56 @@ 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{}
|
||||
@@ -185,19 +281,12 @@ func (t changesetTranslator) optionalIntFromString(value *string, field string)
|
||||
return models.NewOptionalInt(vv), nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) nullBool(value *bool, field string) *sql.NullBool {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
func (t changesetTranslator) bool(value *bool, field string) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ret := &sql.NullBool{}
|
||||
|
||||
if value != nil {
|
||||
ret.Bool = *value
|
||||
ret.Valid = true
|
||||
}
|
||||
|
||||
return ret
|
||||
return *value
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalBool(value *bool, field string) models.OptionalBool {
|
||||
@@ -207,3 +296,11 @@ func (t changesetTranslator) optionalBool(value *bool, field string) models.Opti
|
||||
|
||||
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 {
|
||||
@@ -86,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.
|
||||
@@ -95,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,8 +95,12 @@ func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) err
|
||||
return txn.WithTxn(ctx, r.txnManager, 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.Wall(ctx, q)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -106,7 +110,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Scene.Wall(ctx, q)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -117,7 +121,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.GetMarkerStrings(ctx, q, sort)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -129,7 +133,7 @@ func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *stri
|
||||
|
||||
func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
||||
var ret StatsResultType
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
repo := r.repository
|
||||
scenesQB := repo.Scene
|
||||
imageQB := repo.Image
|
||||
@@ -180,19 +184,22 @@ func (r *queryResolver) Version(ctx context.Context) (*Version, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Latestversion returns the latest git shorthash commit.
|
||||
func (r *queryResolver) Latestversion(ctx context.Context) (*ShortVersion, error) {
|
||||
ver, url, err := GetLatestVersion(ctx, true)
|
||||
if err == nil {
|
||||
logger.Infof("Retrieved latest hash: %s", ver)
|
||||
} 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 &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.
|
||||
@@ -205,7 +212,7 @@ func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([
|
||||
var keys []int
|
||||
tags := make(map[int]*SceneMarkerTag)
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
sceneMarkers, err := r.repository.SceneMarker.FindBySceneID(ctx, sceneID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -73,7 +73,7 @@ func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*Fol
|
||||
|
||||
var ret *file.Folder
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
ret, err = r.repository.Folder.Find(ctx, *obj.FolderID)
|
||||
@@ -123,8 +123,9 @@ func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery)
|
||||
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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// #2376 - sort images by path
|
||||
@@ -143,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.withTxn(ctx, func(ctx context.Context) error {
|
||||
// doing this via Query is really slow, so stick with FindByGalleryID
|
||||
imgs, err := r.repository.Image.FindByGalleryID(ctx, 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
|
||||
}
|
||||
@@ -179,7 +165,7 @@ func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*strin
|
||||
|
||||
func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (string, error) {
|
||||
if !obj.Files.PrimaryLoaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadPrimaryFile(ctx, r.repository.File)
|
||||
}); err != nil {
|
||||
return "", err
|
||||
@@ -189,9 +175,21 @@ func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (st
|
||||
return obj.PrimaryChecksum(), nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int, error) {
|
||||
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 !obj.SceneIDs.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadSceneIDs(ctx, r.repository.Gallery)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -213,7 +211,7 @@ func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret
|
||||
|
||||
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) {
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadTagIDs(ctx, r.repository.Gallery)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -227,7 +225,7 @@ func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []
|
||||
|
||||
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) {
|
||||
if !obj.PerformerIDs.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadPerformerIDs(ctx, r.repository.Gallery)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -240,7 +238,7 @@ func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (
|
||||
}
|
||||
|
||||
func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (ret int, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = r.repository.Image.CountByGalleryID(ctx, obj.ID)
|
||||
return err
|
||||
|
||||
@@ -75,6 +75,14 @@ func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFile
|
||||
}, 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) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) {
|
||||
files, err := r.getFiles(ctx, obj)
|
||||
if err != nil {
|
||||
@@ -132,7 +140,7 @@ func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*ImagePat
|
||||
|
||||
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret []*models.Gallery, err error) {
|
||||
if !obj.GalleryIDs.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadGalleryIDs(ctx, r.repository.Image)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -144,6 +152,18 @@ func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret [
|
||||
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 == nil {
|
||||
return nil, nil
|
||||
@@ -154,7 +174,7 @@ func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *mod
|
||||
|
||||
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*models.Tag, err error) {
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadTagIDs(ctx, r.repository.Image)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -168,7 +188,7 @@ func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*mod
|
||||
|
||||
func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret []*models.Performer, err error) {
|
||||
if !obj.PerformerIDs.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadPerformerIDs(ctx, r.repository.Image)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -48,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
|
||||
@@ -86,7 +94,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
img, err = r.repository.Movie.GetBackImage(ctx, obj.ID)
|
||||
if err != nil {
|
||||
@@ -109,7 +117,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = r.repository.Scene.CountByMovieID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -120,7 +128,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = r.repository.Scene.FindByMovieID(ctx, obj.ID)
|
||||
return 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,19 +69,22 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.FindByPerformerID(ctx, 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = r.repository.Scene.CountByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -166,7 +96,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = image.CountByPerformerID(ctx, r.repository.Image, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -178,7 +108,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = gallery.CountByPerformerID(ctx, r.repository.Gallery, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -189,7 +119,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Scene.FindByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -200,65 +130,37 @@ func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (
|
||||
}
|
||||
|
||||
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) {
|
||||
var ret []models.StashID
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = r.repository.Performer.GetStashIDs(ctx, obj.ID)
|
||||
return err
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadStashIDs(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stashIDsSliceToPtrSlice(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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -270,7 +172,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
|
||||
@@ -29,6 +29,8 @@ func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (
|
||||
obj.Files.SetPrimary(ret)
|
||||
|
||||
return ret, nil
|
||||
} else {
|
||||
_ = obj.LoadPrimaryFile(ctx, r.repository.File)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
@@ -139,6 +141,18 @@ func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoF
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, 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))
|
||||
|
||||
@@ -192,7 +206,7 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
|
||||
}
|
||||
|
||||
func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (ret []*models.SceneMarker, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.FindBySceneID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -211,7 +225,7 @@ func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.File.GetCaptions(ctx, primaryFile.Base().ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -223,7 +237,7 @@ 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 !obj.GalleryIDs.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadGalleryIDs(ctx, r.repository.Scene)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -245,7 +259,7 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod
|
||||
|
||||
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) {
|
||||
if !obj.Movies.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
return obj.LoadMovies(ctx, qb)
|
||||
@@ -276,7 +290,7 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S
|
||||
|
||||
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadTagIDs(ctx, r.repository.Scene)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -290,7 +304,7 @@ func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*mod
|
||||
|
||||
func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) {
|
||||
if !obj.PerformerIDs.Loaded() {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadPerformerIDs(ctx, r.repository.Scene)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -313,7 +327,7 @@ func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID {
|
||||
}
|
||||
|
||||
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadStashIDs(ctx, r.repository.Scene)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -13,7 +13,7 @@ func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker
|
||||
panic("Invalid scene id")
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
sceneID := int(obj.SceneID.Int64)
|
||||
ret, err = r.repository.Scene.Find(ctx, sceneID)
|
||||
return err
|
||||
@@ -25,7 +25,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.Find(ctx, obj.PrimaryTagID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -36,7 +36,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.FindBySceneMarkerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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) {
|
||||
@@ -30,7 +31,7 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
|
||||
imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj).GetStudioImageURL()
|
||||
|
||||
var hasImage bool
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
hasImage, err = r.repository.Studio.HasImage(ctx, obj.ID)
|
||||
return err
|
||||
@@ -47,7 +48,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Studio.GetAliases(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -59,7 +60,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = r.repository.Scene.CountByStudioID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -71,7 +72,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = image.CountByStudioID(ctx, r.repository.Image, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -83,7 +84,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = gallery.CountByStudioID(ctx, r.repository.Gallery, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -93,6 +94,18 @@ func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) (
|
||||
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
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
|
||||
if !obj.ParentID.Valid {
|
||||
return nil, nil
|
||||
@@ -102,7 +115,7 @@ func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (
|
||||
}
|
||||
|
||||
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (ret []*models.Studio, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Studio.FindChildren(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -114,7 +127,7 @@ func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (
|
||||
|
||||
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*models.StashID, error) {
|
||||
var ret []models.StashID
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = r.repository.Studio.GetStashIDs(ctx, obj.ID)
|
||||
return err
|
||||
@@ -126,6 +139,14 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*m
|
||||
}
|
||||
|
||||
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
|
||||
@@ -149,7 +170,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -161,7 +182,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = r.repository.Movie.CountByStudioID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
|
||||
@@ -18,7 +18,7 @@ func (r *tagResolver) Description(ctx context.Context, obj *models.Tag) (*string
|
||||
}
|
||||
|
||||
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.FindByChildTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -29,7 +29,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.FindByParentTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -40,7 +40,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.GetAliases(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -52,7 +52,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
count, err = r.repository.Scene.CountByTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -64,7 +64,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
count, err = r.repository.SceneMarker.CountByTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -76,7 +76,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = image.CountByTagID(ctx, r.repository.Image, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -88,7 +88,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
res, err = gallery.CountByTagID(ctx, r.repository.Gallery, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -100,7 +100,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
count, err = r.repository.Performer.CountByTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
@@ -58,7 +59,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
}
|
||||
|
||||
validateDir := func(key string, value string, optional bool) error {
|
||||
if err := checkConfigOverride(config.Metadata); err != nil {
|
||||
if err := checkConfigOverride(key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -227,10 +228,22 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -280,6 +293,19 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
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
|
||||
}
|
||||
@@ -365,6 +391,12 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -445,6 +477,12 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigSc
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,13 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
||||
d := models.NewDate(*input.Date)
|
||||
newGallery.Date = &d
|
||||
}
|
||||
newGallery.Rating = input.Rating
|
||||
|
||||
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.Atoi(*input.StudioID)
|
||||
@@ -177,8 +183,8 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
updatedGallery.Title = models.NewOptionalString(*input.Title)
|
||||
@@ -187,7 +193,7 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
||||
updatedGallery.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedGallery.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
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)
|
||||
@@ -262,8 +268,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
|
||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
||||
updatedGallery.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedGallery.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
|
||||
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
var err error
|
||||
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
|
||||
@@ -103,7 +103,9 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
||||
|
||||
updatedImage := models.NewImagePartial()
|
||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||
updatedImage.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
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)
|
||||
@@ -189,7 +191,9 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
}
|
||||
|
||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||
updatedImage.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
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)
|
||||
|
||||
@@ -156,3 +156,55 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -76,9 +76,11 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
|
||||
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 {
|
||||
@@ -166,7 +168,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
|
||||
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")
|
||||
@@ -239,7 +241,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
|
||||
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")
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -37,9 +35,6 @@ func stashIDPtrSliceToSlice(v []*models.StashID) []models.StashID {
|
||||
}
|
||||
|
||||
func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerCreateInput) (*models.Performer, error) {
|
||||
// generate checksum from performer name rather than image
|
||||
checksum := md5.FromString(input.Name)
|
||||
|
||||
var imageData []byte
|
||||
var err error
|
||||
|
||||
@@ -51,81 +46,99 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
|
||||
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
|
||||
@@ -138,32 +151,17 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
|
||||
}
|
||||
|
||||
// Start the transaction and save the performer
|
||||
var performer *models.Performer
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
|
||||
performer, err = qb.Create(ctx, newPerformer)
|
||||
err = qb.Create(ctx, &newPerformer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(input.TagIds) > 0 {
|
||||
if err := r.updatePerformerTags(ctx, performer.ID, input.TagIds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(ctx, performer.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(ctx, performer.ID, stashIDJoins); err != nil {
|
||||
if err := qb.UpdateImage(ctx, newPerformer.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -173,17 +171,14 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
|
||||
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 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),
|
||||
@@ -199,58 +194,86 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
|
||||
}
|
||||
}
|
||||
|
||||
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(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
|
||||
// need to get existing performer
|
||||
existing, err := qb.Find(ctx, 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 {
|
||||
@@ -259,34 +282,19 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
|
||||
}
|
||||
}
|
||||
|
||||
p, err = qb.Update(ctx, updatedPerformer)
|
||||
_, err = qb.UpdatePartial(ctx, performerID, updatedPerformer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
if err := r.updatePerformerTags(ctx, p.ID, input.TagIds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(ctx, 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(ctx, p.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if translator.hasField("stash_ids") {
|
||||
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(ctx, performerID, stashIDJoins); err != nil {
|
||||
if err := qb.DestroyImage(ctx, performerID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -296,16 +304,8 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, p.ID, plugin.PerformerUpdatePost, input, translator.getFields())
|
||||
return r.getPerformer(ctx, p.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updatePerformerTags(ctx context.Context, performerID int, tagsIDs []string) error {
|
||||
ids, err := stringslice.StringSliceToIntSlice(tagsIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.repository.Performer.UpdateTags(ctx, performerID, ids)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, performerID, plugin.PerformerUpdatePost, input, translator.getFields())
|
||||
return r.getPerformer(ctx, performerID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPerformerUpdateInput) ([]*models.Performer, error) {
|
||||
@@ -315,43 +315,67 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,24 +402,12 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
return err
|
||||
}
|
||||
|
||||
performer, err := qb.Update(ctx, 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(ctx, qb, performerID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateTags(ctx, performerID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -30,6 +31,79 @@ func (r *mutationResolver) getScene(ctx context.Context, id int) (ret *models.Sc
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneCreate(ctx context.Context, input SceneCreateInput) (ret *models.Scene, err error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
performerIDs, err := stringslice.StringSliceToIntSlice(input.PerformerIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
galleryIDs, err := stringslice.StringSliceToIntSlice(input.GalleryIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
|
||||
moviesScenes, err := models.MoviesScenesFromInput(input.Movies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting movies scenes: %w", err)
|
||||
}
|
||||
|
||||
fileIDsInt, err := stringslice.StringSliceToIntSlice(input.FileIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting file ids: %w", err)
|
||||
}
|
||||
|
||||
fileIDs := make([]file.ID, len(fileIDsInt))
|
||||
for i, v := range fileIDsInt {
|
||||
fileIDs[i] = file.ID(v)
|
||||
}
|
||||
|
||||
newScene := models.Scene{
|
||||
Title: translator.string(input.Title, "title"),
|
||||
Code: translator.string(input.Code, "code"),
|
||||
Details: translator.string(input.Details, "details"),
|
||||
Director: translator.string(input.Director, "director"),
|
||||
URL: translator.string(input.URL, "url"),
|
||||
Date: translator.datePtr(input.Date, "date"),
|
||||
Rating: translator.ratingConversionInt(input.Rating, input.Rating100),
|
||||
Organized: translator.bool(input.Organized, "organized"),
|
||||
PerformerIDs: models.NewRelatedIDs(performerIDs),
|
||||
TagIDs: models.NewRelatedIDs(tagIDs),
|
||||
GalleryIDs: models.NewRelatedIDs(galleryIDs),
|
||||
Movies: models.NewRelatedMovies(moviesScenes),
|
||||
StashIDs: models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)),
|
||||
}
|
||||
|
||||
newScene.StudioID, err = translator.intPtrFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
var coverImageData []byte
|
||||
if input.CoverImage != nil && *input.CoverImage != "" {
|
||||
var err error
|
||||
coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.Resolver.sceneService.Create(ctx, &newScene, fileIDs, coverImageData)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (ret *models.Scene, err error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
@@ -90,32 +164,19 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUpdateInput, translator changesetTranslator) (*models.Scene, error) {
|
||||
// Populate scene from the input
|
||||
sceneID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := r.repository.Scene
|
||||
|
||||
s, err := qb.Find(ctx, sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("scene with id %d not found", sceneID)
|
||||
}
|
||||
|
||||
var coverImageData []byte
|
||||
|
||||
func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTranslator) (*models.ScenePartial, error) {
|
||||
updatedScene := models.NewScenePartial()
|
||||
updatedScene.Title = translator.optionalString(input.Title, "title")
|
||||
updatedScene.Code = translator.optionalString(input.Code, "code")
|
||||
updatedScene.Details = translator.optionalString(input.Details, "details")
|
||||
updatedScene.Director = translator.optionalString(input.Director, "director")
|
||||
updatedScene.URL = translator.optionalString(input.URL, "url")
|
||||
updatedScene.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedScene.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedScene.OCounter = translator.optionalInt(input.OCounter, "o_counter")
|
||||
updatedScene.PlayCount = translator.optionalInt(input.PlayCount, "play_count")
|
||||
updatedScene.PlayDuration = translator.optionalFloat64(input.PlayDuration, "play_duration")
|
||||
var err error
|
||||
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
@@ -131,36 +192,6 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp
|
||||
|
||||
converted := file.ID(primaryFileID)
|
||||
updatedScene.PrimaryFileID = &converted
|
||||
|
||||
// if file hash has changed, we should migrate generated files
|
||||
// after commit
|
||||
if err := s.LoadFiles(ctx, r.repository.Scene); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that new primary file is associated with scene
|
||||
var f *file.VideoFile
|
||||
for _, ff := range s.Files.List() {
|
||||
if ff.ID == converted {
|
||||
f = ff
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, fmt.Errorf("file with id %d not associated with scene", converted)
|
||||
}
|
||||
|
||||
fileNamingAlgorithm := config.GetInstance().GetVideoFileNamingAlgorithm()
|
||||
oldHash := scene.GetHash(s.Files.Primary(), fileNamingAlgorithm)
|
||||
newHash := scene.GetHash(f, fileNamingAlgorithm)
|
||||
|
||||
if oldHash != "" && newHash != "" && oldHash != newHash {
|
||||
// perform migration after commit
|
||||
txn.AddPostCommitHook(ctx, func(ctx context.Context) error {
|
||||
scene.MigrateHash(manager.GetInstance().Paths, oldHash, newHash)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("performer_ids") {
|
||||
@@ -200,39 +231,107 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp
|
||||
}
|
||||
}
|
||||
|
||||
return &updatedScene, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUpdateInput, translator changesetTranslator) (*models.Scene, error) {
|
||||
// Populate scene from the input
|
||||
sceneID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := r.repository.Scene
|
||||
|
||||
s, err := qb.Find(ctx, sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("scene with id %d not found", sceneID)
|
||||
}
|
||||
|
||||
var coverImageData []byte
|
||||
|
||||
updatedScene, err := scenePartialFromInput(input, translator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that title is set where scene has no file
|
||||
if updatedScene.Title.Set && updatedScene.Title.Value == "" {
|
||||
if err := s.LoadFiles(ctx, r.repository.Scene); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(s.Files.List()) == 0 {
|
||||
return nil, errors.New("title must be set if scene has no files")
|
||||
}
|
||||
}
|
||||
|
||||
if updatedScene.PrimaryFileID != nil {
|
||||
newPrimaryFileID := *updatedScene.PrimaryFileID
|
||||
|
||||
// if file hash has changed, we should migrate generated files
|
||||
// after commit
|
||||
if err := s.LoadFiles(ctx, r.repository.Scene); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that new primary file is associated with scene
|
||||
var f *file.VideoFile
|
||||
for _, ff := range s.Files.List() {
|
||||
if ff.ID == newPrimaryFileID {
|
||||
f = ff
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, fmt.Errorf("file with id %d not associated with scene", newPrimaryFileID)
|
||||
}
|
||||
}
|
||||
|
||||
if input.CoverImage != nil && *input.CoverImage != "" {
|
||||
var err error
|
||||
coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update the cover after updating the scene
|
||||
}
|
||||
|
||||
s, err = qb.UpdatePartial(ctx, sceneID, updatedScene)
|
||||
s, err = qb.UpdatePartial(ctx, sceneID, *updatedScene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update cover table
|
||||
if len(coverImageData) > 0 {
|
||||
if err := qb.UpdateCover(ctx, sceneID, coverImageData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// only update the cover image if provided and everything else was successful
|
||||
if coverImageData != nil {
|
||||
err = scene.SetScreenshot(manager.GetInstance().Paths, s.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.sceneUpdateCoverImage(ctx, s, coverImageData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) sceneUpdateCoverImage(ctx context.Context, s *models.Scene, coverImageData []byte) error {
|
||||
if len(coverImageData) > 0 {
|
||||
qb := r.repository.Scene
|
||||
|
||||
// update cover table
|
||||
if err := qb.UpdateCover(ctx, s.ID, coverImageData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Path != "" {
|
||||
// update the file-based screenshot after commit
|
||||
txn.AddPostCommitHook(ctx, func(ctx context.Context) error {
|
||||
return scene.SetScreenshot(manager.GetInstance().Paths, s.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneUpdateInput) ([]*models.Scene, error) {
|
||||
sceneIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
@@ -246,10 +345,12 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
|
||||
|
||||
updatedScene := models.NewScenePartial()
|
||||
updatedScene.Title = translator.optionalString(input.Title, "title")
|
||||
updatedScene.Code = translator.optionalString(input.Code, "code")
|
||||
updatedScene.Details = translator.optionalString(input.Details, "details")
|
||||
updatedScene.Director = translator.optionalString(input.Director, "director")
|
||||
updatedScene.URL = translator.optionalString(input.URL, "url")
|
||||
updatedScene.Date = translator.optionalDate(input.Date, "date")
|
||||
updatedScene.Rating = translator.optionalInt(input.Rating, "rating")
|
||||
updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
@@ -322,56 +423,6 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func adjustIDs(existingIDs []int, updateIDs BulkUpdateIds) []int {
|
||||
// if we are setting the ids, just return the ids
|
||||
if updateIDs.Mode == models.RelationshipUpdateModeSet {
|
||||
existingIDs = []int{}
|
||||
for _, idStr := range updateIDs.Ids {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
existingIDs = append(existingIDs, id)
|
||||
}
|
||||
|
||||
return existingIDs
|
||||
}
|
||||
|
||||
for _, idStr := range updateIDs.Ids {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
|
||||
// look for the id in the list
|
||||
foundExisting := false
|
||||
for idx, existingID := range existingIDs {
|
||||
if existingID == id {
|
||||
if updateIDs.Mode == models.RelationshipUpdateModeRemove {
|
||||
// remove from the list
|
||||
existingIDs = append(existingIDs[:idx], existingIDs[idx+1:]...)
|
||||
}
|
||||
|
||||
foundExisting = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundExisting && updateIDs.Mode != models.RelationshipUpdateModeRemove {
|
||||
existingIDs = append(existingIDs, id)
|
||||
}
|
||||
}
|
||||
|
||||
return existingIDs
|
||||
}
|
||||
|
||||
type tagIDsGetter interface {
|
||||
GetTagIDs(ctx context.Context, id int) ([]int, error)
|
||||
}
|
||||
|
||||
func adjustTagIDs(ctx context.Context, qb tagIDsGetter, sceneID int, ids BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetTagIDs(ctx, sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
|
||||
sceneID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -482,6 +533,84 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneAssignFile(ctx context.Context, input AssignSceneFileInput) (bool, error) {
|
||||
sceneID, err := strconv.Atoi(input.SceneID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting scene ID: %w", err)
|
||||
}
|
||||
|
||||
fileIDInt, err := strconv.Atoi(input.FileID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting file ID: %w", err)
|
||||
}
|
||||
|
||||
fileID := file.ID(fileIDInt)
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.Resolver.sceneService.AssignFile(ctx, sceneID, fileID)
|
||||
}); err != nil {
|
||||
return false, fmt.Errorf("assigning file to scene: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput) (*models.Scene, error) {
|
||||
srcIDs, err := stringslice.StringSliceToIntSlice(input.Source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting source IDs: %w", err)
|
||||
}
|
||||
|
||||
destID, err := strconv.Atoi(input.Destination)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting destination ID %s: %w", input.Destination, err)
|
||||
}
|
||||
|
||||
var values *models.ScenePartial
|
||||
if input.Values != nil {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getNamedUpdateInputMap(ctx, "input.values"),
|
||||
}
|
||||
|
||||
values, err = scenePartialFromInput(*input.Values, translator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
v := models.NewScenePartial()
|
||||
values = &v
|
||||
}
|
||||
|
||||
var coverImageData []byte
|
||||
|
||||
if input.Values.CoverImage != nil && *input.Values.CoverImage != "" {
|
||||
var err error
|
||||
coverImageData, err = utils.ProcessImageInput(ctx, *input.Values.CoverImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var ret *models.Scene
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.Resolver.sceneService.Merge(ctx, srcIDs, destID, *values); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret, err = r.Resolver.repository.Scene.Find(ctx, destID)
|
||||
|
||||
if err == nil && ret != nil {
|
||||
err = r.sceneUpdateCoverImage(ctx, ret, coverImageData)
|
||||
}
|
||||
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) getSceneMarker(ctx context.Context, id int) (ret *models.SceneMarker, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.Find(ctx, id)
|
||||
@@ -679,6 +808,42 @@ func (r *mutationResolver) changeMarker(ctx context.Context, changeType int, cha
|
||||
return sceneMarker, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, resumeTime *float64, playDuration *float64) (ret bool, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.SaveActivity(ctx, sceneID, resumeTime, playDuration)
|
||||
return err
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.IncrementWatchCount(ctx, sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
|
||||
@@ -51,7 +51,7 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
|
||||
}
|
||||
|
||||
var res *string
|
||||
err = r.withTxn(ctx, func(ctx context.Context) error {
|
||||
err = r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
scene, err := qb.Find(ctx, id)
|
||||
if err != nil {
|
||||
@@ -82,7 +82,7 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
|
||||
}
|
||||
|
||||
var res *string
|
||||
err = r.withTxn(ctx, func(ctx context.Context) error {
|
||||
err = r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
performer, err := qb.Find(ctx, id)
|
||||
if err != nil {
|
||||
|
||||
@@ -58,11 +58,18 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateI
|
||||
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}
|
||||
}
|
||||
@@ -150,7 +157,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI
|
||||
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
|
||||
|
||||
@@ -123,6 +123,10 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
||||
ScraperCDPPath: &scraperCDPPath,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
PythonPath: config.GetPythonPath(),
|
||||
TranscodeInputArgs: config.GetTranscodeInputArgs(),
|
||||
TranscodeOutputArgs: config.GetTranscodeOutputArgs(),
|
||||
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
|
||||
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +146,8 @@ func makeConfigInterfaceResult() *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()
|
||||
@@ -166,6 +172,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
|
||||
ContinuePlaylistDefault: &continuePlaylistDefault,
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
Javascript: &javascript,
|
||||
JavascriptEnabled: &javascriptEnabled,
|
||||
CustomLocales: &customLocales,
|
||||
CustomLocalesEnabled: &customLocalesEnabled,
|
||||
Language: &language,
|
||||
|
||||
@@ -13,7 +13,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Gallery.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -24,7 +24,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *FindGalleriesResultType, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) 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
|
||||
@@ -41,3 +41,14 @@ func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllGalleries(ctx context.Context) (ret []*models.Gallery, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Gallery.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
|
||||
var image *models.Image
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
var err error
|
||||
|
||||
@@ -47,7 +47,7 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *FindImagesResultType, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
|
||||
fields := graphql.CollectAllFields(ctx)
|
||||
@@ -84,3 +84,14 @@ func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.Imag
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllImages(ctx context.Context) (ret []*models.Image, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Image.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -24,7 +24,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *FindMoviesResultType, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) 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
|
||||
@@ -44,7 +44,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Performer.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -24,7 +24,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *FindPerformersResultType, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) 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
|
||||
@@ -43,7 +43,7 @@ func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *mod
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllPerformers(ctx context.Context) (ret []*models.Performer, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Performer.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SavedFilter.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -23,7 +23,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
if mode != nil {
|
||||
ret, err = r.repository.SavedFilter.FindByMode(ctx, *mode)
|
||||
} else {
|
||||
@@ -37,7 +37,7 @@ 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.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
|
||||
return err
|
||||
}); err != nil {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
|
||||
var scene *models.Scene
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
var err error
|
||||
if id != nil {
|
||||
@@ -43,7 +43,7 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str
|
||||
func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInput) (*models.Scene, error) {
|
||||
var scene *models.Scene
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
if input.Checksum != nil {
|
||||
scenes, err := qb.FindByChecksum(ctx, *input.Checksum)
|
||||
@@ -74,7 +74,7 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInpu
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var scenes []*models.Scene
|
||||
var err error
|
||||
|
||||
@@ -135,7 +135,7 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
|
||||
sceneFilter := &models.SceneFilterType{}
|
||||
|
||||
@@ -192,7 +192,7 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
|
||||
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config manager.SceneParserInput) (ret *SceneParserResultType, err error) {
|
||||
parser := manager.NewSceneFilenameParser(filter, config)
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
result, count, err := parser.Parse(ctx, manager.SceneFilenameParserRepository{
|
||||
Scene: r.repository.Scene,
|
||||
Performer: r.repository.Performer,
|
||||
@@ -223,7 +223,7 @@ func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int)
|
||||
if distance != nil {
|
||||
dist = *distance
|
||||
}
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Scene.FindDuplicates(ctx, dist)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -232,3 +232,14 @@ func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllScenes(ctx context.Context) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Scene.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (ret *FindSceneMarkersResultType, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
sceneMarkers, total, err := r.repository.SceneMarker.Query(ctx, sceneMarkerFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -24,3 +24,14 @@ func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllSceneMarkers(ctx context.Context) (ret []*models.SceneMarker, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = r.repository.Studio.Find(ctx, idInt)
|
||||
return err
|
||||
@@ -25,7 +25,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (ret *FindStudiosResultType, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
studios, total, err := r.repository.Studio.Query(ctx, studioFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -45,7 +45,7 @@ func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.St
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllStudios(ctx context.Context) (ret []*models.Studio, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Studio.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
|
||||
@@ -13,7 +13,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
@@ -24,7 +24,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType) (ret *FindTagsResultType, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
tags, total, err := r.repository.Tag.Query(ctx, tagFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -44,7 +44,7 @@ func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilte
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllTags(ctx context.Context) (ret []*models.Tag, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*manager.SceneStreamEndpoint, error) {
|
||||
// find the scene
|
||||
var scene *models.Scene
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
idInt, _ := strconv.Atoi(*id)
|
||||
var err error
|
||||
scene, err = r.repository.Scene.Find(ctx, idInt)
|
||||
|
||||
35
internal/api/routes_custom.go
Normal file
35
internal/api/routes_custom.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
)
|
||||
|
||||
type customRoutes struct {
|
||||
servedFolders config.URLMap
|
||||
}
|
||||
|
||||
func (rs customRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = strings.Replace(r.URL.Path, "/custom", "", 1)
|
||||
|
||||
// http.FileServer redirects to / if the path ends with index.html
|
||||
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/index.html")
|
||||
|
||||
// map the path to the applicable filesystem location
|
||||
var dir string
|
||||
r.URL.Path, dir = rs.servedFolders.GetFilesystemLocation(r.URL.Path)
|
||||
if dir != "" {
|
||||
http.FileServer(http.Dir(dir)).ServeHTTP(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -143,7 +143,7 @@ func (rs imageRoutes) ImageCtx(next http.Handler) http.Handler {
|
||||
imageID, _ := strconv.Atoi(imageIdentifierQueryParam)
|
||||
|
||||
var image *models.Image
|
||||
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
qb := rs.imageFinder
|
||||
if imageID == 0 {
|
||||
images, _ := qb.FindByChecksum(ctx, imageIdentifierQueryParam)
|
||||
|
||||
@@ -41,7 +41,7 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
|
||||
defaultParam := r.URL.Query().Get("default")
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
image, _ = rs.movieFinder.GetFrontImage(ctx, movie.ID)
|
||||
return nil
|
||||
})
|
||||
@@ -67,7 +67,7 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
|
||||
defaultParam := r.URL.Query().Get("default")
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
image, _ = rs.movieFinder.GetBackImage(ctx, movie.ID)
|
||||
return nil
|
||||
})
|
||||
@@ -97,7 +97,7 @@ func (rs movieRoutes) MovieCtx(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
var movie *models.Movie
|
||||
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
movie, _ = rs.movieFinder.Find(ctx, movieID)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -41,7 +41,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
image, _ = rs.performerFinder.GetImage(ctx, performer.ID)
|
||||
return nil
|
||||
})
|
||||
@@ -54,7 +54,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if len(image) == 0 || defaultParam == "true" {
|
||||
image, _ = getRandomPerformerImageUsingName(performer.Name.String, performer.Gender.String, config.GetInstance().GetCustomPerformerImageLocation())
|
||||
image, _ = getRandomPerformerImageUsingName(performer.Name, performer.Gender, config.GetInstance().GetCustomPerformerImageLocation())
|
||||
}
|
||||
|
||||
if err := utils.ServeImage(image, w, r); err != nil {
|
||||
@@ -71,7 +71,7 @@ func (rs performerRoutes) PerformerCtx(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
var performer *models.Performer
|
||||
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
var err error
|
||||
performer, err = rs.performerFinder.Find(ctx, performerID)
|
||||
return err
|
||||
|
||||
@@ -185,6 +185,8 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, st
|
||||
width := f.Width
|
||||
height := f.Height
|
||||
|
||||
config := config.GetInstance()
|
||||
|
||||
options := ffmpeg.TranscodeStreamOptions{
|
||||
Input: f.Path,
|
||||
Codec: streamFormat,
|
||||
@@ -194,7 +196,9 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, st
|
||||
VideoHeight: height,
|
||||
|
||||
StartTime: ss,
|
||||
MaxTranscodeSize: config.GetInstance().GetMaxStreamingTranscodeSize().GetMaxResolution(),
|
||||
MaxTranscodeSize: config.GetMaxStreamingTranscodeSize().GetMaxResolution(),
|
||||
ExtraInputArgs: config.GetLiveTranscodeInputArgs(),
|
||||
ExtraOutputArgs: config.GetLiveTranscodeOutputArgs(),
|
||||
}
|
||||
|
||||
if requestedSize != "" {
|
||||
@@ -264,7 +268,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce
|
||||
}
|
||||
|
||||
var title string
|
||||
if err := txn.WithTxn(ctx, rs.txnManager, func(ctx context.Context) error {
|
||||
if err := txn.WithReadTxn(ctx, rs.txnManager, func(ctx context.Context) error {
|
||||
qb := rs.tagFinder
|
||||
primaryTag, err := qb.Find(ctx, marker.PrimaryTagID)
|
||||
if err != nil {
|
||||
@@ -293,7 +297,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce
|
||||
func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
var sceneMarkers []*models.SceneMarker
|
||||
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
var err error
|
||||
sceneMarkers, err = rs.sceneMarkerFinder.FindBySceneID(ctx, scene.ID)
|
||||
return err
|
||||
@@ -349,7 +353,7 @@ func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang strin
|
||||
s := r.Context().Value(sceneKey).(*models.Scene)
|
||||
|
||||
var captions []*models.VideoCaption
|
||||
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
var err error
|
||||
primaryFile := s.Files.Primary()
|
||||
if primaryFile == nil {
|
||||
@@ -377,14 +381,14 @@ func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang strin
|
||||
sub, err := video.ReadSubs(caption.Path(s.Path))
|
||||
if err != nil {
|
||||
logger.Warnf("error while reading subs: %v", err)
|
||||
http.Error(w, readTxnErr.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
err = sub.WriteToWebVTT(&b)
|
||||
if err != nil {
|
||||
http.Error(w, readTxnErr.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -423,7 +427,7 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request)
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
|
||||
var sceneMarker *models.SceneMarker
|
||||
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
var err error
|
||||
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
|
||||
return err
|
||||
@@ -450,7 +454,7 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
|
||||
var sceneMarker *models.SceneMarker
|
||||
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
var err error
|
||||
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
|
||||
return err
|
||||
@@ -487,7 +491,7 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
|
||||
var sceneMarker *models.SceneMarker
|
||||
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
var err error
|
||||
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
|
||||
return err
|
||||
@@ -528,7 +532,7 @@ func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler {
|
||||
sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam)
|
||||
|
||||
var scene *models.Scene
|
||||
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
qb := rs.sceneFinder
|
||||
if sceneID == 0 {
|
||||
var scenes []*models.Scene
|
||||
|
||||
@@ -41,7 +41,7 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
image, _ = rs.studioFinder.GetImage(ctx, studio.ID)
|
||||
return nil
|
||||
})
|
||||
@@ -71,7 +71,7 @@ func (rs studioRoutes) StudioCtx(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
var studio *models.Studio
|
||||
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
var err error
|
||||
studio, err = rs.studioFinder.Find(ctx, studioID)
|
||||
return err
|
||||
|
||||
@@ -41,7 +41,7 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
image, _ = rs.tagFinder.GetImage(ctx, tag.ID)
|
||||
return nil
|
||||
})
|
||||
@@ -71,7 +71,7 @@ func (rs tagRoutes) TagCtx(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
var tag *models.Tag
|
||||
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
var err error
|
||||
tag, err = rs.tagFinder.Find(ctx, tagID)
|
||||
return err
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -31,6 +34,7 @@ import (
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/ui"
|
||||
)
|
||||
|
||||
@@ -166,21 +170,8 @@ func Start() error {
|
||||
}.Routes())
|
||||
r.Mount("/downloads", downloadsRoutes{}.Routes())
|
||||
|
||||
r.HandleFunc("/css", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
if !c.GetCSSEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
// search for custom.css in current directory, then $HOME/.stash
|
||||
fn := c.GetCSSPath()
|
||||
exists, _ := fsutil.FileExists(fn)
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, fn)
|
||||
})
|
||||
r.HandleFunc("/css", cssHandler(c, pluginCache))
|
||||
r.HandleFunc("/javascript", javascriptHandler(c, pluginCache))
|
||||
r.HandleFunc("/customlocales", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if c.GetCustomLocalesEnabled() {
|
||||
@@ -216,18 +207,9 @@ func Start() error {
|
||||
// Serve static folders
|
||||
customServedFolders := c.GetCustomServedFolders()
|
||||
if customServedFolders != nil {
|
||||
r.HandleFunc("/custom/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = strings.Replace(r.URL.Path, "/custom", "", 1)
|
||||
|
||||
// map the path to the applicable filesystem location
|
||||
var dir string
|
||||
r.URL.Path, dir = customServedFolders.GetFilesystemLocation(r.URL.Path)
|
||||
if dir != "" {
|
||||
http.FileServer(http.Dir(dir)).ServeHTTP(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
r.Mount("/custom", customRoutes{
|
||||
servedFolders: customServedFolders,
|
||||
}.Routes())
|
||||
}
|
||||
|
||||
customUILocation := c.GetCustomUILocation()
|
||||
@@ -323,23 +305,136 @@ func Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(w io.Writer, path string) (time.Time, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, f)
|
||||
|
||||
return info.ModTime(), err
|
||||
}
|
||||
|
||||
func serveFiles(w http.ResponseWriter, r *http.Request, name string, paths []string) {
|
||||
buffer := bytes.Buffer{}
|
||||
|
||||
latestModTime := time.Time{}
|
||||
|
||||
for _, path := range paths {
|
||||
modTime, err := copyFile(&buffer, path)
|
||||
if err != nil {
|
||||
logger.Errorf("error serving file %s: %v", path, err)
|
||||
} else {
|
||||
if modTime.After(latestModTime) {
|
||||
latestModTime = modTime
|
||||
}
|
||||
buffer.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
bufferReader := bytes.NewReader(buffer.Bytes())
|
||||
http.ServeContent(w, r, name, latestModTime, bufferReader)
|
||||
}
|
||||
|
||||
func cssHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// concatenate with plugin css files
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
|
||||
// add plugin css files first
|
||||
var paths []string
|
||||
|
||||
for _, p := range pluginCache.ListPlugins() {
|
||||
paths = append(paths, p.UI.CSS...)
|
||||
}
|
||||
|
||||
if c.GetCSSEnabled() {
|
||||
// search for custom.css in current directory, then $HOME/.stash
|
||||
fn := c.GetCSSPath()
|
||||
exists, _ := fsutil.FileExists(fn)
|
||||
if exists {
|
||||
paths = append(paths, fn)
|
||||
}
|
||||
}
|
||||
|
||||
serveFiles(w, r, "custom.css", paths)
|
||||
}
|
||||
}
|
||||
|
||||
func javascriptHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/javascript")
|
||||
|
||||
// add plugin javascript files first
|
||||
var paths []string
|
||||
|
||||
for _, p := range pluginCache.ListPlugins() {
|
||||
paths = append(paths, p.UI.Javascript...)
|
||||
}
|
||||
|
||||
if c.GetJavascriptEnabled() {
|
||||
// search for custom.js in current directory, then $HOME/.stash
|
||||
fn := c.GetJavascriptPath()
|
||||
exists, _ := fsutil.FileExists(fn)
|
||||
if exists {
|
||||
paths = append(paths, fn)
|
||||
}
|
||||
}
|
||||
|
||||
serveFiles(w, r, "custom.js", paths)
|
||||
}
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
versionString := githash
|
||||
var versionString string
|
||||
switch {
|
||||
case version != "":
|
||||
if githash != "" && !IsDevelop() {
|
||||
versionString = version + " (" + githash + ")"
|
||||
} else {
|
||||
versionString = version
|
||||
}
|
||||
case githash != "":
|
||||
versionString = githash
|
||||
default:
|
||||
versionString = "unknown"
|
||||
}
|
||||
if config.IsOfficialBuild() {
|
||||
versionString += " - Official Build"
|
||||
} else {
|
||||
versionString += " - Unofficial Build"
|
||||
}
|
||||
if version != "" {
|
||||
versionString = version + " (" + versionString + ")"
|
||||
if buildstamp != "" {
|
||||
versionString += " - " + buildstamp
|
||||
}
|
||||
fmt.Printf("stash version: %s - %s\n", versionString, buildstamp)
|
||||
logger.Infof("stash version: %s\n", versionString)
|
||||
}
|
||||
|
||||
func GetVersion() (string, string, string) {
|
||||
return version, githash, buildstamp
|
||||
}
|
||||
|
||||
func IsDevelop() bool {
|
||||
if githash == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the version is suffixed with -x-xxxx, then we are running a development build
|
||||
develop := false
|
||||
re := regexp.MustCompile(`-\d+-g\w+$`)
|
||||
if re.MatchString(version) {
|
||||
develop = true
|
||||
}
|
||||
return develop
|
||||
}
|
||||
|
||||
func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
|
||||
c.InitTLS()
|
||||
certFile, keyFile := c.GetTLSFiles()
|
||||
@@ -360,12 +455,12 @@ func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
|
||||
|
||||
cert, err := os.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading SSL certificate file %s: %s", certFile, err.Error())
|
||||
return nil, fmt.Errorf("error reading SSL certificate file %s: %v", certFile, err)
|
||||
}
|
||||
|
||||
key, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading SSL key file %s: %s", keyFile, err.Error())
|
||||
return nil, fmt.Errorf("error reading SSL key file %s: %v", keyFile, err)
|
||||
}
|
||||
|
||||
certs := make([]tls.Certificate, 1)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package urlbuilders
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type PerformerURLBuilder struct {
|
||||
@@ -15,7 +16,7 @@ func NewPerformerURLBuilder(baseURL string, performer *models.Performer) Perform
|
||||
return PerformerURLBuilder{
|
||||
BaseURL: baseURL,
|
||||
PerformerID: strconv.Itoa(performer.ID),
|
||||
UpdatedAt: strconv.FormatInt(performer.UpdatedAt.Timestamp.Unix(), 10),
|
||||
UpdatedAt: strconv.FormatInt(performer.UpdatedAt.Unix(), 10),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,15 +21,17 @@ func TestGalleryPerformers(t *testing.T) {
|
||||
const performerName = "performer name"
|
||||
const performerID = 2
|
||||
performer := models.Performer{
|
||||
ID: performerID,
|
||||
Name: models.NullString(performerName),
|
||||
ID: performerID,
|
||||
Name: performerName,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
}
|
||||
|
||||
const reversedPerformerName = "name performer"
|
||||
const reversedPerformerID = 3
|
||||
reversedPerformer := models.Performer{
|
||||
ID: reversedPerformerID,
|
||||
Name: models.NullString(reversedPerformerName),
|
||||
ID: reversedPerformerID,
|
||||
Name: reversedPerformerName,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
}
|
||||
|
||||
testTables := generateTestTable(performerName, galleryExt)
|
||||
|
||||
@@ -18,15 +18,17 @@ func TestImagePerformers(t *testing.T) {
|
||||
const performerName = "performer name"
|
||||
const performerID = 2
|
||||
performer := models.Performer{
|
||||
ID: performerID,
|
||||
Name: models.NullString(performerName),
|
||||
ID: performerID,
|
||||
Name: performerName,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
}
|
||||
|
||||
const reversedPerformerName = "name performer"
|
||||
const reversedPerformerID = 3
|
||||
reversedPerformer := models.Performer{
|
||||
ID: reversedPerformerID,
|
||||
Name: models.NullString(reversedPerformerName),
|
||||
ID: reversedPerformerID,
|
||||
Name: reversedPerformerName,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
}
|
||||
|
||||
testTables := generateTestTable(performerName, imageExt)
|
||||
|
||||
@@ -86,12 +86,10 @@ func TestMain(m *testing.M) {
|
||||
func createPerformer(ctx context.Context, pqb models.PerformerWriter) error {
|
||||
// create the performer
|
||||
performer := models.Performer{
|
||||
Checksum: testName,
|
||||
Name: sql.NullString{Valid: true, String: testName},
|
||||
Favorite: sql.NullBool{Valid: true, Bool: false},
|
||||
Name: testName,
|
||||
}
|
||||
|
||||
_, err := pqb.Create(ctx, performer)
|
||||
err := pqb.Create(ctx, &performer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -480,6 +478,10 @@ func withTxn(f func(ctx context.Context) error) error {
|
||||
return txn.WithTxn(context.TODO(), db, f)
|
||||
}
|
||||
|
||||
func withDB(f func(ctx context.Context) error) error {
|
||||
return txn.WithDatabase(context.TODO(), db, f)
|
||||
}
|
||||
|
||||
func populateDB() error {
|
||||
if err := withTxn(func(ctx context.Context) error {
|
||||
err := createPerformer(ctx, r.Performer)
|
||||
@@ -539,9 +541,16 @@ func TestParsePerformerScenes(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
tagger := Tagger{
|
||||
TxnManager: db,
|
||||
}
|
||||
|
||||
for _, p := range performers {
|
||||
if err := withTxn(func(ctx context.Context) error {
|
||||
return PerformerScenes(ctx, p, nil, r.Scene, nil)
|
||||
if err := withDB(func(ctx context.Context) error {
|
||||
if err := p.LoadAliases(ctx, r.Performer); err != nil {
|
||||
return err
|
||||
}
|
||||
return tagger.PerformerScenes(ctx, p, nil, r.Scene)
|
||||
}); err != nil {
|
||||
t.Errorf("Error auto-tagging performers: %s", err)
|
||||
}
|
||||
@@ -586,14 +595,18 @@ func TestParseStudioScenes(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
tagger := Tagger{
|
||||
TxnManager: db,
|
||||
}
|
||||
|
||||
for _, s := range studios {
|
||||
if err := withTxn(func(ctx context.Context) error {
|
||||
if err := withDB(func(ctx context.Context) error {
|
||||
aliases, err := r.Studio.GetAliases(ctx, s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return StudioScenes(ctx, s, nil, aliases, r.Scene, nil)
|
||||
return tagger.StudioScenes(ctx, s, nil, aliases, r.Scene)
|
||||
}); err != nil {
|
||||
t.Errorf("Error auto-tagging performers: %s", err)
|
||||
}
|
||||
@@ -642,14 +655,18 @@ func TestParseTagScenes(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
tagger := Tagger{
|
||||
TxnManager: db,
|
||||
}
|
||||
|
||||
for _, s := range tags {
|
||||
if err := withTxn(func(ctx context.Context) error {
|
||||
if err := withDB(func(ctx context.Context) error {
|
||||
aliases, err := r.Tag.GetAliases(ctx, s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return TagScenes(ctx, s, nil, aliases, r.Scene, nil)
|
||||
return tagger.TagScenes(ctx, s, nil, aliases, r.Scene)
|
||||
}); err != nil {
|
||||
t.Errorf("Error auto-tagging performers: %s", err)
|
||||
}
|
||||
@@ -694,9 +711,16 @@ func TestParsePerformerImages(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
tagger := Tagger{
|
||||
TxnManager: db,
|
||||
}
|
||||
|
||||
for _, p := range performers {
|
||||
if err := withTxn(func(ctx context.Context) error {
|
||||
return PerformerImages(ctx, p, nil, r.Image, nil)
|
||||
if err := withDB(func(ctx context.Context) error {
|
||||
if err := p.LoadAliases(ctx, r.Performer); err != nil {
|
||||
return err
|
||||
}
|
||||
return tagger.PerformerImages(ctx, p, nil, r.Image)
|
||||
}); err != nil {
|
||||
t.Errorf("Error auto-tagging performers: %s", err)
|
||||
}
|
||||
@@ -742,14 +766,18 @@ func TestParseStudioImages(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
tagger := Tagger{
|
||||
TxnManager: db,
|
||||
}
|
||||
|
||||
for _, s := range studios {
|
||||
if err := withTxn(func(ctx context.Context) error {
|
||||
if err := withDB(func(ctx context.Context) error {
|
||||
aliases, err := r.Studio.GetAliases(ctx, s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return StudioImages(ctx, s, nil, aliases, r.Image, nil)
|
||||
return tagger.StudioImages(ctx, s, nil, aliases, r.Image)
|
||||
}); err != nil {
|
||||
t.Errorf("Error auto-tagging performers: %s", err)
|
||||
}
|
||||
@@ -798,14 +826,18 @@ func TestParseTagImages(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
tagger := Tagger{
|
||||
TxnManager: db,
|
||||
}
|
||||
|
||||
for _, s := range tags {
|
||||
if err := withTxn(func(ctx context.Context) error {
|
||||
if err := withDB(func(ctx context.Context) error {
|
||||
aliases, err := r.Tag.GetAliases(ctx, s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return TagImages(ctx, s, nil, aliases, r.Image, nil)
|
||||
return tagger.TagImages(ctx, s, nil, aliases, r.Image)
|
||||
}); err != nil {
|
||||
t.Errorf("Error auto-tagging performers: %s", err)
|
||||
}
|
||||
@@ -851,9 +883,16 @@ func TestParsePerformerGalleries(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
tagger := Tagger{
|
||||
TxnManager: db,
|
||||
}
|
||||
|
||||
for _, p := range performers {
|
||||
if err := withTxn(func(ctx context.Context) error {
|
||||
return PerformerGalleries(ctx, p, nil, r.Gallery, nil)
|
||||
if err := withDB(func(ctx context.Context) error {
|
||||
if err := p.LoadAliases(ctx, r.Performer); err != nil {
|
||||
return err
|
||||
}
|
||||
return tagger.PerformerGalleries(ctx, p, nil, r.Gallery)
|
||||
}); err != nil {
|
||||
t.Errorf("Error auto-tagging performers: %s", err)
|
||||
}
|
||||
@@ -899,14 +938,18 @@ func TestParseStudioGalleries(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
tagger := Tagger{
|
||||
TxnManager: db,
|
||||
}
|
||||
|
||||
for _, s := range studios {
|
||||
if err := withTxn(func(ctx context.Context) error {
|
||||
if err := withDB(func(ctx context.Context) error {
|
||||
aliases, err := r.Studio.GetAliases(ctx, s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return StudioGalleries(ctx, s, nil, aliases, r.Gallery, nil)
|
||||
return tagger.StudioGalleries(ctx, s, nil, aliases, r.Gallery)
|
||||
}); err != nil {
|
||||
t.Errorf("Error auto-tagging performers: %s", err)
|
||||
}
|
||||
@@ -955,14 +998,18 @@ func TestParseTagGalleries(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
tagger := Tagger{
|
||||
TxnManager: db,
|
||||
}
|
||||
|
||||
for _, s := range tags {
|
||||
if err := withTxn(func(ctx context.Context) error {
|
||||
if err := withDB(func(ctx context.Context) error {
|
||||
aliases, err := r.Tag.GetAliases(ctx, s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return TagGalleries(ctx, s, nil, aliases, r.Gallery, nil)
|
||||
return tagger.TagGalleries(ctx, s, nil, aliases, r.Gallery)
|
||||
}); err != nil {
|
||||
t.Errorf("Error auto-tagging performers: %s", err)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
type SceneQueryPerformerUpdater interface {
|
||||
@@ -29,77 +30,111 @@ type GalleryQueryPerformerUpdater interface {
|
||||
gallery.PartialUpdater
|
||||
}
|
||||
|
||||
func getPerformerTagger(p *models.Performer, cache *match.Cache) tagger {
|
||||
return tagger{
|
||||
func getPerformerTaggers(p *models.Performer, cache *match.Cache) []tagger {
|
||||
ret := []tagger{{
|
||||
ID: p.ID,
|
||||
Type: "performer",
|
||||
Name: p.Name.String,
|
||||
Name: p.Name,
|
||||
cache: cache,
|
||||
}
|
||||
}}
|
||||
|
||||
// TODO - disabled until we can have finer control over alias matching
|
||||
// for _, a := range p.Aliases.List() {
|
||||
// ret = append(ret, tagger{
|
||||
// ID: p.ID,
|
||||
// Type: "performer",
|
||||
// Name: a,
|
||||
// cache: cache,
|
||||
// })
|
||||
// }
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// PerformerScenes searches for scenes whose path matches the provided performer name and tags the scene with the performer.
|
||||
func PerformerScenes(ctx context.Context, p *models.Performer, paths []string, rw SceneQueryPerformerUpdater, cache *match.Cache) error {
|
||||
t := getPerformerTagger(p, cache)
|
||||
// Performer aliases must be loaded.
|
||||
func (tagger *Tagger) PerformerScenes(ctx context.Context, p *models.Performer, paths []string, rw SceneQueryPerformerUpdater) error {
|
||||
t := getPerformerTaggers(p, tagger.Cache)
|
||||
|
||||
return t.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
|
||||
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
|
||||
return false, err
|
||||
for _, tt := range t {
|
||||
if err := tt.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
|
||||
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
|
||||
return false, err
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if intslice.IntInclude(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
|
||||
return scene.AddPerformer(ctx, rw, o, p.ID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if intslice.IntInclude(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := scene.AddPerformer(ctx, rw, o, p.ID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PerformerImages searches for images whose path matches the provided performer name and tags the image with the performer.
|
||||
func PerformerImages(ctx context.Context, p *models.Performer, paths []string, rw ImageQueryPerformerUpdater, cache *match.Cache) error {
|
||||
t := getPerformerTagger(p, cache)
|
||||
func (tagger *Tagger) PerformerImages(ctx context.Context, p *models.Performer, paths []string, rw ImageQueryPerformerUpdater) error {
|
||||
t := getPerformerTaggers(p, tagger.Cache)
|
||||
|
||||
return t.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) {
|
||||
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
|
||||
return false, err
|
||||
for _, tt := range t {
|
||||
if err := tt.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) {
|
||||
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
|
||||
return false, err
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if intslice.IntInclude(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
|
||||
return image.AddPerformer(ctx, rw, o, p.ID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if intslice.IntInclude(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := image.AddPerformer(ctx, rw, o, p.ID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PerformerGalleries searches for galleries whose path matches the provided performer name and tags the gallery with the performer.
|
||||
func PerformerGalleries(ctx context.Context, p *models.Performer, paths []string, rw GalleryQueryPerformerUpdater, cache *match.Cache) error {
|
||||
t := getPerformerTagger(p, cache)
|
||||
func (tagger *Tagger) PerformerGalleries(ctx context.Context, p *models.Performer, paths []string, rw GalleryQueryPerformerUpdater) error {
|
||||
t := getPerformerTaggers(p, tagger.Cache)
|
||||
|
||||
return t.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
|
||||
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
|
||||
return false, err
|
||||
for _, tt := range t {
|
||||
if err := tt.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
|
||||
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
|
||||
return false, err
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if intslice.IntInclude(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
|
||||
return gallery.AddPerformer(ctx, rw, o, p.ID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if intslice.IntInclude(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := gallery.AddPerformer(ctx, rw, o, p.ID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
func TestPerformerScenes(t *testing.T) {
|
||||
@@ -59,12 +60,15 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
|
||||
}
|
||||
|
||||
performer := models.Performer{
|
||||
ID: performerID,
|
||||
Name: models.NullString(performerName),
|
||||
ID: performerID,
|
||||
Name: performerName,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
}
|
||||
|
||||
organized := false
|
||||
perPage := models.PerPageAll
|
||||
perPage := 1000
|
||||
sort := "id"
|
||||
direction := models.SortDirectionEnumAsc
|
||||
|
||||
expectedSceneFilter := &models.SceneFilterType{
|
||||
Organized: &organized,
|
||||
@@ -75,15 +79,17 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
|
||||
}
|
||||
|
||||
expectedFindFilter := &models.FindFilterType{
|
||||
PerPage: &perPage,
|
||||
PerPage: &perPage,
|
||||
Sort: &sort,
|
||||
Direction: &direction,
|
||||
}
|
||||
|
||||
mockSceneReader.On("Query", testCtx, scene.QueryOptions(expectedSceneFilter, expectedFindFilter, false)).
|
||||
mockSceneReader.On("Query", mock.Anything, scene.QueryOptions(expectedSceneFilter, expectedFindFilter, false)).
|
||||
Return(mocks.SceneQueryResult(scenes, len(scenes)), nil).Once()
|
||||
|
||||
for i := range matchingPaths {
|
||||
sceneID := i + 1
|
||||
mockSceneReader.On("UpdatePartial", testCtx, sceneID, models.ScenePartial{
|
||||
mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, models.ScenePartial{
|
||||
PerformerIDs: &models.UpdateIDs{
|
||||
IDs: []int{performerID},
|
||||
Mode: models.RelationshipUpdateModeAdd,
|
||||
@@ -91,7 +97,11 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
|
||||
}).Return(nil, nil).Once()
|
||||
}
|
||||
|
||||
err := PerformerScenes(testCtx, &performer, nil, mockSceneReader, nil)
|
||||
tagger := Tagger{
|
||||
TxnManager: &mocks.TxnManager{},
|
||||
}
|
||||
|
||||
err := tagger.PerformerScenes(testCtx, &performer, nil, mockSceneReader)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
@@ -139,12 +149,15 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) {
|
||||
}
|
||||
|
||||
performer := models.Performer{
|
||||
ID: performerID,
|
||||
Name: models.NullString(performerName),
|
||||
ID: performerID,
|
||||
Name: performerName,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
}
|
||||
|
||||
organized := false
|
||||
perPage := models.PerPageAll
|
||||
perPage := 1000
|
||||
sort := "id"
|
||||
direction := models.SortDirectionEnumAsc
|
||||
|
||||
expectedImageFilter := &models.ImageFilterType{
|
||||
Organized: &organized,
|
||||
@@ -155,15 +168,17 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) {
|
||||
}
|
||||
|
||||
expectedFindFilter := &models.FindFilterType{
|
||||
PerPage: &perPage,
|
||||
PerPage: &perPage,
|
||||
Sort: &sort,
|
||||
Direction: &direction,
|
||||
}
|
||||
|
||||
mockImageReader.On("Query", testCtx, image.QueryOptions(expectedImageFilter, expectedFindFilter, false)).
|
||||
mockImageReader.On("Query", mock.Anything, image.QueryOptions(expectedImageFilter, expectedFindFilter, false)).
|
||||
Return(mocks.ImageQueryResult(images, len(images)), nil).Once()
|
||||
|
||||
for i := range matchingPaths {
|
||||
imageID := i + 1
|
||||
mockImageReader.On("UpdatePartial", testCtx, imageID, models.ImagePartial{
|
||||
mockImageReader.On("UpdatePartial", mock.Anything, imageID, models.ImagePartial{
|
||||
PerformerIDs: &models.UpdateIDs{
|
||||
IDs: []int{performerID},
|
||||
Mode: models.RelationshipUpdateModeAdd,
|
||||
@@ -171,7 +186,11 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) {
|
||||
}).Return(nil, nil).Once()
|
||||
}
|
||||
|
||||
err := PerformerImages(testCtx, &performer, nil, mockImageReader, nil)
|
||||
tagger := Tagger{
|
||||
TxnManager: &mocks.TxnManager{},
|
||||
}
|
||||
|
||||
err := tagger.PerformerImages(testCtx, &performer, nil, mockImageReader)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
@@ -220,12 +239,15 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) {
|
||||
}
|
||||
|
||||
performer := models.Performer{
|
||||
ID: performerID,
|
||||
Name: models.NullString(performerName),
|
||||
ID: performerID,
|
||||
Name: performerName,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
}
|
||||
|
||||
organized := false
|
||||
perPage := models.PerPageAll
|
||||
perPage := 1000
|
||||
sort := "id"
|
||||
direction := models.SortDirectionEnumAsc
|
||||
|
||||
expectedGalleryFilter := &models.GalleryFilterType{
|
||||
Organized: &organized,
|
||||
@@ -236,14 +258,16 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) {
|
||||
}
|
||||
|
||||
expectedFindFilter := &models.FindFilterType{
|
||||
PerPage: &perPage,
|
||||
PerPage: &perPage,
|
||||
Sort: &sort,
|
||||
Direction: &direction,
|
||||
}
|
||||
|
||||
mockGalleryReader.On("Query", testCtx, expectedGalleryFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
|
||||
mockGalleryReader.On("Query", mock.Anything, expectedGalleryFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
|
||||
|
||||
for i := range matchingPaths {
|
||||
galleryID := i + 1
|
||||
mockGalleryReader.On("UpdatePartial", testCtx, galleryID, models.GalleryPartial{
|
||||
mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, models.GalleryPartial{
|
||||
PerformerIDs: &models.UpdateIDs{
|
||||
IDs: []int{performerID},
|
||||
Mode: models.RelationshipUpdateModeAdd,
|
||||
@@ -251,7 +275,11 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) {
|
||||
}).Return(nil, nil).Once()
|
||||
}
|
||||
|
||||
err := PerformerGalleries(testCtx, &performer, nil, mockGalleryReader, nil)
|
||||
tagger := Tagger{
|
||||
TxnManager: &mocks.TxnManager{},
|
||||
}
|
||||
|
||||
err := tagger.PerformerGalleries(testCtx, &performer, nil, mockGalleryReader)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
|
||||
@@ -151,15 +151,17 @@ func TestScenePerformers(t *testing.T) {
|
||||
const performerName = "performer name"
|
||||
const performerID = 2
|
||||
performer := models.Performer{
|
||||
ID: performerID,
|
||||
Name: models.NullString(performerName),
|
||||
ID: performerID,
|
||||
Name: performerName,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
}
|
||||
|
||||
const reversedPerformerName = "name performer"
|
||||
const reversedPerformerID = 3
|
||||
reversedPerformer := models.Performer{
|
||||
ID: reversedPerformerID,
|
||||
Name: models.NullString(reversedPerformerName),
|
||||
ID: reversedPerformerID,
|
||||
Name: reversedPerformerName,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
}
|
||||
|
||||
testTables := generateTestTable(performerName, sceneExt)
|
||||
|
||||
@@ -8,8 +8,12 @@ import (
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
// the following functions aren't used in Tagger because they assume
|
||||
// use within a transaction
|
||||
|
||||
func addSceneStudio(ctx context.Context, sceneWriter scene.PartialUpdater, o *models.Scene, studioID int) (bool, error) {
|
||||
// don't set if already set
|
||||
if o.StudioID != nil {
|
||||
@@ -86,12 +90,28 @@ type SceneFinderUpdater interface {
|
||||
}
|
||||
|
||||
// StudioScenes searches for scenes whose path matches the provided studio name and tags the scene with the studio, if studio is not already set on the scene.
|
||||
func StudioScenes(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw SceneFinderUpdater, cache *match.Cache) error {
|
||||
t := getStudioTagger(p, aliases, cache)
|
||||
func (tagger *Tagger) StudioScenes(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw SceneFinderUpdater) error {
|
||||
t := getStudioTagger(p, aliases, tagger.Cache)
|
||||
|
||||
for _, tt := range t {
|
||||
if err := tt.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
|
||||
return addSceneStudio(ctx, rw, o, p.ID)
|
||||
// don't set if already set
|
||||
if o.StudioID != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// set the studio id
|
||||
scenePartial := models.ScenePartial{
|
||||
StudioID: models.NewOptionalInt(p.ID),
|
||||
}
|
||||
|
||||
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
|
||||
_, err := rw.UpdatePartial(ctx, o.ID, scenePartial)
|
||||
return err
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -107,12 +127,28 @@ type ImageFinderUpdater interface {
|
||||
}
|
||||
|
||||
// StudioImages searches for images whose path matches the provided studio name and tags the image with the studio, if studio is not already set on the image.
|
||||
func StudioImages(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw ImageFinderUpdater, cache *match.Cache) error {
|
||||
t := getStudioTagger(p, aliases, cache)
|
||||
func (tagger *Tagger) StudioImages(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw ImageFinderUpdater) error {
|
||||
t := getStudioTagger(p, aliases, tagger.Cache)
|
||||
|
||||
for _, tt := range t {
|
||||
if err := tt.tagImages(ctx, paths, rw, func(i *models.Image) (bool, error) {
|
||||
return addImageStudio(ctx, rw, i, p.ID)
|
||||
// don't set if already set
|
||||
if i.StudioID != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// set the studio id
|
||||
imagePartial := models.ImagePartial{
|
||||
StudioID: models.NewOptionalInt(p.ID),
|
||||
}
|
||||
|
||||
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
|
||||
_, err := rw.UpdatePartial(ctx, i.ID, imagePartial)
|
||||
return err
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -128,12 +164,28 @@ type GalleryFinderUpdater interface {
|
||||
}
|
||||
|
||||
// StudioGalleries searches for galleries whose path matches the provided studio name and tags the gallery with the studio, if studio is not already set on the gallery.
|
||||
func StudioGalleries(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw GalleryFinderUpdater, cache *match.Cache) error {
|
||||
t := getStudioTagger(p, aliases, cache)
|
||||
func (tagger *Tagger) StudioGalleries(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw GalleryFinderUpdater) error {
|
||||
t := getStudioTagger(p, aliases, tagger.Cache)
|
||||
|
||||
for _, tt := range t {
|
||||
if err := tt.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
|
||||
return addGalleryStudio(ctx, rw, o, p.ID)
|
||||
// don't set if already set
|
||||
if o.StudioID != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// set the studio id
|
||||
galleryPartial := models.GalleryPartial{
|
||||
StudioID: models.NewOptionalInt(p.ID),
|
||||
}
|
||||
|
||||
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
|
||||
_, err := rw.UpdatePartial(ctx, o.ID, galleryPartial)
|
||||
return err
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type testStudioCase struct {
|
||||
@@ -110,7 +111,9 @@ func testStudioScenes(t *testing.T, tc testStudioCase) {
|
||||
}
|
||||
|
||||
organized := false
|
||||
perPage := models.PerPageAll
|
||||
perPage := 1000
|
||||
sort := "id"
|
||||
direction := models.SortDirectionEnumAsc
|
||||
|
||||
expectedSceneFilter := &models.SceneFilterType{
|
||||
Organized: &organized,
|
||||
@@ -121,7 +124,9 @@ func testStudioScenes(t *testing.T, tc testStudioCase) {
|
||||
}
|
||||
|
||||
expectedFindFilter := &models.FindFilterType{
|
||||
PerPage: &perPage,
|
||||
PerPage: &perPage,
|
||||
Sort: &sort,
|
||||
Direction: &direction,
|
||||
}
|
||||
|
||||
// if alias provided, then don't find by name
|
||||
@@ -140,19 +145,23 @@ func testStudioScenes(t *testing.T, tc testStudioCase) {
|
||||
},
|
||||
}
|
||||
|
||||
mockSceneReader.On("Query", testCtx, scene.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
|
||||
mockSceneReader.On("Query", mock.Anything, scene.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
|
||||
Return(mocks.SceneQueryResult(scenes, len(scenes)), nil).Once()
|
||||
}
|
||||
|
||||
for i := range matchingPaths {
|
||||
sceneID := i + 1
|
||||
expectedStudioID := studioID
|
||||
mockSceneReader.On("UpdatePartial", testCtx, sceneID, models.ScenePartial{
|
||||
mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, models.ScenePartial{
|
||||
StudioID: models.NewOptionalInt(expectedStudioID),
|
||||
}).Return(nil, nil).Once()
|
||||
}
|
||||
|
||||
err := StudioScenes(testCtx, &studio, nil, aliases, mockSceneReader, nil)
|
||||
tagger := Tagger{
|
||||
TxnManager: &mocks.TxnManager{},
|
||||
}
|
||||
|
||||
err := tagger.StudioScenes(testCtx, &studio, nil, aliases, mockSceneReader)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
@@ -201,7 +210,9 @@ func testStudioImages(t *testing.T, tc testStudioCase) {
|
||||
}
|
||||
|
||||
organized := false
|
||||
perPage := models.PerPageAll
|
||||
perPage := 1000
|
||||
sort := "id"
|
||||
direction := models.SortDirectionEnumAsc
|
||||
|
||||
expectedImageFilter := &models.ImageFilterType{
|
||||
Organized: &organized,
|
||||
@@ -212,11 +223,13 @@ func testStudioImages(t *testing.T, tc testStudioCase) {
|
||||
}
|
||||
|
||||
expectedFindFilter := &models.FindFilterType{
|
||||
PerPage: &perPage,
|
||||
PerPage: &perPage,
|
||||
Sort: &sort,
|
||||
Direction: &direction,
|
||||
}
|
||||
|
||||
// if alias provided, then don't find by name
|
||||
onNameQuery := mockImageReader.On("Query", testCtx, image.QueryOptions(expectedImageFilter, expectedFindFilter, false))
|
||||
onNameQuery := mockImageReader.On("Query", mock.Anything, image.QueryOptions(expectedImageFilter, expectedFindFilter, false))
|
||||
if aliasName == "" {
|
||||
onNameQuery.Return(mocks.ImageQueryResult(images, len(images)), nil).Once()
|
||||
} else {
|
||||
@@ -230,19 +243,23 @@ func testStudioImages(t *testing.T, tc testStudioCase) {
|
||||
},
|
||||
}
|
||||
|
||||
mockImageReader.On("Query", testCtx, image.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
|
||||
mockImageReader.On("Query", mock.Anything, image.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
|
||||
Return(mocks.ImageQueryResult(images, len(images)), nil).Once()
|
||||
}
|
||||
|
||||
for i := range matchingPaths {
|
||||
imageID := i + 1
|
||||
expectedStudioID := studioID
|
||||
mockImageReader.On("UpdatePartial", testCtx, imageID, models.ImagePartial{
|
||||
mockImageReader.On("UpdatePartial", mock.Anything, imageID, models.ImagePartial{
|
||||
StudioID: models.NewOptionalInt(expectedStudioID),
|
||||
}).Return(nil, nil).Once()
|
||||
}
|
||||
|
||||
err := StudioImages(testCtx, &studio, nil, aliases, mockImageReader, nil)
|
||||
tagger := Tagger{
|
||||
TxnManager: &mocks.TxnManager{},
|
||||
}
|
||||
|
||||
err := tagger.StudioImages(testCtx, &studio, nil, aliases, mockImageReader)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
@@ -291,7 +308,9 @@ func testStudioGalleries(t *testing.T, tc testStudioCase) {
|
||||
}
|
||||
|
||||
organized := false
|
||||
perPage := models.PerPageAll
|
||||
perPage := 1000
|
||||
sort := "id"
|
||||
direction := models.SortDirectionEnumAsc
|
||||
|
||||
expectedGalleryFilter := &models.GalleryFilterType{
|
||||
Organized: &organized,
|
||||
@@ -302,11 +321,13 @@ func testStudioGalleries(t *testing.T, tc testStudioCase) {
|
||||
}
|
||||
|
||||
expectedFindFilter := &models.FindFilterType{
|
||||
PerPage: &perPage,
|
||||
PerPage: &perPage,
|
||||
Sort: &sort,
|
||||
Direction: &direction,
|
||||
}
|
||||
|
||||
// if alias provided, then don't find by name
|
||||
onNameQuery := mockGalleryReader.On("Query", testCtx, expectedGalleryFilter, expectedFindFilter)
|
||||
onNameQuery := mockGalleryReader.On("Query", mock.Anything, expectedGalleryFilter, expectedFindFilter)
|
||||
if aliasName == "" {
|
||||
onNameQuery.Return(galleries, len(galleries), nil).Once()
|
||||
} else {
|
||||
@@ -320,18 +341,22 @@ func testStudioGalleries(t *testing.T, tc testStudioCase) {
|
||||
},
|
||||
}
|
||||
|
||||
mockGalleryReader.On("Query", testCtx, expectedAliasFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
|
||||
mockGalleryReader.On("Query", mock.Anything, expectedAliasFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
|
||||
}
|
||||
|
||||
for i := range matchingPaths {
|
||||
galleryID := i + 1
|
||||
expectedStudioID := studioID
|
||||
mockGalleryReader.On("UpdatePartial", testCtx, galleryID, models.GalleryPartial{
|
||||
mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, models.GalleryPartial{
|
||||
StudioID: models.NewOptionalInt(expectedStudioID),
|
||||
}).Return(nil, nil).Once()
|
||||
}
|
||||
|
||||
err := StudioGalleries(testCtx, &studio, nil, aliases, mockGalleryReader, nil)
|
||||
tagger := Tagger{
|
||||
TxnManager: &mocks.TxnManager{},
|
||||
}
|
||||
|
||||
err := tagger.StudioGalleries(testCtx, &studio, nil, aliases, mockGalleryReader)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
type SceneQueryTagUpdater interface {
|
||||
@@ -50,8 +51,8 @@ func getTagTaggers(p *models.Tag, aliases []string, cache *match.Cache) []tagger
|
||||
}
|
||||
|
||||
// TagScenes searches for scenes whose path matches the provided tag name and tags the scene with the tag.
|
||||
func TagScenes(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw SceneQueryTagUpdater, cache *match.Cache) error {
|
||||
t := getTagTaggers(p, aliases, cache)
|
||||
func (tagger *Tagger) TagScenes(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw SceneQueryTagUpdater) error {
|
||||
t := getTagTaggers(p, aliases, tagger.Cache)
|
||||
|
||||
for _, tt := range t {
|
||||
if err := tt.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
|
||||
@@ -64,7 +65,9 @@ func TagScenes(ctx context.Context, p *models.Tag, paths []string, aliases []str
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := scene.AddTag(ctx, rw, o, p.ID); err != nil {
|
||||
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
|
||||
return scene.AddTag(ctx, rw, o, p.ID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -77,8 +80,8 @@ func TagScenes(ctx context.Context, p *models.Tag, paths []string, aliases []str
|
||||
}
|
||||
|
||||
// TagImages searches for images whose path matches the provided tag name and tags the image with the tag.
|
||||
func TagImages(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw ImageQueryTagUpdater, cache *match.Cache) error {
|
||||
t := getTagTaggers(p, aliases, cache)
|
||||
func (tagger *Tagger) TagImages(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw ImageQueryTagUpdater) error {
|
||||
t := getTagTaggers(p, aliases, tagger.Cache)
|
||||
|
||||
for _, tt := range t {
|
||||
if err := tt.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) {
|
||||
@@ -91,7 +94,9 @@ func TagImages(ctx context.Context, p *models.Tag, paths []string, aliases []str
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := image.AddTag(ctx, rw, o, p.ID); err != nil {
|
||||
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
|
||||
return image.AddTag(ctx, rw, o, p.ID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -104,8 +109,8 @@ func TagImages(ctx context.Context, p *models.Tag, paths []string, aliases []str
|
||||
}
|
||||
|
||||
// TagGalleries searches for galleries whose path matches the provided tag name and tags the gallery with the tag.
|
||||
func TagGalleries(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw GalleryQueryTagUpdater, cache *match.Cache) error {
|
||||
t := getTagTaggers(p, aliases, cache)
|
||||
func (tagger *Tagger) TagGalleries(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw GalleryQueryTagUpdater) error {
|
||||
t := getTagTaggers(p, aliases, tagger.Cache)
|
||||
|
||||
for _, tt := range t {
|
||||
if err := tt.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
|
||||
@@ -118,7 +123,9 @@ func TagGalleries(ctx context.Context, p *models.Tag, paths []string, aliases []
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := gallery.AddTag(ctx, rw, o, p.ID); err != nil {
|
||||
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
|
||||
return gallery.AddTag(ctx, rw, o, p.ID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type testTagCase struct {
|
||||
@@ -111,7 +112,9 @@ func testTagScenes(t *testing.T, tc testTagCase) {
|
||||
}
|
||||
|
||||
organized := false
|
||||
perPage := models.PerPageAll
|
||||
perPage := 1000
|
||||
sort := "id"
|
||||
direction := models.SortDirectionEnumAsc
|
||||
|
||||
expectedSceneFilter := &models.SceneFilterType{
|
||||
Organized: &organized,
|
||||
@@ -122,7 +125,9 @@ func testTagScenes(t *testing.T, tc testTagCase) {
|
||||
}
|
||||
|
||||
expectedFindFilter := &models.FindFilterType{
|
||||
PerPage: &perPage,
|
||||
PerPage: &perPage,
|
||||
Sort: &sort,
|
||||
Direction: &direction,
|
||||
}
|
||||
|
||||
// if alias provided, then don't find by name
|
||||
@@ -140,13 +145,13 @@ func testTagScenes(t *testing.T, tc testTagCase) {
|
||||
},
|
||||
}
|
||||
|
||||
mockSceneReader.On("Query", testCtx, scene.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
|
||||
mockSceneReader.On("Query", mock.Anything, scene.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
|
||||
Return(mocks.SceneQueryResult(scenes, len(scenes)), nil).Once()
|
||||
}
|
||||
|
||||
for i := range matchingPaths {
|
||||
sceneID := i + 1
|
||||
mockSceneReader.On("UpdatePartial", testCtx, sceneID, models.ScenePartial{
|
||||
mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, models.ScenePartial{
|
||||
TagIDs: &models.UpdateIDs{
|
||||
IDs: []int{tagID},
|
||||
Mode: models.RelationshipUpdateModeAdd,
|
||||
@@ -154,7 +159,11 @@ func testTagScenes(t *testing.T, tc testTagCase) {
|
||||
}).Return(nil, nil).Once()
|
||||
}
|
||||
|
||||
err := TagScenes(testCtx, &tag, nil, aliases, mockSceneReader, nil)
|
||||
tagger := Tagger{
|
||||
TxnManager: &mocks.TxnManager{},
|
||||
}
|
||||
|
||||
err := tagger.TagScenes(testCtx, &tag, nil, aliases, mockSceneReader)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
@@ -204,7 +213,9 @@ func testTagImages(t *testing.T, tc testTagCase) {
|
||||
}
|
||||
|
||||
organized := false
|
||||
perPage := models.PerPageAll
|
||||
perPage := 1000
|
||||
sort := "id"
|
||||
direction := models.SortDirectionEnumAsc
|
||||
|
||||
expectedImageFilter := &models.ImageFilterType{
|
||||
Organized: &organized,
|
||||
@@ -215,7 +226,9 @@ func testTagImages(t *testing.T, tc testTagCase) {
|
||||
}
|
||||
|
||||
expectedFindFilter := &models.FindFilterType{
|
||||
PerPage: &perPage,
|
||||
PerPage: &perPage,
|
||||
Sort: &sort,
|
||||
Direction: &direction,
|
||||
}
|
||||
|
||||
// if alias provided, then don't find by name
|
||||
@@ -233,14 +246,14 @@ func testTagImages(t *testing.T, tc testTagCase) {
|
||||
},
|
||||
}
|
||||
|
||||
mockImageReader.On("Query", testCtx, image.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
|
||||
mockImageReader.On("Query", mock.Anything, image.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
|
||||
Return(mocks.ImageQueryResult(images, len(images)), nil).Once()
|
||||
}
|
||||
|
||||
for i := range matchingPaths {
|
||||
imageID := i + 1
|
||||
|
||||
mockImageReader.On("UpdatePartial", testCtx, imageID, models.ImagePartial{
|
||||
mockImageReader.On("UpdatePartial", mock.Anything, imageID, models.ImagePartial{
|
||||
TagIDs: &models.UpdateIDs{
|
||||
IDs: []int{tagID},
|
||||
Mode: models.RelationshipUpdateModeAdd,
|
||||
@@ -248,7 +261,11 @@ func testTagImages(t *testing.T, tc testTagCase) {
|
||||
}).Return(nil, nil).Once()
|
||||
}
|
||||
|
||||
err := TagImages(testCtx, &tag, nil, aliases, mockImageReader, nil)
|
||||
tagger := Tagger{
|
||||
TxnManager: &mocks.TxnManager{},
|
||||
}
|
||||
|
||||
err := tagger.TagImages(testCtx, &tag, nil, aliases, mockImageReader)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
@@ -299,7 +316,9 @@ func testTagGalleries(t *testing.T, tc testTagCase) {
|
||||
}
|
||||
|
||||
organized := false
|
||||
perPage := models.PerPageAll
|
||||
perPage := 1000
|
||||
sort := "id"
|
||||
direction := models.SortDirectionEnumAsc
|
||||
|
||||
expectedGalleryFilter := &models.GalleryFilterType{
|
||||
Organized: &organized,
|
||||
@@ -310,7 +329,9 @@ func testTagGalleries(t *testing.T, tc testTagCase) {
|
||||
}
|
||||
|
||||
expectedFindFilter := &models.FindFilterType{
|
||||
PerPage: &perPage,
|
||||
PerPage: &perPage,
|
||||
Sort: &sort,
|
||||
Direction: &direction,
|
||||
}
|
||||
|
||||
// if alias provided, then don't find by name
|
||||
@@ -328,13 +349,13 @@ func testTagGalleries(t *testing.T, tc testTagCase) {
|
||||
},
|
||||
}
|
||||
|
||||
mockGalleryReader.On("Query", testCtx, expectedAliasFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
|
||||
mockGalleryReader.On("Query", mock.Anything, expectedAliasFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
|
||||
}
|
||||
|
||||
for i := range matchingPaths {
|
||||
galleryID := i + 1
|
||||
|
||||
mockGalleryReader.On("UpdatePartial", testCtx, galleryID, models.GalleryPartial{
|
||||
mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, models.GalleryPartial{
|
||||
TagIDs: &models.UpdateIDs{
|
||||
IDs: []int{tagID},
|
||||
Mode: models.RelationshipUpdateModeAdd,
|
||||
@@ -343,7 +364,11 @@ func testTagGalleries(t *testing.T, tc testTagCase) {
|
||||
|
||||
}
|
||||
|
||||
err := TagGalleries(testCtx, &tag, nil, aliases, mockGalleryReader, nil)
|
||||
tagger := Tagger{
|
||||
TxnManager: &mocks.TxnManager{},
|
||||
}
|
||||
|
||||
err := tagger.TagGalleries(testCtx, &tag, nil, aliases, mockGalleryReader)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
|
||||
@@ -23,8 +23,14 @@ import (
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
type Tagger struct {
|
||||
TxnManager txn.Manager
|
||||
Cache *match.Cache
|
||||
}
|
||||
|
||||
type tagger struct {
|
||||
ID int
|
||||
Type string
|
||||
@@ -58,11 +64,11 @@ func (t *tagger) tagPerformers(ctx context.Context, performerReader match.Perfor
|
||||
added, err := addFunc(t.ID, p.ID)
|
||||
|
||||
if err != nil {
|
||||
return t.addError("performer", p.Name.String, err)
|
||||
return t.addError("performer", p.Name, err)
|
||||
}
|
||||
|
||||
if added {
|
||||
t.addLog("performer", p.Name.String)
|
||||
t.addLog("performer", p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,12 +118,7 @@ func (t *tagger) tagTags(ctx context.Context, tagReader match.TagAutoTagQueryer,
|
||||
}
|
||||
|
||||
func (t *tagger) tagScenes(ctx context.Context, paths []string, sceneReader scene.Queryer, addFunc addSceneLinkFunc) error {
|
||||
others, err := match.PathToScenes(ctx, t.Name, paths, sceneReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range others {
|
||||
return match.PathToScenesFn(ctx, t.Name, paths, sceneReader, func(ctx context.Context, p *models.Scene) error {
|
||||
added, err := addFunc(p)
|
||||
|
||||
if err != nil {
|
||||
@@ -127,18 +128,13 @@ func (t *tagger) tagScenes(ctx context.Context, paths []string, sceneReader scen
|
||||
if added {
|
||||
t.addLog("scene", p.DisplayName())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (t *tagger) tagImages(ctx context.Context, paths []string, imageReader image.Queryer, addFunc addImageLinkFunc) error {
|
||||
others, err := match.PathToImages(ctx, t.Name, paths, imageReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range others {
|
||||
return match.PathToImagesFn(ctx, t.Name, paths, imageReader, func(ctx context.Context, p *models.Image) error {
|
||||
added, err := addFunc(p)
|
||||
|
||||
if err != nil {
|
||||
@@ -148,18 +144,13 @@ func (t *tagger) tagImages(ctx context.Context, paths []string, imageReader imag
|
||||
if added {
|
||||
t.addLog("image", p.DisplayName())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (t *tagger) tagGalleries(ctx context.Context, paths []string, galleryReader gallery.Queryer, addFunc addGalleryLinkFunc) error {
|
||||
others, err := match.PathToGalleries(ctx, t.Name, paths, galleryReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range others {
|
||||
return match.PathToGalleriesFn(ctx, t.Name, paths, galleryReader, func(ctx context.Context, p *models.Gallery) error {
|
||||
added, err := addFunc(p)
|
||||
|
||||
if err != nil {
|
||||
@@ -169,7 +160,7 @@ func (t *tagger) tagGalleries(ctx context.Context, paths []string, galleryReader
|
||||
if added {
|
||||
t.addLog("gallery", p.DisplayName())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ func (me *contentDirectoryService) handleBrowseMetadata(obj object, host string)
|
||||
} else {
|
||||
var scene *models.Scene
|
||||
|
||||
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
scene, err = me.repository.SceneFinder.Find(ctx, sceneID)
|
||||
if scene != nil {
|
||||
err = scene.LoadPrimaryFile(ctx, me.repository.FileFinder)
|
||||
@@ -443,7 +443,7 @@ func getRootObjects() []interface{} {
|
||||
func (me *contentDirectoryService) getVideos(sceneFilter *models.SceneFilterType, parentID string, host string) []interface{} {
|
||||
var objs []interface{}
|
||||
|
||||
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
sort := "title"
|
||||
findFilter := &models.FindFilterType{
|
||||
PerPage: &pageSize,
|
||||
@@ -486,7 +486,7 @@ func (me *contentDirectoryService) getVideos(sceneFilter *models.SceneFilterType
|
||||
func (me *contentDirectoryService) getPageVideos(sceneFilter *models.SceneFilterType, parentID string, page int, host string) []interface{} {
|
||||
var objs []interface{}
|
||||
|
||||
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
pager := scenePager{
|
||||
sceneFilter: sceneFilter,
|
||||
parentID: parentID,
|
||||
@@ -527,7 +527,7 @@ func (me *contentDirectoryService) getAllScenes(host string) []interface{} {
|
||||
func (me *contentDirectoryService) getStudios() []interface{} {
|
||||
var objs []interface{}
|
||||
|
||||
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
studios, err := me.repository.StudioFinder.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -566,7 +566,7 @@ func (me *contentDirectoryService) getStudioScenes(paths []string, host string)
|
||||
func (me *contentDirectoryService) getTags() []interface{} {
|
||||
var objs []interface{}
|
||||
|
||||
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
tags, err := me.repository.TagFinder.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -605,14 +605,14 @@ func (me *contentDirectoryService) getTagScenes(paths []string, host string) []i
|
||||
func (me *contentDirectoryService) getPerformers() []interface{} {
|
||||
var objs []interface{}
|
||||
|
||||
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
performers, err := me.repository.PerformerFinder.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range performers {
|
||||
objs = append(objs, makeStorageFolder("performers/"+strconv.Itoa(s.ID), s.Name.String, "performers"))
|
||||
objs = append(objs, makeStorageFolder("performers/"+strconv.Itoa(s.ID), s.Name, "performers"))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -644,7 +644,7 @@ func (me *contentDirectoryService) getPerformerScenes(paths []string, host strin
|
||||
func (me *contentDirectoryService) getMovies() []interface{} {
|
||||
var objs []interface{}
|
||||
|
||||
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||
movies, err := me.repository.MovieFinder.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user