mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd7d4ac7ff | ||
|
|
5a6504b4ba | ||
|
|
f8a93789bb | ||
|
|
82cbeff9b5 | ||
|
|
f32d60f208 | ||
|
|
3e9bd8507f | ||
|
|
6ee7e6112b | ||
|
|
9bd36408ee | ||
|
|
0cdea209bb | ||
|
|
e1782d094d | ||
|
|
d258976358 | ||
|
|
afda6decf2 | ||
|
|
a1bd7cf817 | ||
|
|
23b4d4f1e0 | ||
|
|
d0d0d1e11f | ||
|
|
e304d981d0 | ||
|
|
e8af3c8e98 | ||
|
|
8c922ed9e1 | ||
|
|
c9501ef881 | ||
|
|
26c3873122 | ||
|
|
2ef2d89b06 | ||
|
|
43a9df8621 | ||
|
|
7a9cb548ab | ||
|
|
74ddfa47e9 | ||
|
|
d37de0e49b | ||
|
|
eca5838ce0 | ||
|
|
ccb1b7c3c4 | ||
|
|
d4ef182871 | ||
|
|
d24b52ae7f | ||
|
|
46e1715a59 | ||
|
|
5ebd3b3568 | ||
|
|
54461aa140 | ||
|
|
1659c8f185 | ||
|
|
599deb71b6 | ||
|
|
413311711f | ||
|
|
d1018b4c5d | ||
|
|
b78771dbcd | ||
|
|
fc1fc20df4 | ||
|
|
b915428f06 | ||
|
|
11be56cc42 | ||
|
|
910ff27730 | ||
|
|
11a1e49292 | ||
|
|
0e7c58a5a6 | ||
|
|
a8140c11ec | ||
|
|
0dcd58763f | ||
|
|
987fa80786 | ||
|
|
d95ef4059a | ||
|
|
13a24a634d | ||
|
|
b0a34a3dc0 | ||
|
|
343660c378 | ||
|
|
611a1e7854 | ||
|
|
2ea35c4dbc | ||
|
|
f9e11813f0 | ||
|
|
049a1b15c3 | ||
|
|
a0f33e3dab | ||
|
|
61f4d8bd12 | ||
|
|
959f2531fd | ||
|
|
65b416a2d9 | ||
|
|
222475df82 | ||
|
|
4dd4c3c658 | ||
|
|
72779e618d | ||
|
|
4715c5ebb2 | ||
|
|
d96558704a | ||
|
|
795af64e8e | ||
|
|
9621213424 | ||
|
|
cc6673f276 | ||
|
|
2b8c2534dd | ||
|
|
552f86586a | ||
|
|
c4d7a7ab2c | ||
|
|
298f3d4e19 | ||
|
|
87bdbb2058 | ||
|
|
b99d16b712 | ||
|
|
24984da16e | ||
|
|
2b8718100b | ||
|
|
06d8353f4f | ||
|
|
939bb422d1 | ||
|
|
339b9fcc16 | ||
|
|
a83dfff5ff | ||
|
|
21baa23fc5 | ||
|
|
b6714fafba | ||
|
|
a9ab1fcca7 | ||
|
|
5e0f27bed2 | ||
|
|
789de2d5f6 | ||
|
|
2fd7141f0f | ||
|
|
bdf705fe7c | ||
|
|
2ec948a836 | ||
|
|
7605eec6da | ||
|
|
8eb069054e | ||
|
|
b6808dc714 | ||
|
|
e5af37efbc | ||
|
|
409f8fc70c | ||
|
|
90dfaf668b | ||
|
|
bc261f789a | ||
|
|
9552273478 | ||
|
|
33f2ebf2a3 | ||
|
|
40bcb4baa5 | ||
|
|
479e716385 | ||
|
|
06c9d6f554 | ||
|
|
1f0f5eb49c | ||
|
|
c109a58231 |
34
.github/workflows/build.yml
vendored
34
.github/workflows/build.yml
vendored
@@ -12,7 +12,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:7
|
||||
COMPILER_IMAGE: stashapp/compiler:8
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -84,13 +84,13 @@ jobs:
|
||||
|
||||
- name: Compile for all supported platforms
|
||||
run: |
|
||||
docker exec -t build /bin/bash -c "make cross-compile-windows"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-macos-intel"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-macos-applesilicon"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-linux"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-linux-arm64v8"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-linux-arm32v7"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-linux-arm32v6"
|
||||
docker exec -t build /bin/bash -c "make build-cc-windows"
|
||||
docker exec -t build /bin/bash -c "make build-cc-macos"
|
||||
docker exec -t build /bin/bash -c "make build-cc-linux"
|
||||
docker exec -t build /bin/bash -c "make build-cc-linux-arm64v8"
|
||||
docker exec -t build /bin/bash -c "make build-cc-linux-arm32v7"
|
||||
docker exec -t build /bin/bash -c "make build-cc-linux-arm32v6"
|
||||
docker exec -t build /bin/bash -c "make build-cc-freebsd"
|
||||
|
||||
- name: Cleanup build container
|
||||
run: docker rm -f -v build
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
|
||||
sha1sum dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
|
||||
sha1sum dist/Stash.app.zip dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
|
||||
echo "STASH_VERSION=$(git describe --tags --exclude latest_develop)" >> $GITHUB_ENV
|
||||
echo "RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_ENV
|
||||
|
||||
@@ -110,13 +110,13 @@ jobs:
|
||||
name: stash-win.exe
|
||||
path: dist/stash-win.exe
|
||||
|
||||
- name: Upload OSX binary
|
||||
- name: Upload macOS binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: stash-macos-intel
|
||||
path: dist/stash-macos-intel
|
||||
name: stash-macos
|
||||
path: dist/stash-macos
|
||||
|
||||
- name: Upload Linux binary
|
||||
# only upload binaries for pull requests
|
||||
@@ -139,13 +139,14 @@ jobs:
|
||||
automatic_release_tag: latest_develop
|
||||
title: "${{ env.STASH_VERSION }}: Latest development build"
|
||||
files: |
|
||||
dist/stash-macos-intel
|
||||
dist/stash-macos-applesilicon
|
||||
dist/Stash.app.zip
|
||||
dist/stash-macos
|
||||
dist/stash-win.exe
|
||||
dist/stash-linux
|
||||
dist/stash-linux-arm64v8
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-linux-arm32v6
|
||||
dist/stash-freebsd
|
||||
CHECKSUMS_SHA1
|
||||
|
||||
- name: Master release
|
||||
@@ -157,13 +158,14 @@ jobs:
|
||||
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
allow_override: true
|
||||
files: |
|
||||
dist/stash-macos-intel
|
||||
dist/stash-macos-applesilicon
|
||||
dist/Stash.app.zip
|
||||
dist/stash-macos
|
||||
dist/stash-win.exe
|
||||
dist/stash-linux
|
||||
dist/stash-linux-arm64v8
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-linux-arm32v6
|
||||
dist/stash-freebsd
|
||||
CHECKSUMS_SHA1
|
||||
gzip: false
|
||||
|
||||
|
||||
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:7
|
||||
COMPILER_IMAGE: stashapp/compiler:8
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -21,11 +21,6 @@ vendor
|
||||
# GraphQL generated output
|
||||
internal/api/generated_*.go
|
||||
|
||||
####
|
||||
# Jetbrains
|
||||
####
|
||||
|
||||
|
||||
####
|
||||
# Visual Studio
|
||||
####
|
||||
@@ -52,9 +47,6 @@ internal/api/generated_*.go
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Goland Junk
|
||||
pkg/pkg
|
||||
|
||||
####
|
||||
# Random
|
||||
####
|
||||
@@ -64,6 +56,7 @@ node_modules
|
||||
*.db
|
||||
|
||||
/stash
|
||||
/Stash.app
|
||||
/phasher
|
||||
dist
|
||||
.DS_Store
|
||||
|
||||
274
Makefile
274
Makefile
@@ -9,9 +9,11 @@ endif
|
||||
ifdef IS_WIN_SHELL
|
||||
RM := del /s /q
|
||||
RMDIR := rmdir /s /q
|
||||
NOOP := @@
|
||||
else
|
||||
RM := rm -f
|
||||
RMDIR := rm -rf
|
||||
NOOP := @:
|
||||
endif
|
||||
|
||||
# set LDFLAGS environment variable to any extra ldflags required
|
||||
@@ -36,7 +38,7 @@ GO_BUILD_FLAGS := $(GO_BUILD_FLAGS)
|
||||
|
||||
# set GO_BUILD_TAGS environment variable to any extra build tags required
|
||||
GO_BUILD_TAGS := $(GO_BUILD_TAGS)
|
||||
GO_BUILD_TAGS += sqlite_stat4
|
||||
GO_BUILD_TAGS += sqlite_stat4 sqlite_math_functions
|
||||
|
||||
# set STASH_NOLEGACY environment variable or uncomment to disable legacy browser support
|
||||
# STASH_NOLEGACY := true
|
||||
@@ -50,29 +52,40 @@ export CGO_ENABLED := 1
|
||||
release: pre-ui generate ui build-release
|
||||
|
||||
# targets to set various build flags
|
||||
# use combinations on the make command-line to configure a build, e.g.:
|
||||
# for a static-pie release build: `make flags-static-pie flags-release stash`
|
||||
# for a static windows debug build: `make flags-static-windows stash`
|
||||
|
||||
# $(NOOP) prevents "nothing to be done" warnings
|
||||
|
||||
.PHONY: flags-release
|
||||
flags-release:
|
||||
flags-release:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -s -w)
|
||||
$(eval GO_BUILD_FLAGS += -trimpath)
|
||||
|
||||
.PHONY: flags-pie
|
||||
flags-pie:
|
||||
$(NOOP)
|
||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||
|
||||
.PHONY: flags-static
|
||||
flags-static:
|
||||
flags-static:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -extldflags=-static)
|
||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
|
||||
|
||||
.PHONY: flags-static-pie
|
||||
flags-static-pie:
|
||||
flags-static-pie:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -extldflags=-static-pie)
|
||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
|
||||
|
||||
# identical to flags-static-pie, but excluding netgo, which is not needed on windows
|
||||
.PHONY: flags-static-windows
|
||||
flags-static-windows:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -extldflags=-static-pie)
|
||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo)
|
||||
@@ -105,166 +118,141 @@ build-flags: build-info
|
||||
stash: build-flags
|
||||
go build $(STASH_OUTPUT) $(BUILD_FLAGS) ./cmd/stash
|
||||
|
||||
.PHONY: stash-release
|
||||
stash-release: flags-release
|
||||
stash-release: flags-pie
|
||||
stash-release: stash
|
||||
|
||||
.PHONY: stash-release-static
|
||||
stash-release-static: flags-release
|
||||
stash-release-static: flags-static-pie
|
||||
stash-release-static: stash
|
||||
|
||||
.PHONY: stash-release-static-windows
|
||||
stash-release-static-windows: flags-release
|
||||
stash-release-static-windows: flags-static-windows
|
||||
stash-release-static-windows: stash
|
||||
|
||||
.PHONY: phasher
|
||||
phasher: build-flags
|
||||
go build $(PHASHER_OUTPUT) $(BUILD_FLAGS) ./cmd/phasher
|
||||
|
||||
.PHONY: phasher-release
|
||||
phasher-release: flags-release
|
||||
phasher-release: flags-pie
|
||||
phasher-release: phasher
|
||||
|
||||
.PHONY: phasher-release-static
|
||||
phasher-release-static: flags-release
|
||||
phasher-release-static: flags-static-pie
|
||||
phasher-release-static: phasher
|
||||
|
||||
.PHONY: phasher-release-static-windows
|
||||
phasher-release-static-windows: flags-release
|
||||
phasher-release-static-windows: flags-static-windows
|
||||
phasher-release-static-windows: phasher
|
||||
|
||||
# builds dynamically-linked debug binaries
|
||||
.PHONY: build
|
||||
build: stash phasher
|
||||
|
||||
# builds dynamically-linked release binaries
|
||||
# builds dynamically-linked PIE release binaries
|
||||
.PHONY: build-release
|
||||
build-release: stash-release phasher-release
|
||||
build-release: flags-release flags-pie build
|
||||
|
||||
# builds statically-linked release binaries
|
||||
.PHONY: build-release-static
|
||||
build-release-static: stash-release-static phasher-release-static
|
||||
# compile and bundle into Stash.app
|
||||
# for when on macOS itself
|
||||
.PHONY: stash-macapp
|
||||
stash-macapp: STASH_OUTPUT := -o stash
|
||||
stash-macapp: flags-release flags-pie stash
|
||||
rm -rf Stash.app
|
||||
cp -R scripts/macos-bundle Stash.app
|
||||
mkdir Stash.app/Contents/MacOS
|
||||
cp stash Stash.app/Contents/MacOS/stash
|
||||
|
||||
# build-release-static, but excluding netgo, which is not needed on windows
|
||||
.PHONY: build-release-static-windows
|
||||
build-release-static-windows: stash-release-static-windows phasher-release-static-windows
|
||||
# build-cc- targets should be run within the compiler docker container
|
||||
|
||||
# cross-compile- targets should be run within the compiler docker container
|
||||
.PHONY: cross-compile-windows
|
||||
cross-compile-windows: export GOOS := windows
|
||||
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: STASH_OUTPUT := -o dist/stash-win.exe
|
||||
cross-compile-windows: PHASHER_OUTPUT := -o dist/phasher-win.exe
|
||||
cross-compile-windows: flags-release
|
||||
cross-compile-windows: flags-static-windows
|
||||
cross-compile-windows: build
|
||||
.PHONY: build-cc-windows
|
||||
build-cc-windows: export GOOS := windows
|
||||
build-cc-windows: export GOARCH := amd64
|
||||
build-cc-windows: export CC := x86_64-w64-mingw32-gcc
|
||||
build-cc-windows: STASH_OUTPUT := -o dist/stash-win.exe
|
||||
build-cc-windows: PHASHER_OUTPUT :=-o dist/phasher-win.exe
|
||||
build-cc-windows: flags-release
|
||||
build-cc-windows: flags-static-windows
|
||||
build-cc-windows: build
|
||||
|
||||
.PHONY: cross-compile-macos-intel
|
||||
cross-compile-macos-intel: export GOOS := darwin
|
||||
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: STASH_OUTPUT := -o dist/stash-macos-intel
|
||||
cross-compile-macos-intel: PHASHER_OUTPUT := -o dist/phasher-macos-intel
|
||||
cross-compile-macos-intel: flags-release
|
||||
# can't use static build for OSX
|
||||
cross-compile-macos-intel: flags-pie
|
||||
cross-compile-macos-intel: build
|
||||
.PHONY: build-cc-macos-intel
|
||||
build-cc-macos-intel: export GOOS := darwin
|
||||
build-cc-macos-intel: export GOARCH := amd64
|
||||
build-cc-macos-intel: export CC := o64-clang
|
||||
build-cc-macos-intel: STASH_OUTPUT := -o dist/stash-macos-intel
|
||||
build-cc-macos-intel: PHASHER_OUTPUT := -o dist/phasher-macos-intel
|
||||
build-cc-macos-intel: flags-release
|
||||
# can't use static build for macOS
|
||||
build-cc-macos-intel: flags-pie
|
||||
build-cc-macos-intel: build
|
||||
|
||||
.PHONY: cross-compile-macos-applesilicon
|
||||
cross-compile-macos-applesilicon: export GOOS := darwin
|
||||
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: STASH_OUTPUT := -o dist/stash-macos-applesilicon
|
||||
cross-compile-macos-applesilicon: PHASHER_OUTPUT := -o dist/phasher-macos-applesilicon
|
||||
cross-compile-macos-applesilicon: flags-release
|
||||
# can't use static build for OSX
|
||||
cross-compile-macos-applesilicon: flags-pie
|
||||
cross-compile-macos-applesilicon: build
|
||||
.PHONY: build-cc-macos-arm
|
||||
build-cc-macos-arm: export GOOS := darwin
|
||||
build-cc-macos-arm: export GOARCH := arm64
|
||||
build-cc-macos-arm: export CC := oa64e-clang
|
||||
build-cc-macos-arm: STASH_OUTPUT := -o dist/stash-macos-arm
|
||||
build-cc-macos-arm: PHASHER_OUTPUT := -o dist/phasher-macos-arm
|
||||
build-cc-macos-arm: flags-release
|
||||
# can't use static build for macOS
|
||||
build-cc-macos-arm: flags-pie
|
||||
build-cc-macos-arm: build
|
||||
|
||||
.PHONY: build-cc-macos
|
||||
build-cc-macos:
|
||||
make build-cc-macos-arm
|
||||
make build-cc-macos-intel
|
||||
|
||||
# Combine into universal binaries
|
||||
lipo -create -output dist/stash-macos dist/stash-macos-intel dist/stash-macos-arm
|
||||
rm dist/stash-macos-intel dist/stash-macos-arm
|
||||
lipo -create -output dist/phasher-macos dist/phasher-macos-intel dist/phasher-macos-arm
|
||||
rm dist/phasher-macos-intel dist/phasher-macos-arm
|
||||
|
||||
.PHONY: cross-compile-macos
|
||||
cross-compile-macos:
|
||||
rm -rf dist/Stash.app dist/Stash-macos.zip
|
||||
make cross-compile-macos-applesilicon
|
||||
make cross-compile-macos-intel
|
||||
# Combine into one universal binary
|
||||
lipo -create -output dist/stash-macos-universal dist/stash-macos-intel dist/stash-macos-applesilicon
|
||||
rm dist/stash-macos-intel dist/stash-macos-applesilicon
|
||||
# Place into bundle and zip up
|
||||
rm -rf dist/Stash.app
|
||||
cp -R scripts/macos-bundle dist/Stash.app
|
||||
mkdir dist/Stash.app/Contents/MacOS
|
||||
mv dist/stash-macos-universal dist/Stash.app/Contents/MacOS/stash
|
||||
cd dist && zip -r Stash-macos.zip Stash.app && cd ..
|
||||
cp dist/stash-macos dist/Stash.app/Contents/MacOS/stash
|
||||
cd dist && rm -f Stash.app.zip && zip -r Stash.app.zip Stash.app
|
||||
rm -rf dist/Stash.app
|
||||
|
||||
.PHONY: cross-compile-freebsd
|
||||
cross-compile-freebsd: export GOOS := freebsd
|
||||
cross-compile-freebsd: export GOARCH := amd64
|
||||
cross-compile-freebsd: STASH_OUTPUT := -o dist/stash-freebsd
|
||||
cross-compile-freebsd: PHASHER_OUTPUT := -o dist/phasher-freebsd
|
||||
cross-compile-freebsd: flags-release
|
||||
cross-compile-freebsd: flags-static-pie
|
||||
cross-compile-freebsd: build
|
||||
.PHONY: build-cc-freebsd
|
||||
build-cc-freebsd: export GOOS := freebsd
|
||||
build-cc-freebsd: export GOARCH := amd64
|
||||
build-cc-freebsd: export CC := clang -target x86_64-unknown-freebsd12.0 --sysroot=/opt/cross-freebsd
|
||||
build-cc-freebsd: STASH_OUTPUT := -o dist/stash-freebsd
|
||||
build-cc-freebsd: PHASHER_OUTPUT := -o dist/phasher-freebsd
|
||||
build-cc-freebsd: flags-release
|
||||
build-cc-freebsd: flags-static-pie
|
||||
build-cc-freebsd: build
|
||||
|
||||
.PHONY: cross-compile-linux
|
||||
cross-compile-linux: export GOOS := linux
|
||||
cross-compile-linux: export GOARCH := amd64
|
||||
cross-compile-linux: STASH_OUTPUT := -o dist/stash-linux
|
||||
cross-compile-linux: PHASHER_OUTPUT := -o dist/phasher-linux
|
||||
cross-compile-linux: flags-release
|
||||
cross-compile-linux: flags-static-pie
|
||||
cross-compile-linux: build
|
||||
.PHONY: build-cc-linux
|
||||
build-cc-linux: export GOOS := linux
|
||||
build-cc-linux: export GOARCH := amd64
|
||||
build-cc-linux: STASH_OUTPUT := -o dist/stash-linux
|
||||
build-cc-linux: PHASHER_OUTPUT := -o dist/phasher-linux
|
||||
build-cc-linux: flags-release
|
||||
build-cc-linux: flags-static-pie
|
||||
build-cc-linux: build
|
||||
|
||||
.PHONY: cross-compile-linux-arm64v8
|
||||
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: STASH_OUTPUT := -o dist/stash-linux-arm64v8
|
||||
cross-compile-linux-arm64v8: PHASHER_OUTPUT := -o dist/phasher-linux-arm64v8
|
||||
cross-compile-linux-arm64v8: flags-release
|
||||
cross-compile-linux-arm64v8: flags-static-pie
|
||||
cross-compile-linux-arm64v8: build
|
||||
.PHONY: build-cc-linux-arm64v8
|
||||
build-cc-linux-arm64v8: export GOOS := linux
|
||||
build-cc-linux-arm64v8: export GOARCH := arm64
|
||||
build-cc-linux-arm64v8: export CC := aarch64-linux-gnu-gcc
|
||||
build-cc-linux-arm64v8: STASH_OUTPUT := -o dist/stash-linux-arm64v8
|
||||
build-cc-linux-arm64v8: PHASHER_OUTPUT := -o dist/phasher-linux-arm64v8
|
||||
build-cc-linux-arm64v8: flags-release
|
||||
build-cc-linux-arm64v8: flags-static-pie
|
||||
build-cc-linux-arm64v8: build
|
||||
|
||||
.PHONY: cross-compile-linux-arm32v7
|
||||
cross-compile-linux-arm32v7: export GOOS := linux
|
||||
cross-compile-linux-arm32v7: export GOARCH := arm
|
||||
cross-compile-linux-arm32v7: export GOARM := 7
|
||||
cross-compile-linux-arm32v7: export CC := arm-linux-gnueabi-gcc -march=armv7-a
|
||||
cross-compile-linux-arm32v7: STASH_OUTPUT := -o dist/stash-linux-arm32v7
|
||||
cross-compile-linux-arm32v7: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v7
|
||||
cross-compile-linux-arm32v7: flags-release
|
||||
cross-compile-linux-arm32v7: flags-static
|
||||
cross-compile-linux-arm32v7: build
|
||||
.PHONY: build-cc-linux-arm32v7
|
||||
build-cc-linux-arm32v7: export GOOS := linux
|
||||
build-cc-linux-arm32v7: export GOARCH := arm
|
||||
build-cc-linux-arm32v7: export GOARM := 7
|
||||
build-cc-linux-arm32v7: export CC := arm-linux-gnueabi-gcc -march=armv7-a
|
||||
build-cc-linux-arm32v7: STASH_OUTPUT := -o dist/stash-linux-arm32v7
|
||||
build-cc-linux-arm32v7: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v7
|
||||
build-cc-linux-arm32v7: flags-release
|
||||
build-cc-linux-arm32v7: flags-static
|
||||
build-cc-linux-arm32v7: build
|
||||
|
||||
.PHONY: cross-compile-linux-arm32v6
|
||||
cross-compile-linux-arm32v6: export GOOS := linux
|
||||
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: STASH_OUTPUT := -o dist/stash-linux-arm32v6
|
||||
cross-compile-linux-arm32v6: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v6
|
||||
cross-compile-linux-arm32v6: flags-release
|
||||
cross-compile-linux-arm32v6: flags-static
|
||||
cross-compile-linux-arm32v6: build
|
||||
.PHONY: build-cc-linux-arm32v6
|
||||
build-cc-linux-arm32v6: export GOOS := linux
|
||||
build-cc-linux-arm32v6: export GOARCH := arm
|
||||
build-cc-linux-arm32v6: export GOARM := 6
|
||||
build-cc-linux-arm32v6: export CC := arm-linux-gnueabi-gcc
|
||||
build-cc-linux-arm32v6: STASH_OUTPUT := -o dist/stash-linux-arm32v6
|
||||
build-cc-linux-arm32v6: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v6
|
||||
build-cc-linux-arm32v6: flags-release
|
||||
build-cc-linux-arm32v6: flags-static
|
||||
build-cc-linux-arm32v6: build
|
||||
|
||||
.PHONY: cross-compile-all
|
||||
cross-compile-all:
|
||||
make cross-compile-windows
|
||||
make cross-compile-macos-intel
|
||||
make cross-compile-macos-applesilicon
|
||||
make cross-compile-linux
|
||||
make cross-compile-linux-arm64v8
|
||||
make cross-compile-linux-arm32v7
|
||||
make cross-compile-linux-arm32v6
|
||||
.PHONY: build-cc-all
|
||||
build-cc-all:
|
||||
make build-cc-windows
|
||||
make build-cc-macos
|
||||
make build-cc-linux
|
||||
make build-cc-linux-arm64v8
|
||||
make build-cc-linux-arm32v7
|
||||
make build-cc-linux-arm32v6
|
||||
make build-cc-freebsd
|
||||
|
||||
.PHONY: touch-ui
|
||||
touch-ui:
|
||||
@@ -360,14 +348,6 @@ endif
|
||||
ui: ui-env
|
||||
cd ui/v2.5 && yarn build
|
||||
|
||||
.PHONY: ui-nolegacy
|
||||
ui-nolegacy: STASH_NOLEGACY := true
|
||||
ui-nolegacy: ui
|
||||
|
||||
.PHONY: ui-sourcemaps
|
||||
ui-sourcemaps: STASH_SOURCEMAPS := true
|
||||
ui-sourcemaps: ui
|
||||
|
||||
.PHONY: ui-start
|
||||
ui-start: ui-env
|
||||
cd ui/v2.5 && yarn start --host
|
||||
|
||||
19
README.md
19
README.md
@@ -24,15 +24,22 @@ For further information you can consult the [documentation](https://docs.stashap
|
||||
|
||||
# Installing Stash
|
||||
|
||||
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> MacOS| <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
|
||||
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS | <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
|
||||
:---:|:---:|:---:|:---:
|
||||
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release (Apple Silicon)](https://github.com/stashapp/stash/releases/latest/download/stash-macos-applesilicon) <br /> <sup><sub>[Development Preview (Apple Silicon)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-macos-applesilicon)</sub></sup> <br />[Latest Release (Intel)](https://github.com/stashapp/stash/releases/latest/download/stash-macos-intel) <br /> <sup><sub>[Development Preview (Intel)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-macos-intel)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub> [Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
|
||||
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release](https://github.com/stashapp/stash/releases/latest/download/Stash.app.zip) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/Stash.app.zip)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub>[Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
|
||||
|
||||
Download links for other platforms and architectures are available on the [Releases page](https://github.com/stashapp/stash/releases).
|
||||
|
||||
## 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.
|
||||
#### 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.
|
||||
|
||||
#### Windows/macOS Users: Security Prompt
|
||||
|
||||
On Windows or macOS, running the app might present a security prompt since the binary isn't yet signed.
|
||||
|
||||
On Windows, bypass this by clicking "more info" and then the "run anyway" button. On macOS, Control+Click the app, click "Open", and then "Open" again.
|
||||
|
||||
#### 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.
|
||||
|
||||
# Usage
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -66,13 +65,13 @@ func main() {
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
fmt.Fprintln(os.Stderr, "Files will be processed sequentially! Consier using GNU Parallel.")
|
||||
fmt.Fprintln(os.Stderr, "Files will be processed sequentially! If required, use e.g. GNU Parallel to run concurrently.")
|
||||
fmt.Fprintf(os.Stderr, "Example: parallel %v ::: *.mp4\n", os.Args[0])
|
||||
}
|
||||
|
||||
ffmpegPath, ffprobePath := ffmpeg.GetPaths(nil)
|
||||
encoder := ffmpeg.NewEncoder(ffmpegPath)
|
||||
encoder.InitHWSupport(context.TODO())
|
||||
// don't need to InitHWSupport, phashing doesn't use hw acceleration
|
||||
ffprobe := ffmpeg.FFProbe(ffprobePath)
|
||||
|
||||
for _, item := range args {
|
||||
|
||||
@@ -2,67 +2,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/stashapp/stash/internal/api"
|
||||
"github.com/stashapp/stash/internal/build"
|
||||
"github.com/stashapp/stash/internal/desktop"
|
||||
"github.com/stashapp/stash/internal/log"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/ui"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
var exitCode = 0
|
||||
|
||||
func main() {
|
||||
defer recoverPanic()
|
||||
|
||||
_, err := manager.Initialize()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer recoverPanic()
|
||||
if err := api.Start(); err != nil {
|
||||
handleError(err)
|
||||
} else {
|
||||
manager.GetInstance().Shutdown(0)
|
||||
defer func() {
|
||||
if exitCode != 0 {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
}()
|
||||
|
||||
go handleSignals()
|
||||
desktop.Start(manager.GetInstance(), &ui.FaviconProvider)
|
||||
defer recoverPanic()
|
||||
|
||||
blockForever()
|
||||
helpFlag := false
|
||||
pflag.BoolVarP(&helpFlag, "help", "h", false, "show this help text and exit")
|
||||
|
||||
versionFlag := false
|
||||
pflag.BoolVarP(&versionFlag, "version", "v", false, "show version number and exit")
|
||||
|
||||
cpuProfilePath := ""
|
||||
pflag.StringVar(&cpuProfilePath, "cpuprofile", "", "write cpu profile to file")
|
||||
|
||||
pflag.Parse()
|
||||
|
||||
if helpFlag {
|
||||
pflag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
if versionFlag {
|
||||
fmt.Println(build.VersionString())
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Initialize()
|
||||
if err != nil {
|
||||
exitError(fmt.Errorf("config initialization error: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
l := initLog(cfg)
|
||||
|
||||
if cpuProfilePath != "" {
|
||||
if err := initProfiling(cpuProfilePath); err != nil {
|
||||
exitError(err)
|
||||
return
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
mgr, err := manager.Initialize(cfg, l)
|
||||
if err != nil {
|
||||
exitError(fmt.Errorf("manager initialization error: %w", err))
|
||||
return
|
||||
}
|
||||
defer mgr.Shutdown()
|
||||
|
||||
server, err := api.Initialize()
|
||||
if err != nil {
|
||||
exitError(fmt.Errorf("api initialization error: %w", err))
|
||||
return
|
||||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
exit := make(chan int)
|
||||
|
||||
go func() {
|
||||
err := server.Start()
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
exitError(fmt.Errorf("http server error: %w", err))
|
||||
exit <- 1
|
||||
}
|
||||
}()
|
||||
|
||||
go handleSignals(exit)
|
||||
desktop.Start(exit, &ui.FaviconProvider)
|
||||
|
||||
exitCode = <-exit
|
||||
}
|
||||
|
||||
func initLog(cfg *config.Config) *log.Logger {
|
||||
l := log.NewLogger()
|
||||
l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel())
|
||||
logger.Logger = l
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func initProfiling(path string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create CPU profile file: %v", err)
|
||||
}
|
||||
|
||||
if err = pprof.StartCPUProfile(f); err != nil {
|
||||
return fmt.Errorf("could not start CPU profiling: %v", err)
|
||||
}
|
||||
|
||||
logger.Infof("profiling to %s", path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func recoverPanic() {
|
||||
if p := recover(); p != nil {
|
||||
handleError(fmt.Errorf("Panic: %v", p))
|
||||
if err := recover(); err != nil {
|
||||
exitCode = 1
|
||||
logger.Errorf("panic: %v\n%s", err, debug.Stack())
|
||||
if desktop.IsDesktop() {
|
||||
desktop.FatalError(fmt.Errorf("Panic: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleError(err error) {
|
||||
func exitError(err error) {
|
||||
exitCode = 1
|
||||
logger.Error(err)
|
||||
if desktop.IsDesktop() {
|
||||
desktop.FatalError(err)
|
||||
manager.GetInstance().Shutdown(0)
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleSignals() {
|
||||
func handleSignals(exit chan<- int) {
|
||||
// handle signals
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
<-signals
|
||||
manager.GetInstance().Shutdown(0)
|
||||
}
|
||||
|
||||
func blockForever() {
|
||||
select {}
|
||||
exit <- 0
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ COPY --from=frontend /stash /stash/
|
||||
RUN make generate-backend
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN make stash-release
|
||||
RUN make flags-release flags-pie stash
|
||||
|
||||
# Final Runnable Image
|
||||
FROM alpine:latest
|
||||
|
||||
@@ -28,7 +28,7 @@ COPY --from=frontend /stash /stash/
|
||||
RUN make generate-backend
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN make stash-release
|
||||
RUN make flags-release flags-pie stash
|
||||
|
||||
# Final Runnable Image
|
||||
FROM nvidia/cuda:12.0.1-base-ubuntu22.04
|
||||
@@ -48,3 +48,5 @@ ENV NVIDIA_DRIVER_CAPABILITIES=video,utility
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
EXPOSE 9999
|
||||
ENTRYPOINT ["docker-entrypoint.sh", "stash"]
|
||||
|
||||
# vim: ft=dockerfile
|
||||
|
||||
@@ -11,9 +11,14 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-linux-arm32v6; \
|
||||
|
||||
FROM --platform=$TARGETPLATFORM alpine:latest AS app
|
||||
COPY --from=binary /stash /usr/bin/
|
||||
|
||||
# vips version 8.15.0-r0 breaks thumbnail generation on arm32v6
|
||||
# need to use 8.14.3-r0 from alpine 3.18 instead
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps gcc python3-dev musl-dev \
|
||||
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg vips-tools ruby tzdata \
|
||||
&& pip install mechanicalsoup cloudscraper bencoder.pyx \
|
||||
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg ruby tzdata \
|
||||
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.18/community vips=8.14.3-r0 vips-tools=8.14.3-r0 \
|
||||
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx \
|
||||
&& gem install faraday \
|
||||
&& apk del .build-deps
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
@@ -1 +1 @@
|
||||
This dockerfile is used by travis to build the stash image. It must be run after cross-compiling - that is, `stash-linux` must exist in the `dist` directory. This image must be built from the `dist` directory.
|
||||
This Dockerfile is used by CI to build the `stashapp/stash` Docker image. It must be run after cross-compiling - that is, `stash-linux` must exist in the `dist` directory. This image must be built from the `dist` directory.
|
||||
|
||||
@@ -2,66 +2,82 @@ FROM golang:1.19
|
||||
|
||||
LABEL maintainer="https://discord.gg/2TsNFKt"
|
||||
|
||||
# Install tools
|
||||
RUN apt-get update && apt-get install -y apt-transport-https
|
||||
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
RUN apt-get update && apt-get install -y apt-transport-https ca-certificates gnupg
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
|
||||
ADD https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key nodesource.gpg.key
|
||||
RUN cat nodesource.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && rm nodesource.gpg.key
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
|
||||
# prevent caching of the key
|
||||
ADD https://dl.yarnpkg.com/debian/pubkey.gpg yarn.gpg
|
||||
RUN cat yarn.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
rm yarn.gpg
|
||||
RUN cat yarn.gpg | gpg --dearmor -o /etc/apt/keyrings/yarn.gpg && rm yarn.gpg
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y automake autogen cmake \
|
||||
libtool libxml2-dev uuid-dev libssl-dev bash \
|
||||
patch make tar xz-utils bzip2 gzip zlib1g-dev sed cpio \
|
||||
gcc-10-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
|
||||
gcc-arm-linux-gnueabi libc-dev-armel-cross linux-libc-dev-armel-cross \
|
||||
gcc-arm-linux-gnueabihf libc-dev-armhf-cross \
|
||||
gcc-aarch64-linux-gnu libc-dev-arm64-cross \
|
||||
nodejs yarn zip --no-install-recommends || exit 1; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
apt-get install -y --no-install-recommends \
|
||||
git make tar bash nodejs yarn zip \
|
||||
clang llvm-dev cmake patch libxml2-dev uuid-dev libssl-dev xz-utils \
|
||||
bzip2 gzip sed cpio libbz2-dev zlib1g-dev \
|
||||
gcc-mingw-w64 \
|
||||
gcc-arm-linux-gnueabi libc-dev-armel-cross linux-libc-dev-armel-cross \
|
||||
gcc-aarch64-linux-gnu libc-dev-arm64-cross && \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
# Cross compile setup
|
||||
ENV OSX_SDK_VERSION 11.3
|
||||
ENV OSX_SDK_DOWNLOAD_FILE=MacOSX${OSX_SDK_VERSION}.sdk.tar.xz
|
||||
ENV OSX_SDK_DOWNLOAD_URL=https://github.com/phracker/MacOSX-SDKs/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
|
||||
ENV OSX_SDK_SHA=cd4f08a75577145b8f05245a2975f7c81401d75e9535dcffbb879ee1deefcbf4
|
||||
ENV OSX_SDK MacOSX$OSX_SDK_VERSION.sdk
|
||||
ENV OSX_NDK_X86 /usr/local/osx-ndk-x86
|
||||
# FreeBSD cross-compilation setup
|
||||
# https://github.com/smartmontools/docker-build/blob/6b8c92560d17d325310ba02d9f5a4b250cb0764a/Dockerfile#L66
|
||||
ENV FREEBSD_VERSION 12.4
|
||||
ENV FREEBSD_DOWNLOAD_URL http://ftp.plusline.de/FreeBSD/releases/amd64/${FREEBSD_VERSION}-RELEASE/base.txz
|
||||
ENV FREEBSD_SHA 581c7edacfd2fca2bdf5791f667402d22fccd8a5e184635e0cac075564d57aa8
|
||||
|
||||
RUN wget ${OSX_SDK_DOWNLOAD_URL}
|
||||
RUN echo "$OSX_SDK_SHA $OSX_SDK_DOWNLOAD_FILE" | sha256sum -c - || exit 1; \
|
||||
git clone https://github.com/tpoechtrager/osxcross.git; \
|
||||
mv $OSX_SDK_DOWNLOAD_FILE osxcross/tarballs/
|
||||
RUN cd /tmp && \
|
||||
curl -o base.txz $FREEBSD_DOWNLOAD_URL && \
|
||||
echo "$FREEBSD_SHA base.txz" | sha256sum -c - && \
|
||||
mkdir -p /opt/cross-freebsd && \
|
||||
cd /opt/cross-freebsd && \
|
||||
tar -xf /tmp/base.txz ./lib/ ./usr/lib/ ./usr/include/ && \
|
||||
rm -f /tmp/base.txz && \
|
||||
cd /opt/cross-freebsd/usr/lib && \
|
||||
find . -xtype l | xargs ls -l | grep ' /lib/' | awk '{print "ln -sf /opt/cross-freebsd"$11 " " $9}' | /bin/sh && \
|
||||
ln -s libc++.a libstdc++.a && \
|
||||
ln -s libc++.so libstdc++.so
|
||||
|
||||
RUN UNATTENDED=yes SDK_VERSION=${OSX_SDK_VERSION} OSX_VERSION_MIN=10.10 osxcross/build.sh || exit 1;
|
||||
RUN cp osxcross/target/lib/* /usr/lib/ ; \
|
||||
mv osxcross/target $OSX_NDK_X86; \
|
||||
rm -rf osxcross;
|
||||
# macOS cross-compilation setup
|
||||
ENV OSX_SDK_VERSION 11.3
|
||||
ENV OSX_SDK_DOWNLOAD_FILE MacOSX${OSX_SDK_VERSION}.sdk.tar.xz
|
||||
ENV OSX_SDK_DOWNLOAD_URL https://github.com/phracker/MacOSX-SDKs/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
|
||||
ENV OSX_SDK_SHA cd4f08a75577145b8f05245a2975f7c81401d75e9535dcffbb879ee1deefcbf4
|
||||
ENV OSXCROSS_REVISION 5e1b71fcceb23952f3229995edca1b6231525b5b
|
||||
ENV OSXCROSS_DOWNLOAD_URL https://codeload.github.com/tpoechtrager/osxcross/tar.gz/${OSXCROSS_REVISION}
|
||||
ENV OSXCROSS_SHA d3f771bbc20612fea577b18a71be3af2eb5ad2dd44624196cf55de866d008647
|
||||
|
||||
ENV PATH $OSX_NDK_X86/bin:$PATH
|
||||
RUN cd /tmp && \
|
||||
curl -o osxcross.tar.gz $OSXCROSS_DOWNLOAD_URL && \
|
||||
echo "$OSXCROSS_SHA osxcross.tar.gz" | sha256sum -c - && \
|
||||
mkdir osxcross && \
|
||||
tar --strip=1 -C osxcross -xf osxcross.tar.gz && \
|
||||
rm -f osxcross.tar.gz && \
|
||||
curl -Lo $OSX_SDK_DOWNLOAD_FILE $OSX_SDK_DOWNLOAD_URL && \
|
||||
echo "$OSX_SDK_SHA $OSX_SDK_DOWNLOAD_FILE" | sha256sum -c - && \
|
||||
mv $OSX_SDK_DOWNLOAD_FILE osxcross/tarballs/ && \
|
||||
UNATTENDED=yes SDK_VERSION=$OSX_SDK_VERSION OSX_VERSION_MIN=10.10 osxcross/build.sh && \
|
||||
cp osxcross/target/lib/* /usr/lib/ && \
|
||||
mv osxcross/target /opt/osx-ndk-x86 && \
|
||||
rm -rf /tmp/osxcross
|
||||
|
||||
RUN mkdir -p /root/.ssh; \
|
||||
chmod 0700 /root/.ssh; \
|
||||
ssh-keyscan github.com > /root/.ssh/known_hosts;
|
||||
ENV PATH /opt/osx-ndk-x86/bin:$PATH
|
||||
|
||||
# Notes for self:
|
||||
RUN mkdir -p /root/.ssh && \
|
||||
chmod 0700 /root/.ssh && \
|
||||
ssh-keyscan github.com > /root/.ssh/known_hosts
|
||||
|
||||
# ignore "dubious ownership" errors
|
||||
RUN git config --global safe.directory '*'
|
||||
|
||||
# To test locally:
|
||||
# make generate
|
||||
# make ui
|
||||
# cd docker/compiler
|
||||
# make build
|
||||
# docker run -it -v /PATH_TO_STASH:/go/stash stashapp/compiler:latest /bin/bash
|
||||
# cd stash
|
||||
# make cross-compile-all
|
||||
# docker run --rm -v /PATH_TO_STASH:/stash -w /stash -i -t stashapp/compiler:latest make build-cc-all
|
||||
# # binaries will show up in /dist
|
||||
|
||||
# Windows:
|
||||
# GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go build -ldflags "-extldflags '-static'" -tags extended
|
||||
|
||||
# Darwin
|
||||
# CC=o64-clang CXX=o64-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -tags extended
|
||||
# env goreleaser --config=goreleaser-extended.yml --skip-publish --skip-validate --rm-dist --release-notes=temp/0.48-relnotes-ready.md
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
user=stashapp
|
||||
repo=compiler
|
||||
version=7
|
||||
version=8
|
||||
|
||||
latest:
|
||||
docker build -t ${user}/${repo}:latest .
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Modified from https://github.com/bep/dockerfiles/tree/master/ci-goreleaser
|
||||
|
||||
When the dockerfile is changed, the version number should be incremented in the Makefile and the new version tag should be pushed to docker hub. The `scripts/cross-compile.sh` script should also be updated to use the new version number tag, and the github workflow files need to be updated to pull the correct image tag.
|
||||
When the Dockerfile is changed, the version number should be incremented in the Makefile and the new version tag should be pushed to Docker Hub. The GitHub workflow files also need to be updated to pull the correct image tag.
|
||||
|
||||
@@ -9,11 +9,11 @@ https://docs.docker.com/engine/install/
|
||||
|
||||
### Get the docker-compose.yml file
|
||||
|
||||
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml) in the repository, or if you have curl, you can make your Linux console do it for you:
|
||||
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/develop/docker/production/docker-compose.yml) in the repository, or if you have curl, you can make your Linux console do it for you:
|
||||
|
||||
```
|
||||
mkdir stashapp && cd stashapp
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/develop/docker/production/docker-compose.yml
|
||||
```
|
||||
|
||||
Once you have that file where you want it, modify the settings as you please, and then run:
|
||||
|
||||
@@ -32,9 +32,24 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MinGW. For examp
|
||||
|
||||
#### Ubuntu
|
||||
|
||||
1. Install dependencies: `sudo apt-get install golang git gcc nodejs ffmpeg -y`
|
||||
2. Enable corepack in Node.js: `corepack enable`
|
||||
3. Install yarn: `corepack prepare yarn@stable --activate`
|
||||
1. Install dependencies: `sudo apt-get install golang git yarnpkg gcc nodejs ffmpeg -y`
|
||||
|
||||
### OpenBSD
|
||||
|
||||
1. Install dependencies `doas pkg_add gmake go git yarn node cmake`
|
||||
2. Compile a custom ffmpeg from ports. The default ffmpeg in OpenBSD's packages is not compiled with WebP support, which is required by Stash.
|
||||
- If you've already installed ffmpeg, uninstall it: `doas pkg_delete ffmpeg`
|
||||
- If you haven't already, [fetch the ports tree and verify](https://www.openbsd.org/faq/ports/ports.html#PortsFetch).
|
||||
- Find the ffmpeg port in `/usr/ports/graphics/ffmpeg`, and patch the Makefile to include libwebp
|
||||
- Add `webp` to `WANTLIB`
|
||||
- Add `graphics/libwebp` to the list in `LIB_DEPENDS`
|
||||
- Add `-lwebp -lwebpdecoder -lwebpdemux -lwebpmux` to `LIBavcodec_EXTRALIBS`
|
||||
- Add `--enable-libweb` to the list in `CONFIGURE_ARGS`
|
||||
- If you've already built ffmpeg from ports before, you may need to also increment `REVISION`
|
||||
- Run `doas make install`
|
||||
- Follow the instructions below to build a release, but replace the final step `make build-release` with `gmake flags-release stash`, to [avoid the PIE buildmode](https://github.com/golang/go/issues/59866).
|
||||
|
||||
NOTE: The `make` command in OpenBSD will be `gmake`. For example, `make pre-ui` will be `gmake pre-ui`.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -43,11 +58,10 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MinGW. For examp
|
||||
* `make generate-stash-box-client` - Generate Go files for the Stash-box client code.
|
||||
* `make ui` - Builds the UI. Requires `make pre-ui` to have been run.
|
||||
* `make stash` - Builds the `stash` binary (make sure to build the UI as well... see below)
|
||||
* `make stash-release` - Builds a release version the `stash` binary, with debug information removed
|
||||
* `make stash-macapp` - Builds the `Stash.app` macOS app (only works when on macOS, for cross-compilation see below)
|
||||
* `make phasher` - Builds the `phasher` binary
|
||||
* `make phasher-release` - Builds a release version the `phasher` binary, with debug information removed
|
||||
* `make build` - Builds both the `stash` and `phasher` binaries
|
||||
* `make build-release` - Builds release versions of both the `stash` and `phasher` binaries
|
||||
* `make build` - Builds both the `stash` and `phasher` binaries, alias for `make stash phasher`
|
||||
* `make build-release` - Builds release versions (debug information removed) of both the `stash` and `phasher` binaries, alias for `make flags-release flags-pie build`
|
||||
* `make docker-build` - Locally builds and tags a complete 'stash/build' docker image
|
||||
* `make docker-cuda-build` - Locally builds and tags a complete 'stash/cuda-build' docker image
|
||||
* `make validate` - Runs all of the tests and checks required to submit a PR
|
||||
@@ -57,7 +71,15 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MinGW. For examp
|
||||
* `make fmt-ui` - Formats the UI source code
|
||||
* `make server-start` - Runs a development stash server in the `.local` directory
|
||||
* `make server-clean` - Removes the `.local` directory and all of its contents
|
||||
* `make ui-start` - Runs the UI in development mode. Requires a running Stash server to connect to. The server port can be changed from the default of `9999` using the environment variable `VITE_APP_PLATFORM_PORT`. The UI runs on port `3000` or the next available port.
|
||||
* `make ui-start` - Runs the UI in development mode. Requires a running Stash server to connect to - the server URL can be changed from the default of `http://localhost:9999` using the environment variable `VITE_APP_PLATFORM_URL`, but keep in mind that authentication cannot be used since the session authorization cookie cannot be sent cross-origin. The UI runs on port `3000` or the next available port.
|
||||
|
||||
When building, you can optionally prepend `flags-*` targets to the target list in your `make` command to use different build flags:
|
||||
|
||||
* `flags-release` (e.g. `make flags-release stash`) - Remove debug information from the binary.
|
||||
* `flags-pie` (e.g. `make flags-pie build`) - Build a PIE (Position Independent Executable) binary. This provides increased security, but it is unsupported on some systems (notably 32-bit ARM and OpenBSD).
|
||||
* `flags-static` (e.g. `make flags-static phasher`) - Build a statically linked binary (the default is a dynamically linked binary).
|
||||
* `flags-static-pie` (e.g. `make flags-static-pie stash`) - Build a statically linked PIE binary (using `flags-static` and `flags-pie` separately will not work).
|
||||
* `flags-static-windows` (e.g. `make flags-static-windows build`) - Identical to `flags-static-pie`, but does not enable the `netgo` build tag, which is not needed for static builds on Windows.
|
||||
|
||||
## Local development quickstart
|
||||
|
||||
@@ -95,13 +117,19 @@ Simply run `make` or `make release`, or equivalently:
|
||||
3. Run `make ui` to build the frontend
|
||||
4. Run `make build-release` to build a release executable for your current platform
|
||||
|
||||
## Cross compiling
|
||||
## Cross-compiling
|
||||
|
||||
This project uses a modification of the [CI-GoReleaser](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment
|
||||
where the app can be cross-compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following
|
||||
command to open a bash shell to the container to poke around:
|
||||
This project uses a modification of the [CI-GoReleaser](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) Docker container for cross-compilation, defined in `docker/compiler/Dockerfile`.
|
||||
|
||||
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashapp/compiler:latest /bin/bash`
|
||||
To cross-compile the app yourself:
|
||||
|
||||
1. Run `make pre-ui`, `make generate` and `make ui` outside the container, to generate files and build the UI.
|
||||
2. Pull the latest compiler image from Docker Hub: `docker pull stashapp/compiler`
|
||||
3. Run `docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -it stashapp/compiler /bin/bash` to open a shell inside the container.
|
||||
4. From inside the container, run `make build-cc-all` to build for all platforms, or run `make build-cc-{platform}` to build for a specific platform (have a look at the `Makefile` for the list of targets).
|
||||
5. You will find the compiled binaries in `dist/`.
|
||||
|
||||
NOTE: Since the container is run as UID 0 (root), the resulting binaries (and the `dist/` folder itself, if it had to be created) will be owned by root.
|
||||
|
||||
## Profiling
|
||||
|
||||
|
||||
123
go.mod
123
go.mod
@@ -1,67 +1,62 @@
|
||||
module github.com/stashapp/stash
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.2
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552
|
||||
github.com/Yamashou/gqlgenc v0.0.6
|
||||
github.com/anacrolix/dms v1.2.2
|
||||
github.com/antchfx/htmlquery v1.3.0
|
||||
github.com/chromedp/cdproto v0.0.0-20210622022015-fe1827b46b84
|
||||
github.com/chromedp/chromedp v0.7.3
|
||||
github.com/corona10/goimagehash v1.0.3
|
||||
github.com/disintegration/imaging v1.6.0
|
||||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0
|
||||
github.com/golang-migrate/migrate/v4 v4.15.0-beta.1
|
||||
github.com/asticode/go-astisub v0.26.0
|
||||
github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617
|
||||
github.com/chromedp/chromedp v0.9.2
|
||||
github.com/corona10/goimagehash v1.1.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/doug-martin/goqu/v9 v9.18.0
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/httplog v0.3.1
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/gofrs/uuid/v5 v5.0.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
|
||||
github.com/jmoiron/sqlx v1.3.1
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.6
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
github.com/kermieisinthehouse/gosx-notifier v0.1.2
|
||||
github.com/kermieisinthehouse/systray v1.2.4
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cast v1.5.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/tidwall/gjson v1.9.3
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/vektra/mockery/v2 v2.10.0
|
||||
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
|
||||
golang.org/x/image v0.5.0
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/sys v0.5.0
|
||||
golang.org/x/term v0.5.0
|
||||
golang.org/x/text v0.7.0
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552
|
||||
github.com/asticode/go-astisub v0.20.0
|
||||
github.com/doug-martin/goqu/v9 v9.18.0
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/httplog v0.2.1
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/kermieisinthehouse/gosx-notifier v0.1.1
|
||||
github.com/kermieisinthehouse/systray v1.2.4
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/spf13/cast v1.4.1
|
||||
github.com/vearutop/statigz v1.1.6
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.16.0
|
||||
github.com/vearutop/statigz v1.4.0
|
||||
github.com/vektah/dataloaden v0.3.0
|
||||
github.com/vektah/gqlparser/v2 v2.4.2
|
||||
github.com/vektra/mockery/v2 v2.10.0
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
|
||||
github.com/zencoder/go-dash/v3 v3.0.2
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/image v0.12.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/term v0.13.0
|
||||
golang.org/x/text v0.13.0
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -70,46 +65,48 @@ require (
|
||||
github.com/asticode/go-astikit v0.20.0 // indirect
|
||||
github.com/asticode/go-astits v1.8.0 // indirect
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.1.0-rc.5 // indirect
|
||||
github.com/gobwas/ws v1.3.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matryer/moq v0.2.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/zerolog v1.26.1 // indirect
|
||||
github.com/rs/zerolog v1.30.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/cobra v1.4.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cobra v1.7.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/urfave/cli/v2 v2.8.1 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
|
||||
|
||||
go 1.19
|
||||
|
||||
25
gqlgen.yml
25
gqlgen.yml
@@ -14,6 +14,7 @@ resolver:
|
||||
struct_tag: gqlgen
|
||||
|
||||
autobind:
|
||||
- github.com/stashapp/stash/internal/api
|
||||
- github.com/stashapp/stash/pkg/models
|
||||
- github.com/stashapp/stash/pkg/plugin
|
||||
- github.com/stashapp/stash/pkg/scraper
|
||||
@@ -33,36 +34,21 @@ models:
|
||||
model: github.com/99designs/gqlgen/graphql.Int64
|
||||
Timestamp:
|
||||
model: github.com/stashapp/stash/internal/api.Timestamp
|
||||
BoolMap:
|
||||
model: github.com/stashapp/stash/internal/api.BoolMap
|
||||
# define to force resolvers
|
||||
Image:
|
||||
model: github.com/stashapp/stash/pkg/models.Image
|
||||
fields:
|
||||
title:
|
||||
resolver: true
|
||||
# override models, from internal/api/models.go
|
||||
BaseFile:
|
||||
model: github.com/stashapp/stash/internal/api.BaseFile
|
||||
GalleryFile:
|
||||
model: github.com/stashapp/stash/internal/api.GalleryFile
|
||||
fields:
|
||||
# override fingerprint field
|
||||
fingerprints:
|
||||
fieldName: FingerprintSlice
|
||||
VideoFile:
|
||||
fields:
|
||||
# override fingerprint field
|
||||
fingerprints:
|
||||
fieldName: FingerprintSlice
|
||||
# override float fields - #1572
|
||||
duration:
|
||||
fieldName: DurationFinite
|
||||
frame_rate:
|
||||
fieldName: FrameRateFinite
|
||||
ImageFile:
|
||||
fields:
|
||||
# override fingerprint field
|
||||
fingerprints:
|
||||
fieldName: FingerprintSlice
|
||||
# autobind on config causes generation issues
|
||||
BlobsStorageType:
|
||||
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
|
||||
@@ -150,4 +136,9 @@ models:
|
||||
model: github.com/stashapp/stash/pkg/scraper.Source
|
||||
SavedFindFilterType:
|
||||
model: github.com/stashapp/stash/pkg/models.FindFilterType
|
||||
# force resolvers
|
||||
ConfigResult:
|
||||
fields:
|
||||
plugins:
|
||||
resolver: true
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
generatedPath
|
||||
metadataPath
|
||||
scrapersPath
|
||||
pluginsPath
|
||||
cachePath
|
||||
blobsPath
|
||||
blobsStorage
|
||||
@@ -42,9 +43,6 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
excludes
|
||||
imageExcludes
|
||||
customPerformerImageLocation
|
||||
scraperUserAgent
|
||||
scraperCertCheck
|
||||
scraperCDPPath
|
||||
stashBoxes {
|
||||
name
|
||||
endpoint
|
||||
@@ -56,6 +54,17 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
liveTranscodeInputArgs
|
||||
liveTranscodeOutputArgs
|
||||
drawFunscriptHeatmapRange
|
||||
|
||||
scraperPackageSources {
|
||||
name
|
||||
url
|
||||
local_path
|
||||
}
|
||||
pluginPackageSources {
|
||||
name
|
||||
url
|
||||
local_path
|
||||
}
|
||||
}
|
||||
|
||||
fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
@@ -139,8 +148,6 @@ fragment ScraperSourceData on ScraperSource {
|
||||
|
||||
fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
|
||||
scan {
|
||||
useFileMetadata
|
||||
stripFileExtension
|
||||
scanGenerateCovers
|
||||
scanGeneratePreviews
|
||||
scanGenerateImagePreviews
|
||||
@@ -212,4 +219,5 @@ fragment ConfigData on ConfigResult {
|
||||
...ConfigDefaultSettingsData
|
||||
}
|
||||
ui
|
||||
plugins
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
fragment SlimGalleryData on Gallery {
|
||||
id
|
||||
title
|
||||
code
|
||||
date
|
||||
urls
|
||||
details
|
||||
photographer
|
||||
rating100
|
||||
organized
|
||||
files {
|
||||
|
||||
@@ -3,9 +3,11 @@ fragment GalleryData on Gallery {
|
||||
created_at
|
||||
updated_at
|
||||
title
|
||||
code
|
||||
date
|
||||
urls
|
||||
details
|
||||
photographer
|
||||
rating100
|
||||
organized
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
fragment SlimImageData on Image {
|
||||
id
|
||||
title
|
||||
code
|
||||
date
|
||||
urls
|
||||
details
|
||||
photographer
|
||||
rating100
|
||||
organized
|
||||
o_counter
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
fragment ImageData on Image {
|
||||
id
|
||||
title
|
||||
code
|
||||
rating100
|
||||
date
|
||||
urls
|
||||
details
|
||||
photographer
|
||||
organized
|
||||
o_counter
|
||||
created_at
|
||||
|
||||
@@ -20,6 +20,5 @@ fragment MovieData on Movie {
|
||||
scenes {
|
||||
id
|
||||
title
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
8
graphql/documents/data/package.graphql
Normal file
8
graphql/documents/data/package.graphql
Normal file
@@ -0,0 +1,8 @@
|
||||
fragment PackageData on Package {
|
||||
package_id
|
||||
name
|
||||
version
|
||||
date
|
||||
metadata
|
||||
sourceURL
|
||||
}
|
||||
@@ -40,4 +40,5 @@ fragment SelectPerformerData on Performer {
|
||||
name
|
||||
disambiguation
|
||||
alias_list
|
||||
image_path
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
fragment PerformerData on Performer {
|
||||
id
|
||||
checksum
|
||||
name
|
||||
disambiguation
|
||||
url
|
||||
|
||||
@@ -184,8 +184,10 @@ fragment ScrapedSceneData on ScrapedScene {
|
||||
|
||||
fragment ScrapedGalleryData on ScrapedGallery {
|
||||
title
|
||||
code
|
||||
details
|
||||
urls
|
||||
photographer
|
||||
date
|
||||
|
||||
studio {
|
||||
|
||||
@@ -9,3 +9,23 @@ mutation RunPluginTask(
|
||||
) {
|
||||
runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args: $args)
|
||||
}
|
||||
|
||||
mutation ConfigurePlugin($plugin_id: ID!, $input: Map!) {
|
||||
configurePlugin(plugin_id: $plugin_id, input: $input)
|
||||
}
|
||||
|
||||
mutation SetPluginsEnabled($enabledMap: BoolMap!) {
|
||||
setPluginsEnabled(enabledMap: $enabledMap)
|
||||
}
|
||||
|
||||
mutation InstallPluginPackages($packages: [PackageSpecInput!]!) {
|
||||
installPackages(type: Plugin, packages: $packages)
|
||||
}
|
||||
|
||||
mutation UpdatePluginPackages($packages: [PackageSpecInput!]!) {
|
||||
updatePackages(type: Plugin, packages: $packages)
|
||||
}
|
||||
|
||||
mutation UninstallPluginPackages($packages: [PackageSpecInput!]!) {
|
||||
uninstallPackages(type: Plugin, packages: $packages)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
mutation ReloadScrapers {
|
||||
reloadScrapers
|
||||
}
|
||||
|
||||
mutation InstallScraperPackages($packages: [PackageSpecInput!]!) {
|
||||
installPackages(type: Scraper, packages: $packages)
|
||||
}
|
||||
|
||||
mutation UpdateScraperPackages($packages: [PackageSpecInput!]!) {
|
||||
updatePackages(type: Scraper, packages: $packages)
|
||||
}
|
||||
|
||||
mutation UninstallScraperPackages($packages: [PackageSpecInput!]!) {
|
||||
uninstallPackages(type: Scraper, packages: $packages)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ query Plugins {
|
||||
plugins {
|
||||
id
|
||||
name
|
||||
enabled
|
||||
description
|
||||
url
|
||||
version
|
||||
@@ -16,6 +17,20 @@ query Plugins {
|
||||
description
|
||||
hooks
|
||||
}
|
||||
|
||||
settings {
|
||||
name
|
||||
display_name
|
||||
description
|
||||
type
|
||||
}
|
||||
|
||||
requires
|
||||
|
||||
paths {
|
||||
css
|
||||
javascript
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +41,31 @@ query PluginTasks {
|
||||
plugin {
|
||||
id
|
||||
name
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query InstalledPluginPackages {
|
||||
installedPackages(type: Plugin) {
|
||||
...PackageData
|
||||
}
|
||||
}
|
||||
|
||||
query InstalledPluginPackagesStatus {
|
||||
installedPackages(type: Plugin) {
|
||||
...PackageData
|
||||
source_package {
|
||||
...PackageData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query AvailablePluginPackages($source: String!) {
|
||||
availablePackages(source: $source, type: Plugin) {
|
||||
...PackageData
|
||||
requires {
|
||||
package_id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
query ScrapeFreeonesPerformers($q: String!) {
|
||||
scrapeFreeonesPerformerList(query: $q)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
query ListPerformerScrapers {
|
||||
listPerformerScrapers {
|
||||
listScrapers(types: [PERFORMER]) {
|
||||
id
|
||||
name
|
||||
performer {
|
||||
@@ -10,7 +10,7 @@ query ListPerformerScrapers {
|
||||
}
|
||||
|
||||
query ListSceneScrapers {
|
||||
listSceneScrapers {
|
||||
listScrapers(types: [SCENE]) {
|
||||
id
|
||||
name
|
||||
scene {
|
||||
@@ -21,7 +21,7 @@ query ListSceneScrapers {
|
||||
}
|
||||
|
||||
query ListGalleryScrapers {
|
||||
listGalleryScrapers {
|
||||
listScrapers(types: [GALLERY]) {
|
||||
id
|
||||
name
|
||||
gallery {
|
||||
@@ -32,7 +32,7 @@ query ListGalleryScrapers {
|
||||
}
|
||||
|
||||
query ListMovieScrapers {
|
||||
listMovieScrapers {
|
||||
listScrapers(types: [MOVIE]) {
|
||||
id
|
||||
name
|
||||
movie {
|
||||
@@ -119,3 +119,27 @@ query ScrapeMovieURL($url: String!) {
|
||||
...ScrapedMovieData
|
||||
}
|
||||
}
|
||||
|
||||
query InstalledScraperPackages {
|
||||
installedPackages(type: Scraper) {
|
||||
...PackageData
|
||||
}
|
||||
}
|
||||
|
||||
query InstalledScraperPackagesStatus {
|
||||
installedPackages(type: Scraper) {
|
||||
...PackageData
|
||||
source_package {
|
||||
...PackageData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query AvailableScraperPackages($source: String!) {
|
||||
availablePackages(source: $source, type: Scraper) {
|
||||
...PackageData
|
||||
requires {
|
||||
package_id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,8 @@ query SystemStatus {
|
||||
appSchema
|
||||
status
|
||||
configPath
|
||||
os
|
||||
workingDir
|
||||
homeDir
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,14 +109,6 @@ type Query {
|
||||
|
||||
"List available scrapers"
|
||||
listScrapers(types: [ScrapeContentType!]!): [Scraper!]!
|
||||
listPerformerScrapers: [Scraper!]!
|
||||
@deprecated(reason: "Use listScrapers(types: [PERFORMER])")
|
||||
listSceneScrapers: [Scraper!]!
|
||||
@deprecated(reason: "Use listScrapers(types: [SCENE])")
|
||||
listGalleryScrapers: [Scraper!]!
|
||||
@deprecated(reason: "Use listScrapers(types: [GALLERY])")
|
||||
listMovieScrapers: [Scraper!]!
|
||||
@deprecated(reason: "Use listScrapers(types: [MOVIE])")
|
||||
|
||||
"Scrape for a single scene"
|
||||
scrapeSingleScene(
|
||||
@@ -170,33 +162,18 @@ type Query {
|
||||
"Scrapes a complete movie record based on a URL"
|
||||
scrapeMovieURL(url: String!): ScrapedMovie
|
||||
|
||||
"Scrape a list of performers based on name"
|
||||
scrapePerformerList(scraper_id: ID!, query: String!): [ScrapedPerformer!]!
|
||||
@deprecated(reason: "use scrapeSinglePerformer")
|
||||
"Scrapes a complete performer record based on a scrapePerformerList result"
|
||||
scrapePerformer(
|
||||
scraper_id: ID!
|
||||
scraped_performer: ScrapedPerformerInput!
|
||||
): ScrapedPerformer @deprecated(reason: "use scrapeSinglePerformer")
|
||||
"Scrapes a complete scene record based on an existing scene"
|
||||
scrapeScene(scraper_id: ID!, scene: SceneUpdateInput!): ScrapedScene
|
||||
@deprecated(reason: "use scrapeSingleScene")
|
||||
"Scrapes a complete gallery record based on an existing gallery"
|
||||
scrapeGallery(scraper_id: ID!, gallery: GalleryUpdateInput!): ScrapedGallery
|
||||
@deprecated(reason: "use scrapeSingleGallery")
|
||||
|
||||
"Scrape a list of performers from a query"
|
||||
scrapeFreeonesPerformerList(query: String!): [String!]!
|
||||
@deprecated(
|
||||
reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones"
|
||||
)
|
||||
|
||||
# Plugins
|
||||
"List loaded plugins"
|
||||
plugins: [Plugin!]
|
||||
"List available plugin operations"
|
||||
pluginTasks: [PluginTask!]
|
||||
|
||||
# Packages
|
||||
"List installed packages"
|
||||
installedPackages(type: PackageType!): [Package!]!
|
||||
"List available packages"
|
||||
availablePackages(type: PackageType!, source: String!): [Package!]!
|
||||
|
||||
# Config
|
||||
"Returns the current, complete configuration"
|
||||
configuration: ConfigResult!
|
||||
@@ -228,8 +205,7 @@ type Query {
|
||||
allMovies: [Movie!]!
|
||||
allTags: [Tag!]!
|
||||
|
||||
# @deprecated
|
||||
allPerformers: [Performer!]!
|
||||
allPerformers: [Performer!]! @deprecated(reason: "Use findPerformers instead")
|
||||
|
||||
# Get everything with minimal metadata
|
||||
|
||||
@@ -335,6 +311,8 @@ type Mutation {
|
||||
moveFiles(input: MoveFilesInput!): Boolean!
|
||||
deleteFiles(ids: [ID!]!): Boolean!
|
||||
|
||||
fileSetFingerprints(input: FileSetFingerprintsInput!): Boolean!
|
||||
|
||||
# Saved filters
|
||||
saveFilter(input: SaveFilterInput!): SavedFilter!
|
||||
destroySavedFilter(input: DestroyFilterInput!): Boolean!
|
||||
@@ -349,6 +327,9 @@ type Mutation {
|
||||
input: ConfigDefaultSettingsInput!
|
||||
): ConfigDefaultSettingsResult!
|
||||
|
||||
# overwrites the entire plugin configuration for the given plugin
|
||||
configurePlugin(plugin_id: ID!, input: Map!): Map!
|
||||
|
||||
# overwrites the entire UI configuration
|
||||
configureUI(input: Map!): Map!
|
||||
# sets a single UI key value
|
||||
@@ -394,6 +375,12 @@ type Mutation {
|
||||
"Reload scrapers"
|
||||
reloadScrapers: Boolean!
|
||||
|
||||
"""
|
||||
Enable/disable plugins - enabledMap is a map of plugin IDs to enabled booleans.
|
||||
Plugins not in the map are not affected.
|
||||
"""
|
||||
setPluginsEnabled(enabledMap: BoolMap!): Boolean!
|
||||
|
||||
"Run plugin task. Returns the job ID"
|
||||
runPluginTask(
|
||||
plugin_id: ID!
|
||||
@@ -402,6 +389,29 @@ type Mutation {
|
||||
): ID!
|
||||
reloadPlugins: Boolean!
|
||||
|
||||
"""
|
||||
Installs the given packages.
|
||||
If a package is already installed, it will be updated if needed..
|
||||
If an error occurs when installing a package, the job will continue to install the remaining packages.
|
||||
Returns the job ID
|
||||
"""
|
||||
installPackages(type: PackageType!, packages: [PackageSpecInput!]!): ID!
|
||||
"""
|
||||
Updates the given packages.
|
||||
If a package is not installed, it will not be installed.
|
||||
If a package does not need to be updated, it will not be updated.
|
||||
If no packages are provided, all packages of the given type will be updated.
|
||||
If an error occurs when updating a package, the job will continue to update the remaining packages.
|
||||
Returns the job ID.
|
||||
"""
|
||||
updatePackages(type: PackageType!, packages: [PackageSpecInput!]): ID!
|
||||
"""
|
||||
Uninstalls the given packages.
|
||||
If an error occurs when uninstalling a package, the job will continue to uninstall the remaining packages.
|
||||
Returns the job ID
|
||||
"""
|
||||
uninstallPackages(type: PackageType!, packages: [PackageSpecInput!]!): ID!
|
||||
|
||||
stopJob(job_id: ID!): Boolean!
|
||||
stopAllJobs: Boolean!
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ input ConfigGeneralInput {
|
||||
metadataPath: String
|
||||
"Path to scrapers"
|
||||
scrapersPath: String
|
||||
"Path to plugins"
|
||||
pluginsPath: String
|
||||
"Path to cache"
|
||||
cachePath: String
|
||||
"Path to blobs - required for filesystem blob storage"
|
||||
@@ -139,8 +141,6 @@ input ConfigGeneralInput {
|
||||
password: String
|
||||
"Maximum session cookie age"
|
||||
maxSessionAge: Int
|
||||
"Comma separated list of proxies to allow traffic from"
|
||||
trustedProxies: [String!] @deprecated(reason: "no longer supported")
|
||||
"Name of the log file"
|
||||
logFile: String
|
||||
"Whether to also output to stderr"
|
||||
@@ -165,25 +165,15 @@ input ConfigGeneralInput {
|
||||
imageExcludes: [String!]
|
||||
"Custom Performer Image Location"
|
||||
customPerformerImageLocation: String
|
||||
"Scraper user agent string"
|
||||
scraperUserAgent: String
|
||||
@deprecated(
|
||||
reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead"
|
||||
)
|
||||
"Scraper CDP path. Path to chrome executable or remote address"
|
||||
scraperCDPPath: String
|
||||
@deprecated(
|
||||
reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead"
|
||||
)
|
||||
"Whether the scraper should check for invalid certificates"
|
||||
scraperCertCheck: Boolean
|
||||
@deprecated(
|
||||
reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead"
|
||||
)
|
||||
"Stash-box instances used for tagging"
|
||||
stashBoxes: [StashBoxInput!]
|
||||
"Python path - resolved using path if unset"
|
||||
pythonPath: String
|
||||
|
||||
"Source of scraper packages"
|
||||
scraperPackageSources: [PackageSourceInput!]
|
||||
"Source of plugin packages"
|
||||
pluginPackageSources: [PackageSourceInput!]
|
||||
}
|
||||
|
||||
type ConfigGeneralResult {
|
||||
@@ -201,6 +191,8 @@ type ConfigGeneralResult {
|
||||
configFilePath: String!
|
||||
"Path to scrapers"
|
||||
scrapersPath: String!
|
||||
"Path to plugins"
|
||||
pluginsPath: String!
|
||||
"Path to cache"
|
||||
cachePath: String!
|
||||
"Path to blobs - required for filesystem blob storage"
|
||||
@@ -269,8 +261,6 @@ type ConfigGeneralResult {
|
||||
password: String!
|
||||
"Maximum session cookie age"
|
||||
maxSessionAge: Int!
|
||||
"Comma separated list of proxies to allow traffic from"
|
||||
trustedProxies: [String!] @deprecated(reason: "no longer supported")
|
||||
"Name of the log file"
|
||||
logFile: String
|
||||
"Whether to also output to stderr"
|
||||
@@ -295,19 +285,15 @@ type ConfigGeneralResult {
|
||||
imageExcludes: [String!]!
|
||||
"Custom Performer Image Location"
|
||||
customPerformerImageLocation: String
|
||||
"Scraper user agent string"
|
||||
scraperUserAgent: String
|
||||
@deprecated(reason: "use ConfigResult.scraping instead")
|
||||
"Scraper CDP path. Path to chrome executable or remote address"
|
||||
scraperCDPPath: String
|
||||
@deprecated(reason: "use ConfigResult.scraping instead")
|
||||
"Whether the scraper should check for invalid certificates"
|
||||
scraperCertCheck: Boolean!
|
||||
@deprecated(reason: "use ConfigResult.scraping instead")
|
||||
"Stash-box instances used for tagging"
|
||||
stashBoxes: [StashBox!]!
|
||||
"Python path - resolved using path if unset"
|
||||
pythonPath: String!
|
||||
|
||||
"Source of scraper packages"
|
||||
scraperPackageSources: [PackageSource!]!
|
||||
"Source of plugin packages"
|
||||
pluginPackageSources: [PackageSource!]!
|
||||
}
|
||||
|
||||
input ConfigDisableDropdownCreateInput {
|
||||
@@ -388,9 +374,6 @@ input ConfigInterfaceInput {
|
||||
"Interface language"
|
||||
language: String
|
||||
|
||||
"Slideshow Delay"
|
||||
slideshowDelay: Int @deprecated(reason: "Use imageLightbox.slideshowDelay")
|
||||
|
||||
imageLightbox: ConfigImageLightboxInput
|
||||
|
||||
"Set to true to disable creating new objects via the dropdown menus"
|
||||
@@ -461,15 +444,10 @@ type ConfigInterfaceResult {
|
||||
"Interface language"
|
||||
language: String
|
||||
|
||||
"Slideshow Delay"
|
||||
slideshowDelay: Int @deprecated(reason: "Use imageLightbox.slideshowDelay")
|
||||
|
||||
imageLightbox: ConfigImageLightboxResult!
|
||||
|
||||
"Fields are true if creating via dropdown menus are disabled"
|
||||
disableDropdownCreate: ConfigDisableDropdownCreate!
|
||||
disabledDropdownCreate: ConfigDisableDropdownCreate!
|
||||
@deprecated(reason: "Use disableDropdownCreate")
|
||||
|
||||
"Handy Connection Key"
|
||||
handyKey: String
|
||||
@@ -557,6 +535,7 @@ type ConfigResult {
|
||||
scraping: ConfigScrapingResult!
|
||||
defaults: ConfigDefaultSettingsResult!
|
||||
ui: Map!
|
||||
plugins(include: [String!]): Map!
|
||||
}
|
||||
|
||||
"Directory structure of a path"
|
||||
|
||||
@@ -27,6 +27,7 @@ interface BaseFile {
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprint(type: String!): String
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
created_at: Time!
|
||||
@@ -44,6 +45,7 @@ type VideoFile implements BaseFile {
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprint(type: String!): String
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
format: String!
|
||||
@@ -70,6 +72,7 @@ type ImageFile implements BaseFile {
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprint(type: String!): String
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
width: Int!
|
||||
@@ -92,6 +95,7 @@ type GalleryFile implements BaseFile {
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprint(type: String!): String
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
created_at: Time!
|
||||
@@ -109,3 +113,15 @@ input MoveFilesInput {
|
||||
"valid only for single file id. If empty, existing basename is used"
|
||||
destination_basename: String
|
||||
}
|
||||
|
||||
input SetFingerprintsInput {
|
||||
type: String!
|
||||
"an null value will remove the fingerprint"
|
||||
value: String
|
||||
}
|
||||
|
||||
input FileSetFingerprintsInput {
|
||||
id: ID!
|
||||
"only supplied fingerprint types will be modified"
|
||||
fingerprints: [SetFingerprintsInput!]!
|
||||
}
|
||||
|
||||
@@ -98,8 +98,6 @@ input PerformerFilterType {
|
||||
country: StringCriterionInput
|
||||
"Filter by eye color"
|
||||
eye_color: StringCriterionInput
|
||||
"Filter by height"
|
||||
height: StringCriterionInput @deprecated(reason: "Use height_cm instead")
|
||||
"Filter by height in cm"
|
||||
height_cm: IntCriterionInput
|
||||
"Filter by measurements"
|
||||
@@ -135,13 +133,7 @@ input PerformerFilterType {
|
||||
"Filter by o count"
|
||||
o_counter: IntCriterionInput
|
||||
"Filter by StashID"
|
||||
stash_id: StringCriterionInput
|
||||
@deprecated(reason: "Use stash_id_endpoint instead")
|
||||
"Filter by StashID"
|
||||
stash_id_endpoint: StashIDCriterionInput
|
||||
"Filter by rating"
|
||||
rating: IntCriterionInput
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter by url"
|
||||
@@ -169,8 +161,6 @@ input PerformerFilterType {
|
||||
}
|
||||
|
||||
input SceneMarkerFilterType {
|
||||
"Filter to only include scene markers with this tag"
|
||||
tag_id: ID @deprecated(reason: "use tags filter instead")
|
||||
"Filter to only include scene markers with these tags"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter to only include scene markers attached to a scene with these tags"
|
||||
@@ -212,9 +202,6 @@ input SceneFilterType {
|
||||
path: StringCriterionInput
|
||||
"Filter by file count"
|
||||
file_count: IntCriterionInput
|
||||
"Filter by rating"
|
||||
rating: IntCriterionInput
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter by organized"
|
||||
@@ -225,6 +212,8 @@ input SceneFilterType {
|
||||
duplicated: PHashDuplicationCriterionInput
|
||||
"Filter by resolution"
|
||||
resolution: ResolutionCriterionInput
|
||||
"Filter by frame rate"
|
||||
framerate: IntCriterionInput
|
||||
"Filter by video codec"
|
||||
video_codec: StringCriterionInput
|
||||
"Filter by audio codec"
|
||||
@@ -254,9 +243,6 @@ input SceneFilterType {
|
||||
"Filter by performer count"
|
||||
performer_count: IntCriterionInput
|
||||
"Filter by StashID"
|
||||
stash_id: StringCriterionInput
|
||||
@deprecated(reason: "Use stash_id_endpoint instead")
|
||||
"Filter by StashID"
|
||||
stash_id_endpoint: StashIDCriterionInput
|
||||
"Filter by url"
|
||||
url: StringCriterionInput
|
||||
@@ -287,9 +273,6 @@ input MovieFilterType {
|
||||
|
||||
"Filter by duration (in seconds)"
|
||||
duration: IntCriterionInput
|
||||
"Filter by rating"
|
||||
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"
|
||||
@@ -318,15 +301,9 @@ input StudioFilterType {
|
||||
"Filter to only include studios with this parent studio"
|
||||
parents: MultiCriterionInput
|
||||
"Filter by StashID"
|
||||
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
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter by scene count"
|
||||
@@ -366,9 +343,6 @@ input GalleryFilterType {
|
||||
is_missing: String
|
||||
"Filter to include/exclude galleries that were created from zip"
|
||||
is_zip: Boolean
|
||||
"Filter by rating"
|
||||
rating: IntCriterionInput
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter by organized"
|
||||
@@ -403,6 +377,10 @@ input GalleryFilterType {
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
"Filter by studio code"
|
||||
code: StringCriterionInput
|
||||
"Filter by photographer"
|
||||
photographer: StringCriterionInput
|
||||
}
|
||||
|
||||
input TagFilterType {
|
||||
@@ -465,6 +443,7 @@ input ImageFilterType {
|
||||
NOT: ImageFilterType
|
||||
|
||||
title: StringCriterionInput
|
||||
details: StringCriterionInput
|
||||
|
||||
" Filter by image id"
|
||||
id: IntCriterionInput
|
||||
@@ -474,9 +453,6 @@ input ImageFilterType {
|
||||
path: StringCriterionInput
|
||||
"Filter by file count"
|
||||
file_count: IntCriterionInput
|
||||
"Filter by rating"
|
||||
rating: IntCriterionInput
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter by date"
|
||||
@@ -511,6 +487,10 @@ input ImageFilterType {
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
"Filter by studio code"
|
||||
code: StringCriterionInput
|
||||
"Filter by photographer"
|
||||
photographer: StringCriterionInput
|
||||
}
|
||||
|
||||
enum CriterionModifier {
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
"Gallery type"
|
||||
type Gallery {
|
||||
id: ID!
|
||||
checksum: String! @deprecated(reason: "Use files.fingerprints")
|
||||
path: String @deprecated(reason: "Use files.path")
|
||||
title: String
|
||||
code: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]!
|
||||
date: String
|
||||
details: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
photographer: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean!
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
file_mod_time: Time @deprecated(reason: "Use files.mod_time")
|
||||
|
||||
files: [GalleryFile!]!
|
||||
folder: Folder
|
||||
@@ -27,19 +24,17 @@ type Gallery {
|
||||
tags: [Tag!]!
|
||||
performers: [Performer!]!
|
||||
|
||||
"The images in the gallery"
|
||||
images: [Image!]! @deprecated(reason: "Use findImages")
|
||||
cover: Image
|
||||
}
|
||||
|
||||
input GalleryCreateInput {
|
||||
title: String!
|
||||
code: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
details: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
photographer: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
@@ -53,12 +48,12 @@ input GalleryUpdateInput {
|
||||
clientMutationId: String
|
||||
id: ID!
|
||||
title: String
|
||||
code: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
details: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
photographer: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
@@ -73,12 +68,12 @@ input GalleryUpdateInput {
|
||||
input BulkGalleryUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
code: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: BulkUpdateStrings
|
||||
date: String
|
||||
details: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
photographer: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
type Image {
|
||||
id: ID!
|
||||
checksum: String @deprecated(reason: "Use files.fingerprints")
|
||||
title: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
code: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]!
|
||||
date: String
|
||||
details: String
|
||||
photographer: String
|
||||
o_counter: Int
|
||||
organized: Boolean!
|
||||
path: String! @deprecated(reason: "Use files.path")
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
|
||||
file_mod_time: Time @deprecated(reason: "Use files.mod_time")
|
||||
|
||||
file: ImageFileType! @deprecated(reason: "Use visual_files")
|
||||
files: [ImageFile!]! @deprecated(reason: "Use visual_files")
|
||||
visual_files: [VisualFile!]!
|
||||
paths: ImagePathsType! # Resolver
|
||||
@@ -44,14 +40,15 @@ input ImageUpdateInput {
|
||||
clientMutationId: String
|
||||
id: ID!
|
||||
title: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
code: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
details: String
|
||||
photographer: String
|
||||
|
||||
studio_id: ID
|
||||
performer_ids: [ID!]
|
||||
@@ -65,14 +62,15 @@ input BulkImageUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
title: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
code: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: BulkUpdateStrings
|
||||
date: String
|
||||
details: String
|
||||
photographer: String
|
||||
|
||||
studio_id: ID
|
||||
performer_ids: BulkUpdateIds
|
||||
|
||||
@@ -75,19 +75,6 @@ input ScanMetaDataFilterInput {
|
||||
input ScanMetadataInput {
|
||||
paths: [String!]
|
||||
|
||||
# useFileMetadata is deprecated with the new file management system
|
||||
# if this functionality is desired, then we can make a built in scraper instead.
|
||||
|
||||
"Set name, date, details from metadata (if present)"
|
||||
useFileMetadata: Boolean @deprecated(reason: "Not implemented")
|
||||
|
||||
# stripFileExtension is deprecated since we no longer set the title from the
|
||||
# filename - it is automatically returned if the object has no title. If this
|
||||
# functionality is desired, then we could make this an option to not include
|
||||
# the extension in the auto-generated title.
|
||||
|
||||
"Strip file extension from title"
|
||||
stripFileExtension: Boolean @deprecated(reason: "Not implemented")
|
||||
"Generate covers during scan"
|
||||
scanGenerateCovers: Boolean
|
||||
"Generate previews during scan"
|
||||
@@ -108,10 +95,6 @@ input ScanMetadataInput {
|
||||
}
|
||||
|
||||
type ScanMetadataOptions {
|
||||
"Set name, date, details from metadata (if present)"
|
||||
useFileMetadata: Boolean! @deprecated(reason: "Not implemented")
|
||||
"Strip file extension from title"
|
||||
stripFileExtension: Boolean! @deprecated(reason: "Not implemented")
|
||||
"Generate covers during scan"
|
||||
scanGenerateCovers: Boolean!
|
||||
"Generate previews during scan"
|
||||
@@ -320,6 +303,9 @@ type SystemStatus {
|
||||
configPath: String
|
||||
appSchema: Int!
|
||||
status: SystemStatusEnum!
|
||||
os: String!
|
||||
workingDir: String!
|
||||
homeDir: String!
|
||||
}
|
||||
|
||||
input MigrateInput {
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
type Movie {
|
||||
id: ID!
|
||||
name: String!
|
||||
checksum: String! @deprecated(reason: "MD5 hash of name, use name directly")
|
||||
aliases: String
|
||||
"Duration in seconds"
|
||||
duration: Int
|
||||
date: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio: Studio
|
||||
@@ -29,8 +26,6 @@ input MovieCreateInput {
|
||||
"Duration in seconds"
|
||||
duration: Int
|
||||
date: String
|
||||
# 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
|
||||
@@ -49,8 +44,6 @@ input MovieUpdateInput {
|
||||
aliases: String
|
||||
duration: Int
|
||||
date: String
|
||||
# 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
|
||||
@@ -66,8 +59,6 @@ input MovieUpdateInput {
|
||||
input BulkMovieUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
# 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
|
||||
|
||||
36
graphql/schema/types/package.graphql
Normal file
36
graphql/schema/types/package.graphql
Normal file
@@ -0,0 +1,36 @@
|
||||
enum PackageType {
|
||||
Scraper
|
||||
Plugin
|
||||
}
|
||||
|
||||
type Package {
|
||||
package_id: String!
|
||||
name: String!
|
||||
version: String
|
||||
date: Timestamp
|
||||
requires: [Package!]!
|
||||
|
||||
sourceURL: String!
|
||||
|
||||
"The version of this package currently available from the remote source"
|
||||
source_package: Package
|
||||
|
||||
metadata: Map!
|
||||
}
|
||||
|
||||
input PackageSpecInput {
|
||||
id: String!
|
||||
sourceURL: String!
|
||||
}
|
||||
|
||||
type PackageSource {
|
||||
name: String
|
||||
url: String!
|
||||
local_path: String
|
||||
}
|
||||
|
||||
input PackageSourceInput {
|
||||
name: String
|
||||
url: String!
|
||||
local_path: String
|
||||
}
|
||||
@@ -14,7 +14,6 @@ enum CircumisedEnum {
|
||||
|
||||
type Performer {
|
||||
id: ID!
|
||||
checksum: String @deprecated(reason: "Not used")
|
||||
name: String!
|
||||
disambiguation: String
|
||||
url: String
|
||||
@@ -25,7 +24,6 @@ type Performer {
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
@@ -34,7 +32,6 @@ type Performer {
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: [String!]!
|
||||
favorite: Boolean!
|
||||
tags: [Tag!]!
|
||||
@@ -49,8 +46,6 @@ type Performer {
|
||||
o_counter: Int # Resolver
|
||||
scenes: [Scene!]!
|
||||
stash_ids: [StashID!]!
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
@@ -71,8 +66,6 @@ input PerformerCreateInput {
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
# height must be parsable into an integer
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
@@ -81,7 +74,6 @@ input PerformerCreateInput {
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: [String!]
|
||||
twitter: String
|
||||
instagram: String
|
||||
@@ -90,8 +82,6 @@ input PerformerCreateInput {
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
@@ -111,8 +101,6 @@ input PerformerUpdateInput {
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
# height must be parsable into an integer
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
@@ -121,7 +109,6 @@ input PerformerUpdateInput {
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: [String!]
|
||||
twitter: String
|
||||
instagram: String
|
||||
@@ -130,8 +117,6 @@ input PerformerUpdateInput {
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
@@ -156,8 +141,6 @@ input BulkPerformerUpdateInput {
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
# height must be parsable into an integer
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
@@ -166,14 +149,11 @@ input BulkPerformerUpdateInput {
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: BulkUpdateStrings
|
||||
twitter: String
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
tag_ids: BulkUpdateIds
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
type PluginPaths {
|
||||
# path to javascript files
|
||||
javascript: [String!]
|
||||
# path to css files
|
||||
css: [String!]
|
||||
}
|
||||
|
||||
type Plugin {
|
||||
id: ID!
|
||||
name: String!
|
||||
@@ -5,8 +12,19 @@ type Plugin {
|
||||
url: String
|
||||
version: String
|
||||
|
||||
enabled: Boolean!
|
||||
|
||||
tasks: [PluginTask!]
|
||||
hooks: [PluginHook!]
|
||||
settings: [PluginSetting!]
|
||||
|
||||
"""
|
||||
Plugin IDs of plugins that this plugin depends on.
|
||||
Applies only for UI plugins to indicate css/javascript load order.
|
||||
"""
|
||||
requires: [ID!]
|
||||
|
||||
paths: PluginPaths!
|
||||
}
|
||||
|
||||
type PluginTask {
|
||||
@@ -40,3 +58,16 @@ input PluginValueInput {
|
||||
o: [PluginArgInput!]
|
||||
a: [PluginValueInput!]
|
||||
}
|
||||
|
||||
enum PluginSettingTypeEnum {
|
||||
STRING
|
||||
NUMBER
|
||||
BOOLEAN
|
||||
}
|
||||
|
||||
type PluginSetting {
|
||||
name: String!
|
||||
display_name: String
|
||||
description: String
|
||||
type: PluginSettingTypeEnum!
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ scalar Timestamp
|
||||
# generic JSON object
|
||||
scalar Map
|
||||
|
||||
# string, boolean map
|
||||
scalar BoolMap
|
||||
|
||||
scalar Any
|
||||
|
||||
scalar Int64
|
||||
|
||||
@@ -15,7 +15,6 @@ type ScenePathsType {
|
||||
stream: String # Resolver
|
||||
webp: String # Resolver
|
||||
vtt: String # Resolver
|
||||
chapters_vtt: String @deprecated
|
||||
sprite: String # Resolver
|
||||
funscript: String # Resolver
|
||||
interactive_heatmap: String # Resolver
|
||||
@@ -34,8 +33,6 @@ type VideoCaption {
|
||||
|
||||
type Scene {
|
||||
id: ID!
|
||||
checksum: String @deprecated(reason: "Use files.fingerprints")
|
||||
oshash: String @deprecated(reason: "Use files.fingerprints")
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
@@ -43,20 +40,15 @@ type Scene {
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [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
|
||||
organized: Boolean!
|
||||
o_counter: Int
|
||||
path: String! @deprecated(reason: "Use files.path")
|
||||
phash: String @deprecated(reason: "Use files.fingerprints")
|
||||
interactive: Boolean!
|
||||
interactive_speed: Int
|
||||
captions: [VideoCaption!]
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
file_mod_time: Time
|
||||
"The last time play count was updated"
|
||||
last_played_at: Time
|
||||
"The time index a scene was left at"
|
||||
@@ -66,7 +58,6 @@ type Scene {
|
||||
"The number ot times a scene has been played"
|
||||
play_count: Int
|
||||
|
||||
file: SceneFileType! @deprecated(reason: "Use files")
|
||||
files: [VideoFile!]!
|
||||
paths: ScenePathsType! # Resolver
|
||||
scene_markers: [SceneMarker!]!
|
||||
@@ -94,8 +85,6 @@ input SceneCreateInput {
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [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
|
||||
organized: Boolean
|
||||
@@ -126,8 +115,6 @@ input SceneUpdateInput {
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [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
|
||||
@@ -172,8 +159,6 @@ input BulkSceneUpdateInput {
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: BulkUpdateStrings
|
||||
date: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
|
||||
@@ -99,7 +99,9 @@ input ScrapedSceneInput {
|
||||
|
||||
type ScrapedGallery {
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
photographer: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
@@ -111,7 +113,9 @@ type ScrapedGallery {
|
||||
|
||||
input ScrapedGalleryInput {
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
photographer: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
type Studio {
|
||||
id: ID!
|
||||
name: String!
|
||||
checksum: String! @deprecated(reason: "MD5 hash of name, use name directly")
|
||||
url: String
|
||||
parent_studio: Studio
|
||||
child_studios: [Studio!]!
|
||||
@@ -15,8 +14,6 @@ type Studio {
|
||||
performer_count(depth: Int): Int! # Resolver
|
||||
movie_count(depth: Int): Int! # Resolver
|
||||
stash_ids: [StashID!]!
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
@@ -32,8 +29,6 @@ input StudioCreateInput {
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
@@ -49,8 +44,6 @@ input StudioUpdateInput {
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
|
||||
38
internal/api/bool_map.go
Normal file
38
internal/api/bool_map.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
)
|
||||
|
||||
func MarshalBoolMap(val map[string]bool) graphql.Marshaler {
|
||||
return graphql.WriterFunc(func(w io.Writer) {
|
||||
err := json.NewEncoder(w).Encode(val)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func UnmarshalBoolMap(v interface{}) (map[string]bool, error) {
|
||||
m, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T is not a map", v)
|
||||
}
|
||||
|
||||
result := make(map[string]bool)
|
||||
for k, v := range m {
|
||||
key := k
|
||||
val, ok := v.(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("key %s (%T) is not a bool", k, v)
|
||||
}
|
||||
|
||||
result[key] = val
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -153,40 +153,6 @@ func (t changesetTranslator) intPtrFromString(value *string) (*int, error) {
|
||||
return &vv, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) ratingConversion(legacyValue *int, rating100Value *int) *int {
|
||||
const (
|
||||
legacyField = "rating"
|
||||
rating100Field = "rating100"
|
||||
)
|
||||
|
||||
legacyRating := t.optionalInt(legacyValue, legacyField)
|
||||
if legacyRating.Set && !legacyRating.Null {
|
||||
ret := models.Rating5To100(legacyRating.Value)
|
||||
return &ret
|
||||
}
|
||||
|
||||
o := t.optionalInt(rating100Value, rating100Field)
|
||||
if o.Set && !o.Null {
|
||||
return &o.Value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalRatingConversion(legacyValue *int, rating100Value *int) models.OptionalInt {
|
||||
const (
|
||||
legacyField = "rating"
|
||||
rating100Field = "rating100"
|
||||
)
|
||||
|
||||
legacyRating := t.optionalInt(legacyValue, legacyField)
|
||||
if legacyRating.Set && !legacyRating.Null {
|
||||
legacyRating.Value = models.Rating5To100(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{}
|
||||
|
||||
@@ -26,8 +26,8 @@ const defaultSHLength int = 8 // default length of SHA short hash returned by <g
|
||||
|
||||
var stashReleases = func() map[string]string {
|
||||
return map[string]string{
|
||||
"darwin/amd64": "stash-osx",
|
||||
"darwin/arm64": "stash-osx-applesilicon",
|
||||
"darwin/amd64": "stash-macos",
|
||||
"darwin/arm64": "stash-macos",
|
||||
"linux/amd64": "stash-linux",
|
||||
"windows/amd64": "stash-win.exe",
|
||||
"linux/arm": "stash-linux-arm32v6",
|
||||
|
||||
@@ -13,4 +13,5 @@ const (
|
||||
tagKey
|
||||
downloadKey
|
||||
imageKey
|
||||
pluginKey
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/collate"
|
||||
)
|
||||
@@ -25,13 +26,27 @@ func (s dirLister) Bytes(i int) []byte {
|
||||
// listDir will return the contents of a given directory path as a string slice
|
||||
func listDir(col *collate.Collator, path string) ([]string, error) {
|
||||
var dirPaths []string
|
||||
dirPath := path
|
||||
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
path = filepath.Dir(path)
|
||||
files, err = os.ReadDir(path)
|
||||
dirPath = filepath.Dir(path)
|
||||
dirFiles, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return dirPaths, err
|
||||
}
|
||||
|
||||
// Filter dir contents by last path fragment if the dir isn't an exact match
|
||||
base := strings.ToLower(filepath.Base(path))
|
||||
if base != "." && base != string(filepath.Separator) {
|
||||
for _, file := range dirFiles {
|
||||
if strings.HasPrefix(strings.ToLower(file.Name()), base) {
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files = dirFiles
|
||||
}
|
||||
}
|
||||
|
||||
if col != nil {
|
||||
@@ -42,7 +57,7 @@ func listDir(col *collate.Collator, path string) ([]string, error) {
|
||||
if !file.IsDir() {
|
||||
continue
|
||||
}
|
||||
dirPaths = append(dirPaths, filepath.Join(path, file.Name()))
|
||||
dirPaths = append(dirPaths, filepath.Join(dirPath, file.Name()))
|
||||
}
|
||||
return dirPaths, nil
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/hash"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
@@ -18,7 +19,7 @@ type imageBox struct {
|
||||
files []string
|
||||
}
|
||||
|
||||
var imageExtensions = []string{
|
||||
var imageBoxExts = []string{
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
@@ -42,7 +43,7 @@ func newImageBox(box fs.FS) (*imageBox, error) {
|
||||
}
|
||||
|
||||
baseName := strings.ToLower(d.Name())
|
||||
for _, ext := range imageExtensions {
|
||||
for _, ext := range imageBoxExts {
|
||||
if strings.HasSuffix(baseName, ext) {
|
||||
ret.files = append(ret.files, path)
|
||||
break
|
||||
@@ -55,65 +56,14 @@ func newImageBox(box fs.FS) (*imageBox, error) {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
var performerBox *imageBox
|
||||
var performerBoxMale *imageBox
|
||||
var performerBoxCustom *imageBox
|
||||
|
||||
func initialiseImages() {
|
||||
var err error
|
||||
performerBox, err = newImageBox(&static.Performer)
|
||||
if err != nil {
|
||||
logger.Warnf("error loading performer images: %v", err)
|
||||
}
|
||||
performerBoxMale, err = newImageBox(&static.PerformerMale)
|
||||
if err != nil {
|
||||
logger.Warnf("error loading male performer images: %v", err)
|
||||
}
|
||||
initialiseCustomImages()
|
||||
}
|
||||
|
||||
func initialiseCustomImages() {
|
||||
customPath := config.GetInstance().GetCustomPerformerImageLocation()
|
||||
if customPath != "" {
|
||||
logger.Debugf("Loading custom performer images from %s", customPath)
|
||||
// We need to set performerBoxCustom at runtime, as this is a custom path, and store it in a pointer.
|
||||
var err error
|
||||
performerBoxCustom, err = newImageBox(os.DirFS(customPath))
|
||||
if err != nil {
|
||||
logger.Warnf("error loading custom performer from %s: %v", customPath, err)
|
||||
}
|
||||
} else {
|
||||
performerBoxCustom = nil
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
if performerBoxCustom != nil && len(performerBoxCustom.files) > 0 {
|
||||
box = performerBoxCustom
|
||||
func (box *imageBox) GetRandomImageByName(name string) ([]byte, error) {
|
||||
files := box.files
|
||||
if len(files) == 0 {
|
||||
return nil, errors.New("box is empty")
|
||||
}
|
||||
|
||||
var g models.GenderEnum
|
||||
if gender != nil {
|
||||
g = *gender
|
||||
}
|
||||
|
||||
if box == nil {
|
||||
switch g {
|
||||
case models.GenderEnumFemale, models.GenderEnumTransgenderFemale:
|
||||
box = performerBox
|
||||
case models.GenderEnumMale, models.GenderEnumTransgenderMale:
|
||||
box = performerBoxMale
|
||||
default:
|
||||
box = performerBox
|
||||
}
|
||||
}
|
||||
|
||||
imageFiles := box.files
|
||||
index := hash.IntFromString(name) % uint64(len(imageFiles))
|
||||
img, err := box.box.Open(imageFiles[index])
|
||||
index := hash.IntFromString(name) % uint64(len(files))
|
||||
img, err := box.box.Open(files[index])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -121,3 +71,64 @@ func getRandomPerformerImageUsingName(name string, gender *models.GenderEnum, cu
|
||||
|
||||
return io.ReadAll(img)
|
||||
}
|
||||
|
||||
var performerBox *imageBox
|
||||
var performerBoxMale *imageBox
|
||||
var performerBoxCustom *imageBox
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
performerBox, err = newImageBox(static.Sub(static.Performer))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("loading performer images: %v", err))
|
||||
}
|
||||
performerBoxMale, err = newImageBox(static.Sub(static.PerformerMale))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("loading male performer images: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func initCustomPerformerImages(customPath string) {
|
||||
if customPath != "" {
|
||||
logger.Debugf("Loading custom performer images from %s", customPath)
|
||||
var err error
|
||||
performerBoxCustom, err = newImageBox(os.DirFS(customPath))
|
||||
if err != nil {
|
||||
logger.Warnf("error loading custom performer images from %s: %v", customPath, err)
|
||||
}
|
||||
} else {
|
||||
performerBoxCustom = nil
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultPerformerImage(name string, gender *models.GenderEnum) []byte {
|
||||
// try the custom box first if we have one
|
||||
if performerBoxCustom != nil {
|
||||
ret, err := performerBoxCustom.GetRandomImageByName(name)
|
||||
if err == nil {
|
||||
return ret
|
||||
}
|
||||
logger.Warnf("error loading custom default performer image: %v", err)
|
||||
}
|
||||
|
||||
var g models.GenderEnum
|
||||
if gender != nil {
|
||||
g = *gender
|
||||
}
|
||||
|
||||
var box *imageBox
|
||||
switch g {
|
||||
case models.GenderEnumFemale, models.GenderEnumTransgenderFemale:
|
||||
box = performerBox
|
||||
case models.GenderEnumMale, models.GenderEnumTransgenderMale:
|
||||
box = performerBoxMale
|
||||
default:
|
||||
box = performerBox
|
||||
}
|
||||
|
||||
ret, err := box.GetRandomImageByName(name)
|
||||
if err != nil {
|
||||
logger.Warnf("error loading default performer image: %v", err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
type contextKey struct{ name string }
|
||||
@@ -49,8 +47,7 @@ type Loaders struct {
|
||||
}
|
||||
|
||||
type Middleware struct {
|
||||
DatabaseProvider txn.DatabaseProvider
|
||||
Repository manager.Repository
|
||||
Repository models.Repository
|
||||
}
|
||||
|
||||
func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
@@ -131,13 +128,9 @@ func toErrorSlice(err error) []error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Middleware) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
return txn.WithDatabase(ctx, m.DatabaseProvider, fn)
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models.Scene, []error) {
|
||||
return func(keys []int) (ret []*models.Scene, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -148,7 +141,7 @@ func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models
|
||||
|
||||
func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models.Image, []error) {
|
||||
return func(keys []int) (ret []*models.Image, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Image.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -160,7 +153,7 @@ func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models
|
||||
|
||||
func (m Middleware) fetchGalleries(ctx context.Context) func(keys []int) ([]*models.Gallery, []error) {
|
||||
return func(keys []int) (ret []*models.Gallery, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Gallery.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -172,7 +165,7 @@ func (m Middleware) fetchGalleries(ctx context.Context) func(keys []int) ([]*mod
|
||||
|
||||
func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*models.Performer, []error) {
|
||||
return func(keys []int) (ret []*models.Performer, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Performer.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -184,7 +177,7 @@ func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*mo
|
||||
|
||||
func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*models.Studio, []error) {
|
||||
return func(keys []int) (ret []*models.Studio, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Studio.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -195,7 +188,7 @@ func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*model
|
||||
|
||||
func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.Tag, []error) {
|
||||
return func(keys []int) (ret []*models.Tag, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Tag.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -206,7 +199,7 @@ func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.T
|
||||
|
||||
func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models.Movie, []error) {
|
||||
return func(keys []int) (ret []*models.Movie, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Movie.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -217,7 +210,7 @@ func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models
|
||||
|
||||
func (m Middleware) fetchFiles(ctx context.Context) func(keys []models.FileID) ([]models.File, []error) {
|
||||
return func(keys []models.FileID) (ret []models.File, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.File.Find(ctx, keys...)
|
||||
return err
|
||||
@@ -228,7 +221,7 @@ func (m Middleware) fetchFiles(ctx context.Context) func(keys []models.FileID) (
|
||||
|
||||
func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
|
||||
return func(keys []int) (ret [][]models.FileID, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyFileIDs(ctx, keys)
|
||||
return err
|
||||
@@ -239,7 +232,7 @@ func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([]
|
||||
|
||||
func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
|
||||
return func(keys []int) (ret [][]models.FileID, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Image.GetManyFileIDs(ctx, keys)
|
||||
return err
|
||||
@@ -250,7 +243,7 @@ func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([]
|
||||
|
||||
func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
|
||||
return func(keys []int) (ret [][]models.FileID, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Gallery.GetManyFileIDs(ctx, keys)
|
||||
return err
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type BaseFile interface{}
|
||||
type BaseFile interface {
|
||||
IsBaseFile()
|
||||
}
|
||||
|
||||
type VisualFile interface {
|
||||
IsVisualFile()
|
||||
}
|
||||
|
||||
func convertVisualFile(f models.File) (VisualFile, error) {
|
||||
switch f := f.(type) {
|
||||
case VisualFile:
|
||||
return f, nil
|
||||
case *models.VideoFile:
|
||||
return &VideoFile{VideoFile: f}, nil
|
||||
case *models.ImageFile:
|
||||
return &ImageFile{ImageFile: f}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("file %s is not a visual file", f.Base().Path)
|
||||
}
|
||||
}
|
||||
|
||||
type GalleryFile struct {
|
||||
*models.BaseFile
|
||||
}
|
||||
|
||||
var ErrTimestamp = errors.New("cannot parse Timestamp")
|
||||
func (GalleryFile) IsBaseFile() {}
|
||||
|
||||
func MarshalTimestamp(t time.Time) graphql.Marshaler {
|
||||
if t.IsZero() {
|
||||
return graphql.Null
|
||||
}
|
||||
func (GalleryFile) IsVisualFile() {}
|
||||
|
||||
return graphql.WriterFunc(func(w io.Writer) {
|
||||
_, err := io.WriteString(w, strconv.Quote(t.Format(time.RFC3339Nano)))
|
||||
if err != nil {
|
||||
logger.Warnf("could not marshal timestamp: %v", err)
|
||||
}
|
||||
})
|
||||
func (f *GalleryFile) Fingerprints() []models.Fingerprint {
|
||||
return f.BaseFile.Fingerprints
|
||||
}
|
||||
|
||||
func UnmarshalTimestamp(v interface{}) (time.Time, error) {
|
||||
if tmpStr, ok := v.(string); ok {
|
||||
if len(tmpStr) == 0 {
|
||||
return time.Time{}, fmt.Errorf("%w: empty string", ErrTimestamp)
|
||||
}
|
||||
|
||||
switch tmpStr[0] {
|
||||
case '>', '<':
|
||||
d, err := time.ParseDuration(tmpStr[1:])
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("%w: cannot parse %v-duration: %v", ErrTimestamp, tmpStr[0], err)
|
||||
}
|
||||
t := time.Now()
|
||||
// Compute point in time:
|
||||
if tmpStr[0] == '<' {
|
||||
t = t.Add(-d)
|
||||
} else {
|
||||
t = t.Add(d)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
return utils.ParseDateStringAsTime(tmpStr)
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("%w: not a string", ErrTimestamp)
|
||||
type VideoFile struct {
|
||||
*models.VideoFile
|
||||
}
|
||||
|
||||
func (VideoFile) IsBaseFile() {}
|
||||
|
||||
func (VideoFile) IsVisualFile() {}
|
||||
|
||||
func (f *VideoFile) Fingerprints() []models.Fingerprint {
|
||||
return f.VideoFile.Fingerprints
|
||||
}
|
||||
|
||||
type ImageFile struct {
|
||||
*models.ImageFile
|
||||
}
|
||||
|
||||
func (ImageFile) IsBaseFile() {}
|
||||
|
||||
func (ImageFile) IsVisualFile() {}
|
||||
|
||||
func (f *ImageFile) Fingerprints() []models.Fingerprint {
|
||||
return f.ImageFile.Fingerprints
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -33,8 +33,7 @@ type hookExecutor interface {
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
txnManager txn.Manager
|
||||
repository manager.Repository
|
||||
repository models.Repository
|
||||
sceneService manager.SceneService
|
||||
imageService manager.ImageService
|
||||
galleryService manager.GalleryService
|
||||
@@ -82,9 +81,24 @@ func (r *Resolver) Subscription() SubscriptionResolver {
|
||||
func (r *Resolver) Tag() TagResolver {
|
||||
return &tagResolver{r}
|
||||
}
|
||||
func (r *Resolver) GalleryFile() GalleryFileResolver {
|
||||
return &galleryFileResolver{r}
|
||||
}
|
||||
func (r *Resolver) VideoFile() VideoFileResolver {
|
||||
return &videoFileResolver{r}
|
||||
}
|
||||
func (r *Resolver) ImageFile() ImageFileResolver {
|
||||
return &imageFileResolver{r}
|
||||
}
|
||||
func (r *Resolver) SavedFilter() SavedFilterResolver {
|
||||
return &savedFilterResolver{r}
|
||||
}
|
||||
func (r *Resolver) Plugin() PluginResolver {
|
||||
return &pluginResolver{r}
|
||||
}
|
||||
func (r *Resolver) ConfigResult() ConfigResultResolver {
|
||||
return &configResultResolver{r}
|
||||
}
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
||||
@@ -99,14 +113,23 @@ type imageResolver struct{ *Resolver }
|
||||
type studioResolver struct{ *Resolver }
|
||||
type movieResolver struct{ *Resolver }
|
||||
type tagResolver struct{ *Resolver }
|
||||
type galleryFileResolver struct{ *Resolver }
|
||||
type videoFileResolver struct{ *Resolver }
|
||||
type imageFileResolver struct{ *Resolver }
|
||||
type savedFilterResolver struct{ *Resolver }
|
||||
type pluginResolver struct{ *Resolver }
|
||||
type configResultResolver struct{ *Resolver }
|
||||
|
||||
func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
return txn.WithTxn(ctx, r.txnManager, fn)
|
||||
return r.repository.WithTxn(ctx, fn)
|
||||
}
|
||||
|
||||
func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
return txn.WithReadTxn(ctx, r.txnManager, fn)
|
||||
return r.repository.WithReadTxn(ctx, fn)
|
||||
}
|
||||
|
||||
func (r *Resolver) stashboxRepository() stashbox.Repository {
|
||||
return stashbox.NewRepository(r.repository)
|
||||
}
|
||||
|
||||
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
|
||||
@@ -145,27 +168,90 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
||||
var ret StatsResultType
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
repo := r.repository
|
||||
scenesQB := repo.Scene
|
||||
sceneQB := repo.Scene
|
||||
imageQB := repo.Image
|
||||
galleryQB := repo.Gallery
|
||||
studiosQB := repo.Studio
|
||||
performersQB := repo.Performer
|
||||
moviesQB := repo.Movie
|
||||
tagsQB := repo.Tag
|
||||
scenesCount, _ := scenesQB.Count(ctx)
|
||||
scenesSize, _ := scenesQB.Size(ctx)
|
||||
scenesDuration, _ := scenesQB.Duration(ctx)
|
||||
imageCount, _ := imageQB.Count(ctx)
|
||||
imageSize, _ := imageQB.Size(ctx)
|
||||
galleryCount, _ := galleryQB.Count(ctx)
|
||||
performersCount, _ := performersQB.Count(ctx)
|
||||
studiosCount, _ := studiosQB.Count(ctx)
|
||||
moviesCount, _ := moviesQB.Count(ctx)
|
||||
tagsCount, _ := tagsQB.Count(ctx)
|
||||
totalOCount, _ := scenesQB.OCount(ctx)
|
||||
totalPlayDuration, _ := scenesQB.PlayDuration(ctx)
|
||||
totalPlayCount, _ := scenesQB.PlayCount(ctx)
|
||||
uniqueScenePlayCount, _ := scenesQB.UniqueScenePlayCount(ctx)
|
||||
studioQB := repo.Studio
|
||||
performerQB := repo.Performer
|
||||
movieQB := repo.Movie
|
||||
tagQB := repo.Tag
|
||||
|
||||
// embrace the error
|
||||
|
||||
scenesCount, err := sceneQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenesSize, err := sceneQB.Size(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenesDuration, err := sceneQB.Duration(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageCount, err := imageQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageSize, err := imageQB.Size(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
galleryCount, err := galleryQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
performersCount, err := performerQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
studiosCount, err := studioQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
moviesCount, err := movieQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tagsCount, err := tagQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenesTotalOCount, err := sceneQB.OCount(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imagesTotalOCount, err := imageQB.OCount(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
totalOCount := scenesTotalOCount + imagesTotalOCount
|
||||
|
||||
totalPlayDuration, err := sceneQB.PlayDuration(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalPlayCount, err := sceneQB.PlayCount(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uniqueScenePlayCount, err := sceneQB.UniqueScenePlayCount(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = StatsResultType{
|
||||
SceneCount: scenesCount,
|
||||
|
||||
25
internal/api/resolver_model_config.go
Normal file
25
internal/api/resolver_model_config.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
)
|
||||
|
||||
func (r *configResultResolver) Plugins(ctx context.Context, obj *ConfigResult, include []string) (map[string]interface{}, error) {
|
||||
if len(include) == 0 {
|
||||
ret := config.GetInstance().GetAllPluginConfiguration()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
ret := make(map[string]interface{})
|
||||
|
||||
for _, plugin := range include {
|
||||
c := config.GetInstance().GetPluginConfiguration(plugin)
|
||||
if len(c) > 0 {
|
||||
ret[plugin] = c
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
30
internal/api/resolver_model_file.go
Normal file
30
internal/api/resolver_model_file.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package api
|
||||
|
||||
import "context"
|
||||
|
||||
func (r *galleryFileResolver) Fingerprint(ctx context.Context, obj *GalleryFile, type_ string) (*string, error) {
|
||||
fp := obj.BaseFile.Fingerprints.For(type_)
|
||||
if fp != nil {
|
||||
v := fp.Value()
|
||||
return &v, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageFileResolver) Fingerprint(ctx context.Context, obj *ImageFile, type_ string) (*string, error) {
|
||||
fp := obj.ImageFile.Fingerprints.For(type_)
|
||||
if fp != nil {
|
||||
v := fp.Value()
|
||||
return &v, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *videoFileResolver) Fingerprint(ctx context.Context, obj *VideoFile, type_ string) (*string, error) {
|
||||
fp := obj.VideoFile.Fingerprints.For(type_)
|
||||
if fp != nil {
|
||||
v := fp.Value()
|
||||
return &v, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
@@ -11,19 +10,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *galleryResolver) getPrimaryFile(ctx context.Context, obj *models.Gallery) (models.File, error) {
|
||||
if obj.PrimaryFileID != nil {
|
||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) getFiles(ctx context.Context, obj *models.Gallery) ([]models.File, error) {
|
||||
fileIDs, err := loaders.From(ctx).GalleryFiles.Load(obj.ID)
|
||||
if err != nil {
|
||||
@@ -78,38 +64,6 @@ func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*mod
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f != nil {
|
||||
return &f.Base().ModTime, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Images is deprecated, slow and shouldn't be used
|
||||
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret []*models.Image, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// #2376 - sort images by path
|
||||
// doing this via Query is really slow, so stick with FindByGalleryID
|
||||
ret, err = r.repository.Image.FindByGalleryID(ctx, obj.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *models.Image, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
// Find cover image first
|
||||
@@ -130,26 +84,6 @@ func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*strin
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (string, error) {
|
||||
if !obj.Files.PrimaryLoaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadPrimaryFile(ctx, r.repository.File)
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return 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
|
||||
}
|
||||
|
||||
@@ -2,35 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func convertVisualFile(f models.File) (models.VisualFile, error) {
|
||||
vf, ok := f.(models.VisualFile)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file %s is not a visual file", f.Base().Path)
|
||||
}
|
||||
return vf, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) getPrimaryFile(ctx context.Context, obj *models.Image) (models.VisualFile, error) {
|
||||
if obj.PrimaryFileID != nil {
|
||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertVisualFile(f)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) getFiles(ctx context.Context, obj *models.Image) ([]models.File, error) {
|
||||
fileIDs, err := loaders.From(ctx).ImageFiles.Load(obj.ID)
|
||||
if err != nil {
|
||||
@@ -46,33 +23,13 @@ func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string,
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFileType, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
width := f.GetWidth()
|
||||
height := f.GetHeight()
|
||||
size := f.Base().Size
|
||||
return &ImageFileType{
|
||||
Size: int(size),
|
||||
Width: width,
|
||||
Height: height,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) VisualFiles(ctx context.Context, obj *models.Image) ([]models.VisualFile, error) {
|
||||
func (r *imageResolver) VisualFiles(ctx context.Context, obj *models.Image) ([]VisualFile, error) {
|
||||
files, err := r.getFiles(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]models.VisualFile, len(files))
|
||||
ret := make([]VisualFile, len(files))
|
||||
for i, f := range files {
|
||||
ret[i], err = convertVisualFile(f)
|
||||
if err != nil {
|
||||
@@ -91,13 +48,13 @@ func (r *imageResolver) Date(ctx context.Context, obj *models.Image) (*string, e
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*models.ImageFile, error) {
|
||||
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) {
|
||||
files, err := r.getFiles(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []*models.ImageFile
|
||||
var ret []*ImageFile
|
||||
|
||||
for _, f := range files {
|
||||
// filter out non-image files
|
||||
@@ -106,24 +63,14 @@ func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*models
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, imageFile)
|
||||
ret = append(ret, &ImageFile{
|
||||
ImageFile: imageFile,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) FileModTime(ctx context.Context, obj *models.Image) (*time.Time, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f != nil {
|
||||
return &f.Base().ModTime, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*ImagePathsType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
builder := urlbuilders.NewImageURLBuilder(baseURL, obj)
|
||||
@@ -151,14 +98,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -5,15 +5,9 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/hash/md5"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *movieResolver) Checksum(ctx context.Context, obj *models.Movie) (string, error) {
|
||||
// generate checksum from movie name
|
||||
return md5.FromString(obj.Name), nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
if obj.Date != nil {
|
||||
result := obj.Date.String()
|
||||
@@ -22,14 +16,6 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
@@ -13,24 +12,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
)
|
||||
|
||||
// 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.Loaded() {
|
||||
if err := r.withReadTxn(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.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -186,14 +167,6 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer)
|
||||
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Rating100(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
61
internal/api/resolver_model_plugin.go
Normal file
61
internal/api/resolver_model_plugin.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
)
|
||||
|
||||
type pluginURLBuilder struct {
|
||||
BaseURL string
|
||||
Plugin *plugin.Plugin
|
||||
}
|
||||
|
||||
func (b pluginURLBuilder) javascript() []string {
|
||||
ui := b.Plugin.UI
|
||||
if len(ui.Javascript) == 0 && len(ui.ExternalScript) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ret []string
|
||||
|
||||
ret = append(ret, ui.ExternalScript...)
|
||||
ret = append(ret, b.BaseURL+"/plugin/"+b.Plugin.ID+"/javascript")
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (b pluginURLBuilder) css() []string {
|
||||
ui := b.Plugin.UI
|
||||
if len(ui.CSS) == 0 && len(ui.ExternalCSS) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ret []string
|
||||
|
||||
ret = append(ret, b.Plugin.UI.ExternalCSS...)
|
||||
ret = append(ret, b.BaseURL+"/plugin/"+b.Plugin.ID+"/css")
|
||||
return ret
|
||||
}
|
||||
|
||||
func (b *pluginURLBuilder) paths() *PluginPaths {
|
||||
return &PluginPaths{
|
||||
Javascript: b.javascript(),
|
||||
CSS: b.css(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *pluginResolver) Paths(ctx context.Context, obj *plugin.Plugin) (*PluginPaths, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
b := pluginURLBuilder{
|
||||
BaseURL: baseURL,
|
||||
Plugin: obj,
|
||||
}
|
||||
|
||||
return b.paths(), nil
|
||||
}
|
||||
|
||||
func (r *pluginResolver) Requires(ctx context.Context, obj *plugin.Plugin) ([]string, error) {
|
||||
return obj.UI.Requires, nil
|
||||
}
|
||||
@@ -3,14 +3,11 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func convertVideoFile(f models.File) (*models.VideoFile, error) {
|
||||
@@ -68,18 +65,6 @@ func (r *sceneResolver) getFiles(ctx context.Context, obj *models.Scene) ([]*mod
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) FileModTime(ctx context.Context, obj *models.Scene) (*time.Time, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f != nil {
|
||||
return &f.ModTime, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Date(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
if obj.Date != nil {
|
||||
result := obj.Date.String()
|
||||
@@ -88,38 +73,21 @@ func (r *sceneResolver) Date(ctx context.Context, obj *models.Scene) (*string, e
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// File is deprecated
|
||||
func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.SceneFileType, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
bitrate := int(f.BitRate)
|
||||
size := strconv.FormatInt(f.Size, 10)
|
||||
|
||||
return &models.SceneFileType{
|
||||
Size: &size,
|
||||
Duration: handleFloat64(f.Duration),
|
||||
VideoCodec: &f.VideoCodec,
|
||||
AudioCodec: &f.AudioCodec,
|
||||
Width: &f.Width,
|
||||
Height: &f.Height,
|
||||
Framerate: handleFloat64(f.FrameRate),
|
||||
Bitrate: &bitrate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*models.VideoFile, error) {
|
||||
func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoFile, error) {
|
||||
files, err := r.getFiles(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
ret := make([]*VideoFile, len(files))
|
||||
|
||||
for i, f := range files {
|
||||
ret[i] = &VideoFile{
|
||||
VideoFile: f,
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
@@ -145,7 +113,6 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
|
||||
objHash := obj.GetHash(config.GetVideoFileNamingAlgorithm())
|
||||
vttPath := builder.GetSpriteVTTURL(objHash)
|
||||
spritePath := builder.GetSpriteURL(objHash)
|
||||
chaptersVttPath := builder.GetChaptersVTTURL()
|
||||
funscriptPath := builder.GetFunscriptURL()
|
||||
captionBasePath := builder.GetCaptionURL()
|
||||
interactiveHeatmap := builder.GetInteractiveHeatmapURL()
|
||||
@@ -156,7 +123,6 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
|
||||
Stream: &streamPath,
|
||||
Webp: &webpPath,
|
||||
Vtt: &vttPath,
|
||||
ChaptersVtt: &chaptersVttPath,
|
||||
Sprite: &spritePath,
|
||||
Funscript: &funscriptPath,
|
||||
InteractiveHeatmap: &interactiveHeatmap,
|
||||
@@ -285,30 +251,6 @@ func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []
|
||||
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Phash(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
val := f.Fingerprints.Get(models.FingerprintTypePhash)
|
||||
if val == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
phash, _ := val.(int64)
|
||||
|
||||
if phash != 0 {
|
||||
hexval := utils.PhashToString(phash)
|
||||
return &hexval, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) SceneStreams(ctx context.Context, obj *models.Scene) ([]*manager.SceneStreamEndpoint, error) {
|
||||
// load the primary file into the scene
|
||||
_, err := r.getPrimaryFile(ctx, obj)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/hash/md5"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/movie"
|
||||
@@ -14,11 +13,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
)
|
||||
|
||||
func (r *studioResolver) Checksum(ctx context.Context, obj *models.Studio) (string, error) {
|
||||
// generate checksum from studio name
|
||||
return md5.FromString(obj.Name), nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*string, error) {
|
||||
var hasImage bool
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -132,14 +126,6 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*m
|
||||
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
}
|
||||
|
||||
refreshScraperCache := false
|
||||
refreshScraperSource := false
|
||||
existingScrapersPath := c.GetScrapersPath()
|
||||
if input.ScrapersPath != nil && existingScrapersPath != *input.ScrapersPath {
|
||||
if err := validateDir(config.ScrapersPath, *input.ScrapersPath, false); err != nil {
|
||||
@@ -111,9 +112,23 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
}
|
||||
|
||||
refreshScraperCache = true
|
||||
refreshScraperSource = true
|
||||
c.Set(config.ScrapersPath, input.ScrapersPath)
|
||||
}
|
||||
|
||||
refreshPluginCache := false
|
||||
refreshPluginSource := false
|
||||
existingPluginsPath := c.GetPluginsPath()
|
||||
if input.PluginsPath != nil && existingPluginsPath != *input.PluginsPath {
|
||||
if err := validateDir(config.PluginsPath, *input.PluginsPath, false); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
refreshPluginCache = true
|
||||
refreshPluginSource = true
|
||||
c.Set(config.PluginsPath, input.PluginsPath)
|
||||
}
|
||||
|
||||
existingMetadataPath := c.GetMetadataPath()
|
||||
if input.MetadataPath != nil && existingMetadataPath != *input.MetadataPath {
|
||||
if err := validateDir(config.Metadata, *input.MetadataPath, true); err != nil {
|
||||
@@ -316,21 +331,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
|
||||
if input.CustomPerformerImageLocation != nil {
|
||||
c.Set(config.CustomPerformerImageLocation, *input.CustomPerformerImageLocation)
|
||||
initialiseCustomImages()
|
||||
}
|
||||
|
||||
if input.ScraperUserAgent != nil {
|
||||
c.Set(config.ScraperUserAgent, input.ScraperUserAgent)
|
||||
refreshScraperCache = true
|
||||
}
|
||||
|
||||
if input.ScraperCDPPath != nil {
|
||||
c.Set(config.ScraperCDPPath, input.ScraperCDPPath)
|
||||
refreshScraperCache = true
|
||||
}
|
||||
|
||||
if input.ScraperCertCheck != nil {
|
||||
c.Set(config.ScraperCertCheck, input.ScraperCertCheck)
|
||||
initCustomPerformerImages(*input.CustomPerformerImageLocation)
|
||||
}
|
||||
|
||||
if input.StashBoxes != nil {
|
||||
@@ -361,6 +362,16 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
c.Set(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
|
||||
}
|
||||
|
||||
if input.ScraperPackageSources != nil {
|
||||
c.Set(config.ScraperPackageSources, input.ScraperPackageSources)
|
||||
refreshScraperSource = true
|
||||
}
|
||||
|
||||
if input.PluginPackageSources != nil {
|
||||
c.Set(config.PluginPackageSources, input.PluginPackageSources)
|
||||
refreshPluginSource = true
|
||||
}
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
@@ -369,12 +380,21 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
if refreshScraperCache {
|
||||
manager.GetInstance().RefreshScraperCache()
|
||||
}
|
||||
if refreshPluginCache {
|
||||
manager.GetInstance().RefreshPluginCache()
|
||||
}
|
||||
if refreshStreamManager {
|
||||
manager.GetInstance().RefreshStreamManager()
|
||||
}
|
||||
if refreshBlobStorage {
|
||||
manager.GetInstance().SetBlobStoreOptions()
|
||||
}
|
||||
if refreshScraperSource {
|
||||
manager.GetInstance().RefreshScraperSourceManager()
|
||||
}
|
||||
if refreshPluginSource {
|
||||
manager.GetInstance().RefreshPluginSourceManager()
|
||||
}
|
||||
|
||||
return makeConfigGeneralResult(), nil
|
||||
}
|
||||
@@ -424,11 +444,6 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
|
||||
c.Set(config.Language, *input.Language)
|
||||
}
|
||||
|
||||
// deprecated field
|
||||
if input.SlideshowDelay != nil {
|
||||
c.Set(config.ImageLightboxSlideshowDelay, *input.SlideshowDelay)
|
||||
}
|
||||
|
||||
if input.ImageLightbox != nil {
|
||||
options := input.ImageLightbox
|
||||
|
||||
@@ -506,19 +521,10 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAIn
|
||||
c.Set(config.DLNAVideoSortOrder, input.VideoSortOrder)
|
||||
}
|
||||
|
||||
currentDLNAEnabled := c.GetDLNADefaultEnabled()
|
||||
if input.Enabled != nil && *input.Enabled != currentDLNAEnabled {
|
||||
refresh := false
|
||||
if input.Enabled != nil {
|
||||
c.Set(config.DLNADefaultEnabled, *input.Enabled)
|
||||
|
||||
// start/stop the DLNA service as needed
|
||||
dlnaService := manager.GetInstance().DLNAService
|
||||
if !*input.Enabled && dlnaService.IsRunning() {
|
||||
dlnaService.Stop(nil)
|
||||
} else if *input.Enabled && !dlnaService.IsRunning() {
|
||||
if err := dlnaService.Start(nil); err != nil {
|
||||
logger.Warnf("error starting DLNA service: %v", err)
|
||||
}
|
||||
}
|
||||
refresh = true
|
||||
}
|
||||
|
||||
if input.Interfaces != nil {
|
||||
@@ -529,6 +535,10 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAIn
|
||||
return makeConfigDLNAResult(), err
|
||||
}
|
||||
|
||||
if refresh {
|
||||
manager.GetInstance().RefreshDLNA()
|
||||
}
|
||||
|
||||
return makeConfigDLNAResult(), nil
|
||||
}
|
||||
|
||||
@@ -648,3 +658,14 @@ func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, v
|
||||
|
||||
return r.ConfigureUI(ctx, cfg)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigurePlugin(ctx context.Context, pluginID string, input map[string]interface{}) (map[string]interface{}, error) {
|
||||
c := config.GetInstance()
|
||||
c.SetPluginConfiguration(pluginID, input)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return c.GetPluginConfiguration(pluginID), err
|
||||
}
|
||||
|
||||
return c.GetPluginConfiguration(pluginID), nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
|
||||
fileStore := r.repository.File
|
||||
folderStore := r.repository.Folder
|
||||
mover := file.NewMover(fileStore, folderStore)
|
||||
mover.RegisterHooks(ctx, r.txnManager)
|
||||
mover.RegisterHooks(ctx)
|
||||
|
||||
var (
|
||||
folder *models.Folder
|
||||
@@ -207,3 +207,68 @@ func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret b
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) FileSetFingerprints(ctx context.Context, input FileSetFingerprintsInput) (bool, error) {
|
||||
fileIDInt, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
fileID := models.FileID(fileIDInt)
|
||||
|
||||
// determine what we're doing
|
||||
var (
|
||||
fingerprints []models.Fingerprint
|
||||
toDelete []string
|
||||
)
|
||||
|
||||
for _, i := range input.Fingerprints {
|
||||
if i.Type == models.FingerprintTypeMD5 || i.Type == models.FingerprintTypeOshash {
|
||||
return false, fmt.Errorf("cannot modify %s fingerprint", i.Type)
|
||||
}
|
||||
|
||||
if i.Value == nil {
|
||||
toDelete = append(toDelete, i.Type)
|
||||
} else {
|
||||
// phashes need to be converted from string into uint64
|
||||
var v interface{}
|
||||
v = *i.Value
|
||||
|
||||
if i.Type == models.FingerprintTypePhash {
|
||||
vInt, err := strconv.ParseUint(*i.Value, 16, 64)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting phash %s: %w", *i.Value, err)
|
||||
}
|
||||
|
||||
v = vInt
|
||||
}
|
||||
|
||||
fingerprints = append(fingerprints, models.Fingerprint{
|
||||
Type: i.Type,
|
||||
Fingerprint: v,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.File
|
||||
|
||||
if len(fingerprints) > 0 {
|
||||
if err := qb.ModifyFingerprints(ctx, fileID, fingerprints); err != nil {
|
||||
return fmt.Errorf("modifying fingerprints: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(toDelete) > 0 {
|
||||
if err := qb.DestroyFingerprints(ctx, fileID, toDelete); err != nil {
|
||||
return fmt.Errorf("destroying fingerprints: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -43,8 +43,10 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
||||
newGallery := models.NewGallery()
|
||||
|
||||
newGallery.Title = input.Title
|
||||
newGallery.Code = translator.string(input.Code)
|
||||
newGallery.Details = translator.string(input.Details)
|
||||
newGallery.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
newGallery.Photographer = translator.string(input.Photographer)
|
||||
newGallery.Rating = input.Rating100
|
||||
|
||||
var err error
|
||||
|
||||
@@ -182,8 +184,10 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||
updatedGallery.Title = models.NewOptionalString(*input.Title)
|
||||
}
|
||||
|
||||
updatedGallery.Code = translator.optionalString(input.Code, "code")
|
||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||
updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedGallery.Photographer = translator.optionalString(input.Photographer, "photographer")
|
||||
updatedGallery.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
updatedGallery.Date, err = translator.optionalDate(input.Date, "date")
|
||||
@@ -257,8 +261,10 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
|
||||
// Populate gallery from the input
|
||||
updatedGallery := models.NewGalleryPartial()
|
||||
|
||||
updatedGallery.Code = translator.optionalString(input.Code, "code")
|
||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||
updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedGallery.Photographer = translator.optionalString(input.Photographer, "photographer")
|
||||
updatedGallery.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
updatedGallery.URLs = translator.optionalURLsBulk(input.Urls, input.URL)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
@@ -107,7 +107,10 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
||||
updatedImage := models.NewImagePartial()
|
||||
|
||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||
updatedImage.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedImage.Code = translator.optionalString(input.Code, "code")
|
||||
updatedImage.Details = translator.optionalString(input.Details, "details")
|
||||
updatedImage.Photographer = translator.optionalString(input.Photographer, "photographer")
|
||||
updatedImage.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
updatedImage.Date, err = translator.optionalDate(input.Date, "date")
|
||||
@@ -203,7 +206,10 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
updatedImage := models.NewImagePartial()
|
||||
|
||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||
updatedImage.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedImage.Code = translator.optionalString(input.Code, "code")
|
||||
updatedImage.Details = translator.optionalString(input.Details, "details")
|
||||
updatedImage.Photographer = translator.optionalString(input.Photographer, "photographer")
|
||||
updatedImage.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
updatedImage.Date, err = translator.optionalDate(input.Date, "date")
|
||||
@@ -256,7 +262,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
}
|
||||
|
||||
thisUpdatedGalleryIDs := updatedImage.GalleryIDs.ImpactedIDs(i.GalleryIDs.List())
|
||||
updatedGalleryIDs = intslice.IntAppendUniques(updatedGalleryIDs, thisUpdatedGalleryIDs)
|
||||
updatedGalleryIDs = sliceutil.AppendUniques(updatedGalleryIDs, thisUpdatedGalleryIDs)
|
||||
}
|
||||
|
||||
image, err := qb.UpdatePartial(ctx, imageID, updatedImage)
|
||||
|
||||
@@ -3,8 +3,6 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -12,7 +10,6 @@ import (
|
||||
"github.com/stashapp/stash/internal/identify"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -110,31 +107,10 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
||||
// if download is true, then backup to temporary file and return a link
|
||||
download := input.Download != nil && *input.Download
|
||||
mgr := manager.GetInstance()
|
||||
database := mgr.Database
|
||||
var backupPath string
|
||||
if download {
|
||||
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
|
||||
}
|
||||
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "backup*.sqlite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backupPath = 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)
|
||||
}
|
||||
}
|
||||
backupPath = database.DatabaseBackupPath(backupDirectoryPath)
|
||||
}
|
||||
|
||||
err := database.Backup(backupPath)
|
||||
backupPath, backupName, err := mgr.BackupDatabase(download)
|
||||
if err != nil {
|
||||
logger.Errorf("Error backing up database: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -147,8 +123,7 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
fn := filepath.Base(database.DatabaseBackupPath(""))
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + backupName
|
||||
return &ret, nil
|
||||
} else {
|
||||
logger.Infof("Successfully backed up database to: %s", backupPath)
|
||||
@@ -158,33 +133,11 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input AnonymiseDatabaseInput) (*string, error) {
|
||||
// if download is true, then backup to temporary file and return a link
|
||||
// if download is true, then save 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)
|
||||
outPath, outName, err := mgr.AnonymiseDatabase(download)
|
||||
if err != nil {
|
||||
logger.Errorf("Error anonymising database: %v", err)
|
||||
return nil, err
|
||||
@@ -199,8 +152,7 @@ func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input Anonymis
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
fn := filepath.Base(database.DatabaseBackupPath(""))
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + outName
|
||||
return &ret, nil
|
||||
} else {
|
||||
logger.Infof("Successfully anonymised database to: %s", outPath)
|
||||
|
||||
@@ -11,30 +11,30 @@ import (
|
||||
)
|
||||
|
||||
func (r *mutationResolver) MigrateSceneScreenshots(ctx context.Context, input MigrateSceneScreenshotsInput) (string, error) {
|
||||
db := manager.GetInstance().Database
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.MigrateSceneScreenshotsJob{
|
||||
ScreenshotsPath: manager.GetInstance().Paths.Generated.Screenshots,
|
||||
Input: scene.MigrateSceneScreenshotsInput{
|
||||
DeleteFiles: utils.IsTrue(input.DeleteFiles),
|
||||
OverwriteExisting: utils.IsTrue(input.OverwriteExisting),
|
||||
},
|
||||
SceneRepo: db.Scene,
|
||||
TxnManager: db,
|
||||
SceneRepo: mgr.Repository.Scene,
|
||||
TxnManager: mgr.Repository.TxnManager,
|
||||
}
|
||||
jobID := manager.GetInstance().JobManager.Add(ctx, "Migrating scene screenshots to blobs...", t)
|
||||
jobID := mgr.JobManager.Add(ctx, "Migrating scene screenshots to blobs...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MigrateBlobs(ctx context.Context, input MigrateBlobsInput) (string, error) {
|
||||
db := manager.GetInstance().Database
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.MigrateBlobsJob{
|
||||
TxnManager: db,
|
||||
BlobStore: db.Blobs,
|
||||
Vacuumer: db,
|
||||
TxnManager: mgr.Database,
|
||||
BlobStore: mgr.Database.Blobs,
|
||||
Vacuumer: mgr.Database,
|
||||
DeleteOld: utils.IsTrue(input.DeleteOld),
|
||||
}
|
||||
jobID := manager.GetInstance().JobManager.Add(ctx, "Migrating blobs...", t)
|
||||
jobID := mgr.JobManager.Add(ctx, "Migrating blobs...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
@@ -34,7 +35,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
|
||||
newMovie.Name = input.Name
|
||||
newMovie.Aliases = translator.string(input.Aliases)
|
||||
newMovie.Duration = input.Duration
|
||||
newMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
newMovie.Rating = input.Rating100
|
||||
newMovie.Director = translator.string(input.Director)
|
||||
newMovie.Synopsis = translator.string(input.Synopsis)
|
||||
newMovie.URL = translator.string(input.URL)
|
||||
@@ -50,12 +51,6 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
// HACK: if back image is being set, set the front image to the default.
|
||||
// This is because we can't have a null front image with a non-null back image.
|
||||
if input.FrontImage == nil && input.BackImage != nil {
|
||||
input.FrontImage = &models.DefaultMovieImage
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var frontimageData []byte
|
||||
if input.FrontImage != nil {
|
||||
@@ -74,6 +69,12 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: if back image is being set, set the front image to the default.
|
||||
// This is because we can't have a null front image with a non-null back image.
|
||||
if len(frontimageData) == 0 && len(backimageData) != 0 {
|
||||
frontimageData = static.ReadAll(static.DefaultMovieImage)
|
||||
}
|
||||
|
||||
// Start the transaction and save the movie
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Movie
|
||||
@@ -121,7 +122,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
|
||||
updatedMovie.Name = translator.optionalString(input.Name, "name")
|
||||
updatedMovie.Aliases = translator.optionalString(input.Aliases, "aliases")
|
||||
updatedMovie.Duration = translator.optionalInt(input.Duration, "duration")
|
||||
updatedMovie.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedMovie.Director = translator.optionalString(input.Director, "director")
|
||||
updatedMovie.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
|
||||
updatedMovie.URL = translator.optionalString(input.URL, "url")
|
||||
@@ -197,7 +198,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
|
||||
// Populate movie from the input
|
||||
updatedMovie := models.NewMoviePartial()
|
||||
|
||||
updatedMovie.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedMovie.Director = translator.optionalString(input.Director, "director")
|
||||
|
||||
updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
|
||||
77
internal/api/resolver_mutation_package.go
Normal file
77
internal/api/resolver_mutation_package.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/task"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func refreshPackageType(typeArg PackageType) {
|
||||
mgr := manager.GetInstance()
|
||||
|
||||
if typeArg == PackageTypePlugin {
|
||||
mgr.RefreshPluginCache()
|
||||
} else if typeArg == PackageTypeScraper {
|
||||
mgr.RefreshScraperCache()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) InstallPackages(ctx context.Context, typeArg PackageType, packages []*models.PackageSpecInput) (string, error) {
|
||||
pm, err := getPackageManager(typeArg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.InstallPackagesJob{
|
||||
PackagesJob: task.PackagesJob{
|
||||
PackageManager: pm,
|
||||
OnComplete: func() { refreshPackageType(typeArg) },
|
||||
},
|
||||
Packages: packages,
|
||||
}
|
||||
jobID := mgr.JobManager.Add(ctx, "Installing packages...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) UpdatePackages(ctx context.Context, typeArg PackageType, packages []*models.PackageSpecInput) (string, error) {
|
||||
pm, err := getPackageManager(typeArg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.UpdatePackagesJob{
|
||||
PackagesJob: task.PackagesJob{
|
||||
PackageManager: pm,
|
||||
OnComplete: func() { refreshPackageType(typeArg) },
|
||||
},
|
||||
Packages: packages,
|
||||
}
|
||||
jobID := mgr.JobManager.Add(ctx, "Updating packages...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) UninstallPackages(ctx context.Context, typeArg PackageType, packages []*models.PackageSpecInput) (string, error) {
|
||||
pm, err := getPackageManager(typeArg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.UninstallPackagesJob{
|
||||
PackagesJob: task.PackagesJob{
|
||||
PackageManager: pm,
|
||||
OnComplete: func() { refreshPackageType(typeArg) },
|
||||
},
|
||||
Packages: packages,
|
||||
}
|
||||
jobID := mgr.JobManager.Add(ctx, "Updating packages...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
@@ -34,6 +34,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
|
||||
newPerformer.Name = input.Name
|
||||
newPerformer.Disambiguation = translator.string(input.Disambiguation)
|
||||
newPerformer.Aliases = models.NewRelatedStrings(input.AliasList)
|
||||
newPerformer.URL = translator.string(input.URL)
|
||||
newPerformer.Gender = input.Gender
|
||||
newPerformer.Ethnicity = translator.string(input.Ethnicity)
|
||||
@@ -49,9 +50,10 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
newPerformer.Twitter = translator.string(input.Twitter)
|
||||
newPerformer.Instagram = translator.string(input.Instagram)
|
||||
newPerformer.Favorite = translator.bool(input.Favorite)
|
||||
newPerformer.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
newPerformer.Rating = input.Rating100
|
||||
newPerformer.Details = translator.string(input.Details)
|
||||
newPerformer.HairColor = translator.string(input.HairColor)
|
||||
newPerformer.Height = input.HeightCm
|
||||
newPerformer.Weight = input.Weight
|
||||
newPerformer.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds)
|
||||
@@ -67,34 +69,11 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
return nil, fmt.Errorf("converting death date: %w", err)
|
||||
}
|
||||
|
||||
// prefer height_cm over height
|
||||
if input.HeightCm != nil {
|
||||
newPerformer.Height = input.HeightCm
|
||||
} else {
|
||||
newPerformer.Height, err = translator.intPtrFromString(input.Height)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting height: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// prefer alias_list over aliases
|
||||
if input.AliasList != nil {
|
||||
newPerformer.Aliases = models.NewRelatedStrings(input.AliasList)
|
||||
} else if input.Aliases != nil {
|
||||
newPerformer.Aliases = models.NewRelatedStrings(stringslice.FromString(*input.Aliases, ","))
|
||||
}
|
||||
|
||||
newPerformer.TagIDs, err = translator.relatedIds(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
if err := performer.ValidateDeathDate(nil, input.Birthdate, input.DeathDate); err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var imageData []byte
|
||||
if input.Image != nil {
|
||||
@@ -108,6 +87,10 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
|
||||
if err := performer.ValidateCreate(ctx, newPerformer, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = qb.Create(ctx, &newPerformer)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -159,7 +142,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedPerformer.Details = translator.optionalString(input.Details, "details")
|
||||
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
|
||||
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
|
||||
@@ -178,22 +161,11 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
// 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, fmt.Errorf("converting height: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// prefer alias_list over aliases
|
||||
if translator.hasField("alias_list") {
|
||||
updatedPerformer.Aliases = translator.updateStrings(input.AliasList, "alias_list")
|
||||
} else if translator.hasField("aliases") {
|
||||
var aliasList []string
|
||||
if input.Aliases != nil {
|
||||
aliasList = stringslice.FromString(*input.Aliases, ",")
|
||||
}
|
||||
updatedPerformer.Aliases = translator.updateStrings(aliasList, "aliases")
|
||||
}
|
||||
|
||||
updatedPerformer.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
|
||||
@@ -214,17 +186,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
|
||||
// need to get existing performer
|
||||
existing, err := qb.Find(ctx, performerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
return fmt.Errorf("performer with id %d not found", performerID)
|
||||
}
|
||||
|
||||
if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil {
|
||||
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -278,7 +240,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedPerformer.Details = translator.optionalString(input.Details, "details")
|
||||
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
|
||||
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
|
||||
@@ -296,22 +258,11 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
// 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, fmt.Errorf("converting height: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// prefer alias_list over aliases
|
||||
if translator.hasField("alias_list") {
|
||||
updatedPerformer.Aliases = translator.updateStringsBulk(input.AliasList, "alias_list")
|
||||
} else if translator.hasField("aliases") {
|
||||
var aliasList []string
|
||||
if input.Aliases != nil {
|
||||
aliasList = stringslice.FromString(*input.Aliases, ",")
|
||||
}
|
||||
updatedPerformer.Aliases = translator.updateStrings(aliasList, "aliases")
|
||||
}
|
||||
|
||||
updatedPerformer.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
|
||||
@@ -326,18 +277,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
qb := r.repository.Performer
|
||||
|
||||
for _, performerID := range performerIDs {
|
||||
// need to get existing performer
|
||||
existing, err := qb.Find(ctx, performerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
return fmt.Errorf("performer with id %d not found", performerID)
|
||||
}
|
||||
|
||||
err = performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate)
|
||||
if err != nil {
|
||||
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, taskName string, args []*plugin.PluginArgInput) (string, error) {
|
||||
@@ -15,9 +16,34 @@ func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, t
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) {
|
||||
err := manager.GetInstance().PluginCache.LoadPlugins()
|
||||
if err != nil {
|
||||
logger.Errorf("Error reading plugin configs: %v", err)
|
||||
manager.GetInstance().RefreshPluginCache()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SetPluginsEnabled(ctx context.Context, enabledMap map[string]bool) (bool, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
existingDisabled := c.GetDisabledPlugins()
|
||||
var newDisabled []string
|
||||
|
||||
// remove plugins that are no longer disabled
|
||||
for _, disabledID := range existingDisabled {
|
||||
if enabled, found := enabledMap[disabledID]; !enabled || !found {
|
||||
newDisabled = append(newDisabled, disabledID)
|
||||
}
|
||||
}
|
||||
|
||||
// add plugins that are newly disabled
|
||||
for pluginID, enabled := range enabledMap {
|
||||
if !enabled {
|
||||
newDisabled = sliceutil.AppendUnique(newDisabled, pluginID)
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(config.DisabledPlugins, newDisabled)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
@@ -45,7 +45,7 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr
|
||||
newScene.Code = translator.string(input.Code)
|
||||
newScene.Details = translator.string(input.Details)
|
||||
newScene.Director = translator.string(input.Director)
|
||||
newScene.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
newScene.Rating = input.Rating100
|
||||
newScene.Organized = translator.bool(input.Organized)
|
||||
newScene.StashIDs = models.NewRelatedStashIDs(input.StashIds)
|
||||
|
||||
@@ -168,7 +168,7 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
|
||||
updatedScene.Code = translator.optionalString(input.Code, "code")
|
||||
updatedScene.Details = translator.optionalString(input.Details, "details")
|
||||
updatedScene.Director = translator.optionalString(input.Director, "director")
|
||||
updatedScene.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedScene.Rating = translator.optionalInt(input.Rating100, "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")
|
||||
@@ -321,7 +321,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
|
||||
updatedScene.Code = translator.optionalString(input.Code, "code")
|
||||
updatedScene.Details = translator.optionalString(input.Details, "details")
|
||||
updatedScene.Director = translator.optionalString(input.Director, "director")
|
||||
updatedScene.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedScene.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedScene.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
updatedScene.Date, err = translator.optionalDate(input.Date, "date")
|
||||
@@ -627,7 +627,7 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
|
||||
|
||||
// Save the marker tags
|
||||
// If this tag is the primary tag, then let's not add it.
|
||||
tagIDs = intslice.IntExclude(tagIDs, []int{newMarker.PrimaryTagID})
|
||||
tagIDs = sliceutil.Exclude(tagIDs, []int{newMarker.PrimaryTagID})
|
||||
return qb.UpdateTags(ctx, newMarker.ID, tagIDs)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -716,7 +716,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
|
||||
if tagIdsIncluded {
|
||||
// Save the marker tags
|
||||
// If this tag is the primary tag, then let's not add it.
|
||||
tagIDs = intslice.IntExclude(tagIDs, []int{newMarker.PrimaryTagID})
|
||||
tagIDs = sliceutil.Exclude(tagIDs, []int{newMarker.PrimaryTagID})
|
||||
if err := qb.UpdateTags(ctx, markerID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,11 +7,6 @@ import (
|
||||
)
|
||||
|
||||
func (r *mutationResolver) ReloadScrapers(ctx context.Context) (bool, error) {
|
||||
err := manager.GetInstance().ScraperCache.ReloadScrapers()
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
manager.GetInstance().RefreshScraperCache()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -11,15 +11,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
)
|
||||
|
||||
func (r *Resolver) stashboxRepository() stashbox.Repository {
|
||||
return stashbox.Repository{
|
||||
Scene: r.repository.Scene,
|
||||
Performer: r.repository.Performer,
|
||||
Tag: r.repository.Tag,
|
||||
Studio: r.repository.Studio,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input StashBoxFingerprintSubmissionInput) (bool, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
@@ -27,7 +18,7 @@ func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input
|
||||
return false, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
|
||||
|
||||
return client.SubmitStashBoxFingerprints(ctx, input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
|
||||
}
|
||||
@@ -49,7 +40,7 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
|
||||
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
|
||||
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -91,7 +82,7 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
|
||||
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
|
||||
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -34,7 +34,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
|
||||
newStudio.Name = input.Name
|
||||
newStudio.URL = translator.string(input.URL)
|
||||
newStudio.Rating = translator.ratingConversion(input.Rating, input.Rating100)
|
||||
newStudio.Rating = input.Rating100
|
||||
newStudio.Details = translator.string(input.Details)
|
||||
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
newStudio.Aliases = models.NewRelatedStrings(input.Aliases)
|
||||
@@ -61,6 +61,10 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Studio
|
||||
|
||||
if err := studio.EnsureStudioNameUnique(ctx, 0, newStudio.Name, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(input.Aliases) > 0 {
|
||||
if err := studio.EnsureAliasesUnique(ctx, 0, input.Aliases, qb); err != nil {
|
||||
return err
|
||||
@@ -104,7 +108,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
updatedStudio.Name = translator.optionalString(input.Name, "name")
|
||||
updatedStudio.URL = translator.optionalString(input.URL, "url")
|
||||
updatedStudio.Details = translator.optionalString(input.Details, "details")
|
||||
updatedStudio.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
|
||||
updatedStudio.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases")
|
||||
updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
@@ -15,14 +14,9 @@ import (
|
||||
)
|
||||
|
||||
// TODO - move this into a common area
|
||||
func newResolver() *Resolver {
|
||||
txnMgr := &mocks.TxnManager{}
|
||||
func newResolver(db *mocks.Database) *Resolver {
|
||||
return &Resolver{
|
||||
txnManager: txnMgr,
|
||||
repository: manager.Repository{
|
||||
TxnManager: txnMgr,
|
||||
Tag: &mocks.TagReaderWriter{},
|
||||
},
|
||||
repository: db.Repository(),
|
||||
hookExecutor: &mockHookExecutor{},
|
||||
}
|
||||
}
|
||||
@@ -45,9 +39,8 @@ func (*mockHookExecutor) ExecutePostHooks(ctx context.Context, id int, hookType
|
||||
}
|
||||
|
||||
func TestTagCreate(t *testing.T) {
|
||||
r := newResolver()
|
||||
|
||||
tagRW := r.repository.Tag.(*mocks.TagReaderWriter)
|
||||
db := mocks.NewDatabase()
|
||||
r := newResolver(db)
|
||||
|
||||
pp := 1
|
||||
findFilter := &models.FindFilterType{
|
||||
@@ -72,17 +65,17 @@ func TestTagCreate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
tagRW.On("Query", mock.Anything, tagFilterForName(existingTagName), findFilter).Return([]*models.Tag{
|
||||
db.Tag.On("Query", mock.Anything, tagFilterForName(existingTagName), findFilter).Return([]*models.Tag{
|
||||
{
|
||||
ID: existingTagID,
|
||||
Name: existingTagName,
|
||||
},
|
||||
}, 1, nil).Once()
|
||||
tagRW.On("Query", mock.Anything, tagFilterForName(errTagName), findFilter).Return(nil, 0, nil).Once()
|
||||
tagRW.On("Query", mock.Anything, tagFilterForAlias(errTagName), findFilter).Return(nil, 0, nil).Once()
|
||||
db.Tag.On("Query", mock.Anything, tagFilterForName(errTagName), findFilter).Return(nil, 0, nil).Once()
|
||||
db.Tag.On("Query", mock.Anything, tagFilterForAlias(errTagName), findFilter).Return(nil, 0, nil).Once()
|
||||
|
||||
expectedErr := errors.New("TagCreate error")
|
||||
tagRW.On("Create", mock.Anything, mock.AnythingOfType("*models.Tag")).Return(expectedErr)
|
||||
db.Tag.On("Create", mock.Anything, mock.AnythingOfType("*models.Tag")).Return(expectedErr)
|
||||
|
||||
// fails here because testCtx is empty
|
||||
// TODO: Fix this
|
||||
@@ -101,22 +94,22 @@ func TestTagCreate(t *testing.T) {
|
||||
})
|
||||
|
||||
assert.Equal(t, expectedErr, err)
|
||||
tagRW.AssertExpectations(t)
|
||||
db.AssertExpectations(t)
|
||||
|
||||
r = newResolver()
|
||||
tagRW = r.repository.Tag.(*mocks.TagReaderWriter)
|
||||
db = mocks.NewDatabase()
|
||||
r = newResolver(db)
|
||||
|
||||
tagRW.On("Query", mock.Anything, tagFilterForName(tagName), findFilter).Return(nil, 0, nil).Once()
|
||||
tagRW.On("Query", mock.Anything, tagFilterForAlias(tagName), findFilter).Return(nil, 0, nil).Once()
|
||||
db.Tag.On("Query", mock.Anything, tagFilterForName(tagName), findFilter).Return(nil, 0, nil).Once()
|
||||
db.Tag.On("Query", mock.Anything, tagFilterForAlias(tagName), findFilter).Return(nil, 0, nil).Once()
|
||||
newTag := &models.Tag{
|
||||
ID: newTagID,
|
||||
Name: tagName,
|
||||
}
|
||||
tagRW.On("Create", mock.Anything, mock.AnythingOfType("*models.Tag")).Run(func(args mock.Arguments) {
|
||||
db.Tag.On("Create", mock.Anything, mock.AnythingOfType("*models.Tag")).Run(func(args mock.Arguments) {
|
||||
arg := args.Get(1).(*models.Tag)
|
||||
arg.ID = newTagID
|
||||
}).Return(nil)
|
||||
tagRW.On("Find", mock.Anything, newTagID).Return(newTag, nil)
|
||||
db.Tag.On("Find", mock.Anything, newTagID).Return(newTag, nil)
|
||||
|
||||
tag, err := r.Mutation().TagCreate(testCtx, TagCreateInput{
|
||||
Name: tagName,
|
||||
@@ -124,4 +117,5 @@ func TestTagCreate(t *testing.T) {
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, tag)
|
||||
db.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func getDir(path string) string {
|
||||
}
|
||||
|
||||
func getParent(path string) *string {
|
||||
isRoot := path[len(path)-1:] == "/"
|
||||
isRoot := path == "/"
|
||||
if isRoot {
|
||||
return nil
|
||||
} else {
|
||||
@@ -79,9 +79,6 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
||||
|
||||
customPerformerImageLocation := config.GetCustomPerformerImageLocation()
|
||||
|
||||
scraperUserAgent := config.GetScraperUserAgent()
|
||||
scraperCDPPath := config.GetScraperCDPPath()
|
||||
|
||||
return &ConfigGeneralResult{
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
@@ -90,6 +87,7 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
||||
MetadataPath: config.GetMetadataPath(),
|
||||
ConfigFilePath: config.GetConfigFile(),
|
||||
ScrapersPath: config.GetScrapersPath(),
|
||||
PluginsPath: config.GetPluginsPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
BlobsPath: config.GetBlobsPath(),
|
||||
BlobsStorage: config.GetBlobsStorage(),
|
||||
@@ -123,9 +121,6 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
||||
Excludes: config.GetExcludes(),
|
||||
ImageExcludes: config.GetImageExcludes(),
|
||||
CustomPerformerImageLocation: &customPerformerImageLocation,
|
||||
ScraperUserAgent: &scraperUserAgent,
|
||||
ScraperCertCheck: config.GetScraperCertCheck(),
|
||||
ScraperCDPPath: &scraperCDPPath,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
PythonPath: config.GetPythonPath(),
|
||||
TranscodeInputArgs: config.GetTranscodeInputArgs(),
|
||||
@@ -133,6 +128,8 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
||||
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
|
||||
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
|
||||
DrawFunscriptHeatmapRange: config.GetDrawFunscriptHeatmapRange(),
|
||||
ScraperPackageSources: config.GetScraperPackageSources(),
|
||||
PluginPackageSources: config.GetPluginPackageSources(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +158,6 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
|
||||
scriptOffset := config.GetFunscriptOffset()
|
||||
useStashHostedFunscript := config.GetUseStashHostedFunscript()
|
||||
imageLightboxOptions := config.GetImageLightboxOptions()
|
||||
// FIXME - misnamed output field means we have redundant fields
|
||||
disableDropdownCreate := config.GetDisableDropdownCreate()
|
||||
|
||||
return &ConfigInterfaceResult{
|
||||
@@ -187,9 +183,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
|
||||
|
||||
ImageLightbox: &imageLightboxOptions,
|
||||
|
||||
// FIXME - see above
|
||||
DisabledDropdownCreate: disableDropdownCreate,
|
||||
DisableDropdownCreate: disableDropdownCreate,
|
||||
DisableDropdownCreate: disableDropdownCreate,
|
||||
|
||||
HandyKey: &handyKey,
|
||||
FunscriptOffset: &scriptOffset,
|
||||
@@ -243,7 +237,9 @@ func makeConfigUIResult() map[string]interface{} {
|
||||
}
|
||||
|
||||
func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input config.StashBoxInput) (*StashBoxValidationResult, error) {
|
||||
client := stashbox.NewClient(models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}, r.txnManager, r.stashboxRepository())
|
||||
box := models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}
|
||||
client := stashbox.NewClient(box, r.stashboxRepository())
|
||||
|
||||
user, err := client.GetUser(ctx)
|
||||
|
||||
valid := user != nil && user.Me != nil
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
|
||||
@@ -55,11 +55,11 @@ func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.Imag
|
||||
result, err := qb.Query(ctx, models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: stringslice.StrInclude(fields, "count"),
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
},
|
||||
ImageFilter: imageFilter,
|
||||
Megapixels: stringslice.StrInclude(fields, "megapixels"),
|
||||
TotalSize: stringslice.StrInclude(fields, "filesize"),
|
||||
Megapixels: sliceutil.Contains(fields, "megapixels"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
|
||||
@@ -105,11 +105,11 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
|
||||
result, err = r.repository.Scene.Query(ctx, models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: stringslice.StrInclude(fields, "count"),
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
},
|
||||
SceneFilter: sceneFilter,
|
||||
TotalDuration: stringslice.StrInclude(fields, "duration"),
|
||||
TotalSize: stringslice.StrInclude(fields, "filesize"),
|
||||
TotalDuration: sliceutil.Contains(fields, "duration"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
})
|
||||
if err == nil {
|
||||
scenes, err = result.Resolve(ctx)
|
||||
@@ -160,11 +160,11 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
|
||||
result, err := r.repository.Scene.Query(ctx, models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: queryFilter,
|
||||
Count: stringslice.StrInclude(fields, "count"),
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
},
|
||||
SceneFilter: sceneFilter,
|
||||
TotalDuration: stringslice.StrInclude(fields, "duration"),
|
||||
TotalSize: stringslice.StrInclude(fields, "filesize"),
|
||||
TotalDuration: sliceutil.Contains(fields, "duration"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -191,16 +191,11 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
|
||||
}
|
||||
|
||||
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config models.SceneParserInput) (ret *SceneParserResultType, err error) {
|
||||
parser := scene.NewFilenameParser(filter, config)
|
||||
repo := scene.NewFilenameParserRepository(r.repository)
|
||||
parser := scene.NewFilenameParser(filter, config, repo)
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
result, count, err := parser.Parse(ctx, scene.FilenameParserRepository{
|
||||
Scene: r.repository.Scene,
|
||||
Performer: r.repository.Performer,
|
||||
Studio: r.repository.Studio,
|
||||
Movie: r.repository.Movie,
|
||||
Tag: r.repository.Tag,
|
||||
})
|
||||
result, count, err := parser.Parse(ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
212
internal/api/resolver_query_package.go
Normal file
212
internal/api/resolver_query_package.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/pkg"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
var ErrInvalidPackageType = errors.New("invalid package type")
|
||||
|
||||
func getPackageManager(typeArg PackageType) (*pkg.Manager, error) {
|
||||
var pm *pkg.Manager
|
||||
switch typeArg {
|
||||
case PackageTypeScraper:
|
||||
pm = manager.GetInstance().ScraperPackageManager
|
||||
case PackageTypePlugin:
|
||||
pm = manager.GetInstance().PluginPackageManager
|
||||
default:
|
||||
return nil, ErrInvalidPackageType
|
||||
}
|
||||
|
||||
if pm == nil {
|
||||
return nil, fmt.Errorf("%s package manager not initialized", typeArg)
|
||||
}
|
||||
|
||||
return pm, nil
|
||||
}
|
||||
|
||||
func manifestToPackage(p pkg.Manifest) *Package {
|
||||
ret := &Package{
|
||||
PackageID: p.ID,
|
||||
Name: p.Name,
|
||||
SourceURL: p.RepositoryURL,
|
||||
}
|
||||
|
||||
if len(p.Version) > 0 {
|
||||
ret.Version = &p.Version
|
||||
}
|
||||
if !p.Date.IsZero() {
|
||||
ret.Date = &p.Date.Time
|
||||
}
|
||||
|
||||
ret.Metadata = p.Metadata
|
||||
if ret.Metadata == nil {
|
||||
ret.Metadata = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func remotePackageToPackage(p pkg.RemotePackage, index pkg.RemotePackageIndex) *Package {
|
||||
ret := &Package{
|
||||
PackageID: p.ID,
|
||||
Name: p.Name,
|
||||
}
|
||||
|
||||
if len(p.Version) > 0 {
|
||||
ret.Version = &p.Version
|
||||
}
|
||||
if !p.Date.IsZero() {
|
||||
ret.Date = &p.Date.Time
|
||||
}
|
||||
|
||||
ret.Metadata = p.Metadata
|
||||
if ret.Metadata == nil {
|
||||
ret.Metadata = make(map[string]interface{})
|
||||
}
|
||||
|
||||
ret.SourceURL = p.Repository.Path()
|
||||
|
||||
for _, r := range p.Requires {
|
||||
// required packages must come from the same source
|
||||
spec := models.PackageSpecInput{
|
||||
ID: r,
|
||||
SourceURL: p.Repository.Path(),
|
||||
}
|
||||
|
||||
req, found := index[spec]
|
||||
if !found {
|
||||
// shouldn't happen, but we'll ignore it
|
||||
continue
|
||||
}
|
||||
|
||||
ret.Requires = append(ret.Requires, remotePackageToPackage(req, index))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func sortedPackageSpecKeys[V any](m map[models.PackageSpecInput]V) []models.PackageSpecInput {
|
||||
// sort keys
|
||||
var keys []models.PackageSpecInput
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
a := keys[i]
|
||||
b := keys[j]
|
||||
|
||||
aID := a.ID
|
||||
bID := b.ID
|
||||
|
||||
if aID == bID {
|
||||
return a.SourceURL < b.SourceURL
|
||||
}
|
||||
|
||||
aIDL := strings.ToLower(aID)
|
||||
bIDL := strings.ToLower(bID)
|
||||
|
||||
if aIDL == bIDL {
|
||||
return aID < bID
|
||||
}
|
||||
|
||||
return aIDL < bIDL
|
||||
})
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func (r *queryResolver) getInstalledPackagesWithUpgrades(ctx context.Context, pm *pkg.Manager) ([]*Package, error) {
|
||||
// get all installed packages
|
||||
installed, err := pm.ListInstalled(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get remotes for all installed packages
|
||||
allRemoteList, err := pm.ListInstalledRemotes(ctx, installed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packageStatusIndex := pkg.MakePackageStatusIndex(installed, allRemoteList)
|
||||
|
||||
ret := make([]*Package, len(packageStatusIndex))
|
||||
i := 0
|
||||
|
||||
for _, k := range sortedPackageSpecKeys(packageStatusIndex) {
|
||||
v := packageStatusIndex[k]
|
||||
p := manifestToPackage(*v.Local)
|
||||
if v.Remote != nil {
|
||||
pp := remotePackageToPackage(*v.Remote, allRemoteList)
|
||||
p.SourcePackage = pp
|
||||
}
|
||||
ret[i] = p
|
||||
i++
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) InstalledPackages(ctx context.Context, typeArg PackageType) ([]*Package, error) {
|
||||
pm, err := getPackageManager(typeArg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []*Package
|
||||
|
||||
if sliceutil.Contains(graphql.CollectAllFields(ctx), "source_package") {
|
||||
ret, err = r.getInstalledPackagesWithUpgrades(ctx, pm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
installed, err := pm.ListInstalled(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = make([]*Package, len(installed))
|
||||
i := 0
|
||||
for _, k := range sortedPackageSpecKeys(installed) {
|
||||
ret[i] = manifestToPackage(installed[k])
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AvailablePackages(ctx context.Context, typeArg PackageType, source string) ([]*Package, error) {
|
||||
pm, err := getPackageManager(typeArg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
available, err := pm.ListRemote(ctx, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]*Package, len(available))
|
||||
i := 0
|
||||
for _, k := range sortedPackageSpecKeys(available) {
|
||||
p := available[k]
|
||||
ret[i] = remotePackageToPackage(p, available)
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
@@ -21,70 +22,10 @@ func (r *queryResolver) ScrapeURL(ctx context.Context, url string, ty scraper.Sc
|
||||
return r.scraperCache().ScrapeURL(ctx, url, ty)
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *queryResolver) ScrapeFreeonesPerformerList(ctx context.Context, query string) ([]string, error) {
|
||||
content, err := r.scraperCache().ScrapeName(ctx, scraper.FreeonesScraperID, query, scraper.ScrapeContentTypePerformer)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
performers, err := marshalScrapedPerformers(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []string
|
||||
for _, p := range performers {
|
||||
if p.Name != nil {
|
||||
ret = append(ret, *p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ListScrapers(ctx context.Context, types []scraper.ScrapeContentType) ([]*scraper.Scraper, error) {
|
||||
return r.scraperCache().ListScrapers(types), nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ListPerformerScrapers(ctx context.Context) ([]*scraper.Scraper, error) {
|
||||
return r.scraperCache().ListScrapers([]scraper.ScrapeContentType{scraper.ScrapeContentTypePerformer}), nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ListSceneScrapers(ctx context.Context) ([]*scraper.Scraper, error) {
|
||||
return r.scraperCache().ListScrapers([]scraper.ScrapeContentType{scraper.ScrapeContentTypeScene}), nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ListGalleryScrapers(ctx context.Context) ([]*scraper.Scraper, error) {
|
||||
return r.scraperCache().ListScrapers([]scraper.ScrapeContentType{scraper.ScrapeContentTypeGallery}), nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ListMovieScrapers(ctx context.Context) ([]*scraper.Scraper, error) {
|
||||
return r.scraperCache().ListScrapers([]scraper.ScrapeContentType{scraper.ScrapeContentTypeMovie}), nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapePerformerList(ctx context.Context, scraperID string, query string) ([]*models.ScrapedPerformer, error) {
|
||||
if query == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
content, err := r.scraperCache().ScrapeName(ctx, scraperID, query, scraper.ScrapeContentTypePerformer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return marshalScrapedPerformers(content)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapePerformer(ctx context.Context, scraperID string, scrapedPerformer scraper.ScrapedPerformerInput) (*models.ScrapedPerformer, error) {
|
||||
content, err := r.scraperCache().ScrapeFragment(ctx, scraperID, scraper.Input{Performer: &scrapedPerformer})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marshalScrapedPerformer(content)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapePerformerURL(ctx context.Context, url string) (*models.ScrapedPerformer, error) {
|
||||
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypePerformer)
|
||||
if err != nil {
|
||||
@@ -113,29 +54,6 @@ func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string,
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeScene(ctx context.Context, scraperID string, scene models.SceneUpdateInput) (*scraper.ScrapedScene, error) {
|
||||
id, err := strconv.Atoi(scene.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: scene.ID is not an integer: '%s'", ErrInput, scene.ID)
|
||||
}
|
||||
|
||||
content, err := r.scraperCache().ScrapeID(ctx, scraperID, id, scraper.ScrapeContentTypeScene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret, err := marshalScrapedScene(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ret != nil {
|
||||
filterSceneTags([]*scraper.ScrapedScene{ret})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// filterSceneTags removes tags matching excluded tag patterns from the provided scraped scenes
|
||||
func filterSceneTags(scenes []*scraper.ScrapedScene) {
|
||||
excludePatterns := manager.GetInstance().Config.GetScraperExcludeTagPatterns()
|
||||
@@ -163,7 +81,7 @@ func filterSceneTags(scenes []*scraper.ScrapedScene) {
|
||||
for _, reg := range excludeRegexps {
|
||||
if reg.MatchString(strings.ToLower(t.Name)) {
|
||||
ignore = true
|
||||
ignoredTags = stringslice.StrAppendUnique(ignoredTags, t.Name)
|
||||
ignoredTags = sliceutil.AppendUnique(ignoredTags, t.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -199,20 +117,6 @@ func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*scrape
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeGallery(ctx context.Context, scraperID string, gallery models.GalleryUpdateInput) (*scraper.ScrapedGallery, error) {
|
||||
id, err := strconv.Atoi(gallery.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: gallery id is not an integer: '%s'", ErrInput, gallery.ID)
|
||||
}
|
||||
|
||||
content, err := r.scraperCache().ScrapeID(ctx, scraperID, id, scraper.ScrapeContentTypeGallery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return marshalScrapedGallery(content)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*scraper.ScrapedGallery, error) {
|
||||
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeGallery)
|
||||
if err != nil {
|
||||
@@ -238,9 +142,11 @@ func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) {
|
||||
return nil, fmt.Errorf("%w: invalid stash_box_index %d", ErrInput, index)
|
||||
}
|
||||
|
||||
return stashbox.NewClient(*boxes[index], r.txnManager, r.stashboxRepository()), nil
|
||||
return stashbox.NewClient(*boxes[index], r.stashboxRepository()), nil
|
||||
}
|
||||
|
||||
// FIXME - in the following resolvers, we're processing the deprecated field and not processing the new endpoint input
|
||||
|
||||
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*scraper.ScrapedScene, error) {
|
||||
var ret []*scraper.ScrapedScene
|
||||
|
||||
@@ -378,6 +284,7 @@ func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scrape
|
||||
}
|
||||
|
||||
return nil, ErrNotImplemented
|
||||
// FIXME - we're relying on a deprecated field and not processing the endpoint input
|
||||
} else if source.StashBoxIndex != nil {
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
if err != nil {
|
||||
|
||||
15
internal/api/routes.go
Normal file
15
internal/api/routes.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
type routes struct {
|
||||
txnManager txn.Manager
|
||||
}
|
||||
|
||||
func (rs routes) withReadTxn(r *http.Request, fn txn.TxnFunc) error {
|
||||
return txn.WithReadTxn(r.Context(), rs.txnManager, fn)
|
||||
}
|
||||
@@ -4,12 +4,16 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type customRoutes struct {
|
||||
servedFolders config.URLMap
|
||||
servedFolders utils.URLMap
|
||||
}
|
||||
|
||||
func getCustomRoutes(servedFolders utils.URLMap) chi.Router {
|
||||
return customRoutes{servedFolders: servedFolders}.Routes()
|
||||
}
|
||||
|
||||
func (rs customRoutes) Routes() chi.Router {
|
||||
|
||||
@@ -4,7 +4,8 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -27,7 +26,7 @@ type ImageFinder interface {
|
||||
}
|
||||
|
||||
type imageRoutes struct {
|
||||
txnManager txn.Manager
|
||||
routes
|
||||
imageFinder ImageFinder
|
||||
fileGetter models.FileGetter
|
||||
}
|
||||
@@ -46,8 +45,6 @@ func (rs imageRoutes) Routes() chi.Router {
|
||||
return r
|
||||
}
|
||||
|
||||
// region Handlers
|
||||
|
||||
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
img := r.Context().Value(imageKey).(*models.Image)
|
||||
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
|
||||
@@ -71,7 +68,7 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
Preset: manager.GetInstance().Config.GetPreviewPreset().String(),
|
||||
}
|
||||
|
||||
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMPEG, manager.GetInstance().FFProbe, clipPreviewOptions)
|
||||
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMpeg, manager.GetInstance().FFProbe, clipPreviewOptions)
|
||||
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
|
||||
if err != nil {
|
||||
// don't log for unsupported image format
|
||||
@@ -119,8 +116,6 @@ func (rs imageRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (rs imageRoutes) serveImage(w http.ResponseWriter, r *http.Request, i *models.Image, useDefault bool) {
|
||||
const defaultImageImage = "image/image.svg"
|
||||
|
||||
if i.Files.Primary() != nil {
|
||||
err := i.Files.Primary().Base().Serve(&file.OsFS{}, w, r)
|
||||
if err == nil {
|
||||
@@ -141,22 +136,18 @@ func (rs imageRoutes) serveImage(w http.ResponseWriter, r *http.Request, i *mode
|
||||
return
|
||||
}
|
||||
|
||||
// fall back to static image
|
||||
f, _ := static.Image.Open(defaultImageImage)
|
||||
defer f.Close()
|
||||
image, _ := io.ReadAll(f)
|
||||
// fallback to default image
|
||||
image := static.ReadAll(static.DefaultImageImage)
|
||||
utils.ServeImage(w, r, image)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
func (rs imageRoutes) ImageCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
imageIdentifierQueryParam := chi.URLParam(r, "imageId")
|
||||
imageID, _ := strconv.Atoi(imageIdentifierQueryParam)
|
||||
|
||||
var image *models.Image
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
qb := rs.imageFinder
|
||||
if imageID == 0 {
|
||||
images, _ := qb.FindByChecksum(ctx, imageIdentifierQueryParam)
|
||||
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -20,7 +21,7 @@ type MovieFinder interface {
|
||||
}
|
||||
|
||||
type movieRoutes struct {
|
||||
txnManager txn.Manager
|
||||
routes
|
||||
movieFinder MovieFinder
|
||||
}
|
||||
|
||||
@@ -41,7 +42,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.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
image, err = rs.movieFinder.GetFrontImage(ctx, movie.ID)
|
||||
return err
|
||||
@@ -54,8 +55,9 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to default image
|
||||
if len(image) == 0 {
|
||||
image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
|
||||
image = static.ReadAll(static.DefaultMovieImage)
|
||||
}
|
||||
|
||||
utils.ServeImage(w, r, image)
|
||||
@@ -66,7 +68,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.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
image, err = rs.movieFinder.GetBackImage(ctx, movie.ID)
|
||||
return err
|
||||
@@ -79,8 +81,9 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to default image
|
||||
if len(image) == 0 {
|
||||
image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
|
||||
image = static.ReadAll(static.DefaultMovieImage)
|
||||
}
|
||||
|
||||
utils.ServeImage(w, r, image)
|
||||
@@ -95,7 +98,7 @@ func (rs movieRoutes) MovieCtx(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
var movie *models.Movie
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
movie, _ = rs.movieFinder.Find(ctx, movieID)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -6,11 +6,10 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -20,7 +19,7 @@ type PerformerFinder interface {
|
||||
}
|
||||
|
||||
type performerRoutes struct {
|
||||
txnManager txn.Manager
|
||||
routes
|
||||
performerFinder PerformerFinder
|
||||
}
|
||||
|
||||
@@ -41,7 +40,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
image, err = rs.performerFinder.GetImage(ctx, performer.ID)
|
||||
return err
|
||||
@@ -55,7 +54,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if len(image) == 0 {
|
||||
image, _ = getRandomPerformerImageUsingName(performer.Name, performer.Gender, config.GetInstance().GetCustomPerformerImageLocation())
|
||||
image = getDefaultPerformerImage(performer.Name, performer.Gender)
|
||||
}
|
||||
|
||||
utils.ServeImage(w, r, image)
|
||||
@@ -70,7 +69,7 @@ func (rs performerRoutes) PerformerCtx(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
var performer *models.Performer
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
performer, err = rs.performerFinder.Find(ctx, performerID)
|
||||
return err
|
||||
|
||||
101
internal/api/routes_plugin.go
Normal file
101
internal/api/routes_plugin.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
)
|
||||
|
||||
type pluginRoutes struct {
|
||||
pluginCache *plugin.Cache
|
||||
}
|
||||
|
||||
func (rs pluginRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{pluginId}", func(r chi.Router) {
|
||||
r.Use(rs.PluginCtx)
|
||||
r.Get("/assets/*", rs.Assets)
|
||||
r.Get("/javascript", rs.Javascript)
|
||||
r.Get("/css", rs.CSS)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (rs pluginRoutes) Assets(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.Context().Value(pluginKey).(*plugin.Plugin)
|
||||
|
||||
if !p.Enabled {
|
||||
http.Error(w, "plugin disabled", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
prefix := "/plugin/" + chi.URLParam(r, "pluginId") + "/assets"
|
||||
|
||||
r.URL.Path = strings.Replace(r.URL.Path, prefix, "", 1)
|
||||
|
||||
// http.FileServer redirects to / if the path ends with index.html
|
||||
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/index.html")
|
||||
|
||||
pluginDir := filepath.Dir(p.ConfigPath)
|
||||
|
||||
// map the path to the applicable filesystem location
|
||||
var dir string
|
||||
r.URL.Path, dir = p.UI.Assets.GetFilesystemLocation(r.URL.Path)
|
||||
if dir == "" {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
dir = filepath.Join(pluginDir, filepath.FromSlash(dir))
|
||||
|
||||
// ensure directory is still within the plugin directory
|
||||
if !strings.HasPrefix(dir, pluginDir) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.FileServer(http.Dir(dir)).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (rs pluginRoutes) Javascript(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.Context().Value(pluginKey).(*plugin.Plugin)
|
||||
|
||||
if !p.Enabled {
|
||||
http.Error(w, "plugin disabled", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/javascript")
|
||||
serveFiles(w, r, p.UI.Javascript)
|
||||
}
|
||||
|
||||
func (rs pluginRoutes) CSS(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.Context().Value(pluginKey).(*plugin.Plugin)
|
||||
|
||||
if !p.Enabled {
|
||||
http.Error(w, "plugin disabled", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
serveFiles(w, r, p.UI.CSS)
|
||||
}
|
||||
|
||||
func (rs pluginRoutes) PluginCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := rs.pluginCache.GetPlugin(chi.URLParam(r, "pluginId"))
|
||||
if p == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), pluginKey, p)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ type CaptionFinder interface {
|
||||
}
|
||||
|
||||
type sceneRoutes struct {
|
||||
txnManager txn.Manager
|
||||
routes
|
||||
sceneFinder SceneFinder
|
||||
fileGetter models.FileGetter
|
||||
captionFinder CaptionFinder
|
||||
@@ -89,8 +89,6 @@ func (rs sceneRoutes) Routes() chi.Router {
|
||||
return r
|
||||
}
|
||||
|
||||
// region Handlers
|
||||
|
||||
func (rs sceneRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
ss := manager.SceneServer{
|
||||
@@ -270,13 +268,13 @@ func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) {
|
||||
utils.ServeStaticFile(w, r, filepath)
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.SceneMarker) (*string, error) {
|
||||
func (rs sceneRoutes) getChapterVttTitle(r *http.Request, marker *models.SceneMarker) (*string, error) {
|
||||
if marker.Title != "" {
|
||||
return &marker.Title, nil
|
||||
}
|
||||
|
||||
var title string
|
||||
if err := txn.WithReadTxn(ctx, rs.txnManager, func(ctx context.Context) error {
|
||||
if err := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
qb := rs.tagFinder
|
||||
primaryTag, err := qb.Find(ctx, marker.PrimaryTagID)
|
||||
if err != nil {
|
||||
@@ -305,7 +303,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce
|
||||
func (rs sceneRoutes) VttChapter(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
var sceneMarkers []*models.SceneMarker
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
sceneMarkers, err = rs.sceneMarkerFinder.FindBySceneID(ctx, scene.ID)
|
||||
return err
|
||||
@@ -325,7 +323,7 @@ func (rs sceneRoutes) VttChapter(w http.ResponseWriter, r *http.Request) {
|
||||
time := utils.GetVTTTime(marker.Seconds)
|
||||
vttLines = append(vttLines, time+" --> "+time)
|
||||
|
||||
vttTitle, err := rs.getChapterVttTitle(r.Context(), marker)
|
||||
vttTitle, err := rs.getChapterVttTitle(r, marker)
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
@@ -404,7 +402,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.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
primaryFile := s.Files.Primary()
|
||||
if primaryFile == nil {
|
||||
@@ -466,7 +464,7 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request)
|
||||
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
|
||||
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
|
||||
var sceneMarker *models.SceneMarker
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
|
||||
return err
|
||||
@@ -494,7 +492,7 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
|
||||
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
|
||||
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
|
||||
var sceneMarker *models.SceneMarker
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
|
||||
return err
|
||||
@@ -530,7 +528,7 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque
|
||||
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
|
||||
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
|
||||
var sceneMarker *models.SceneMarker
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
|
||||
return err
|
||||
@@ -561,8 +559,6 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sceneID, err := strconv.Atoi(chi.URLParam(r, "sceneId"))
|
||||
@@ -572,7 +568,7 @@ func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
var scene *models.Scene
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
qb := rs.sceneFinder
|
||||
scene, _ = qb.Find(ctx, sceneID)
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -21,7 +20,7 @@ type StudioFinder interface {
|
||||
}
|
||||
|
||||
type studioRoutes struct {
|
||||
txnManager txn.Manager
|
||||
routes
|
||||
studioFinder StudioFinder
|
||||
}
|
||||
|
||||
@@ -42,7 +41,7 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
image, err = rs.studioFinder.GetImage(ctx, studio.ID)
|
||||
return err
|
||||
@@ -55,15 +54,9 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to default image
|
||||
if len(image) == 0 {
|
||||
const defaultStudioImage = "studio/studio.svg"
|
||||
|
||||
// fall back to static image
|
||||
f, _ := static.Studio.Open(defaultStudioImage)
|
||||
defer f.Close()
|
||||
stat, _ := f.Stat()
|
||||
http.ServeContent(w, r, "studio.svg", stat.ModTime(), f.(io.ReadSeeker))
|
||||
return
|
||||
image = static.ReadAll(static.DefaultStudioImage)
|
||||
}
|
||||
|
||||
utils.ServeImage(w, r, image)
|
||||
@@ -78,7 +71,7 @@ func (rs studioRoutes) StudioCtx(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
var studio *models.Studio
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
studio, err = rs.studioFinder.Find(ctx, studioID)
|
||||
return err
|
||||
|
||||
@@ -3,15 +3,14 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -21,8 +20,8 @@ type TagFinder interface {
|
||||
}
|
||||
|
||||
type tagRoutes struct {
|
||||
txnManager txn.Manager
|
||||
tagFinder TagFinder
|
||||
routes
|
||||
tagFinder TagFinder
|
||||
}
|
||||
|
||||
func (rs tagRoutes) Routes() chi.Router {
|
||||
@@ -42,7 +41,7 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
image, err = rs.tagFinder.GetImage(ctx, tag.ID)
|
||||
return err
|
||||
@@ -55,15 +54,9 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to default image
|
||||
if len(image) == 0 {
|
||||
const defaultTagImage = "tag/tag.svg"
|
||||
|
||||
// fall back to static image
|
||||
f, _ := static.Tag.Open(defaultTagImage)
|
||||
defer f.Close()
|
||||
stat, _ := f.Stat()
|
||||
http.ServeContent(w, r, "tag.svg", stat.ModTime(), f.(io.ReadSeeker))
|
||||
return
|
||||
image = static.ReadAll(static.DefaultTagImage)
|
||||
}
|
||||
|
||||
utils.ServeImage(w, r, image)
|
||||
@@ -78,7 +71,7 @@ func (rs tagRoutes) TagCtx(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
var tag *models.Tag
|
||||
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
|
||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
tag, err = rs.tagFinder.Find(ctx, tagID)
|
||||
return err
|
||||
|
||||
@@ -21,13 +21,13 @@ import (
|
||||
gqlLru "github.com/99designs/gqlgen/graphql/handler/lru"
|
||||
gqlTransport "github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
gqlPlayground "github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/go-chi/httplog"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/vearutop/statigz"
|
||||
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/go-chi/httplog"
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/build"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
@@ -46,31 +46,72 @@ const (
|
||||
playgroundEndpoint = "/playground"
|
||||
)
|
||||
|
||||
var uiBox = ui.UIBox
|
||||
var loginUIBox = ui.LoginUIBox
|
||||
type Server struct {
|
||||
http.Server
|
||||
displayAddress string
|
||||
|
||||
func Start() error {
|
||||
initialiseImages()
|
||||
manager *manager.Manager
|
||||
}
|
||||
|
||||
// Called at startup
|
||||
func Initialize() (*Server, error) {
|
||||
mgr := manager.GetInstance()
|
||||
cfg := mgr.Config
|
||||
|
||||
initCustomPerformerImages(cfg.GetCustomPerformerImageLocation())
|
||||
|
||||
displayHost := cfg.GetHost()
|
||||
if displayHost == "0.0.0.0" {
|
||||
displayHost = "localhost"
|
||||
}
|
||||
displayAddress := displayHost + ":" + strconv.Itoa(cfg.GetPort())
|
||||
|
||||
address := cfg.GetHost() + ":" + strconv.Itoa(cfg.GetPort())
|
||||
tlsConfig, err := makeTLSConfig(cfg)
|
||||
if err != nil {
|
||||
// assume we don't want to start with a broken TLS configuration
|
||||
return nil, fmt.Errorf("error loading TLS config: %v", err)
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
displayAddress = "https://" + displayAddress + "/"
|
||||
} else {
|
||||
displayAddress = "http://" + displayAddress + "/"
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
server := &Server{
|
||||
Server: http.Server{
|
||||
Addr: address,
|
||||
Handler: r,
|
||||
TLSConfig: tlsConfig,
|
||||
// disable http/2 support by default
|
||||
// when http/2 is enabled, we are unable to hijack and close
|
||||
// the connection/request. This is necessary to stop running
|
||||
// streams when deleting a scene file.
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
},
|
||||
displayAddress: displayAddress,
|
||||
manager: mgr,
|
||||
}
|
||||
|
||||
r.Use(middleware.Heartbeat("/healthz"))
|
||||
r.Use(cors.AllowAll().Handler)
|
||||
r.Use(authenticateHandler())
|
||||
visitedPluginHandler := manager.GetInstance().SessionStore.VisitedPluginHandler()
|
||||
visitedPluginHandler := mgr.SessionStore.VisitedPluginHandler()
|
||||
r.Use(visitedPluginHandler)
|
||||
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
c := config.GetInstance()
|
||||
if c.GetLogAccess() {
|
||||
if cfg.GetLogAccess() {
|
||||
httpLogger := httplog.NewLogger("Stash", httplog.Options{
|
||||
Concise: true,
|
||||
})
|
||||
r.Use(httplog.RequestLogger(httpLogger))
|
||||
}
|
||||
r.Use(SecurityHeadersMiddleware)
|
||||
r.Use(middleware.DefaultCompress)
|
||||
r.Use(middleware.Compress(4))
|
||||
r.Use(middleware.StripSlashes)
|
||||
r.Use(BaseURLMiddleware)
|
||||
|
||||
@@ -82,22 +123,20 @@ func Start() error {
|
||||
return errors.New(message)
|
||||
}
|
||||
|
||||
txnManager := manager.GetInstance().Repository
|
||||
repo := mgr.Repository
|
||||
|
||||
dataloaders := loaders.Middleware{
|
||||
DatabaseProvider: txnManager,
|
||||
Repository: txnManager,
|
||||
Repository: repo,
|
||||
}
|
||||
|
||||
r.Use(dataloaders.Middleware)
|
||||
|
||||
pluginCache := manager.GetInstance().PluginCache
|
||||
sceneService := manager.GetInstance().SceneService
|
||||
imageService := manager.GetInstance().ImageService
|
||||
galleryService := manager.GetInstance().GalleryService
|
||||
pluginCache := mgr.PluginCache
|
||||
sceneService := mgr.SceneService
|
||||
imageService := mgr.ImageService
|
||||
galleryService := mgr.GalleryService
|
||||
resolver := &Resolver{
|
||||
txnManager: txnManager,
|
||||
repository: txnManager,
|
||||
repository: repo,
|
||||
sceneService: sceneService,
|
||||
imageService: imageService,
|
||||
galleryService: galleryService,
|
||||
@@ -118,7 +157,7 @@ func Start() error {
|
||||
gqlSrv.AddTransport(gqlTransport.GET{})
|
||||
gqlSrv.AddTransport(gqlTransport.POST{})
|
||||
gqlSrv.AddTransport(gqlTransport.MultipartForm{
|
||||
MaxUploadSize: c.GetMaxUploadSize(),
|
||||
MaxUploadSize: cfg.GetMaxUploadSize(),
|
||||
})
|
||||
|
||||
gqlSrv.SetQueryCache(gqlLru.New(1000))
|
||||
@@ -135,54 +174,32 @@ func Start() error {
|
||||
// chain the visited plugin handler
|
||||
// also requires the dataloader middleware
|
||||
gqlHandler := visitedPluginHandler(dataloaders.Middleware(http.HandlerFunc(gqlHandlerFunc)))
|
||||
manager.GetInstance().PluginCache.RegisterGQLHandler(gqlHandler)
|
||||
pluginCache.RegisterGQLHandler(gqlHandler)
|
||||
|
||||
r.HandleFunc(gqlEndpoint, gqlHandlerFunc)
|
||||
r.HandleFunc(playgroundEndpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
setPageSecurityHeaders(w, r)
|
||||
setPageSecurityHeaders(w, r, pluginCache.ListPlugins())
|
||||
endpoint := getProxyPrefix(r) + gqlEndpoint
|
||||
gqlPlayground.Handler("GraphQL playground", endpoint)(w, r)
|
||||
})
|
||||
|
||||
r.Mount("/performer", performerRoutes{
|
||||
txnManager: txnManager,
|
||||
performerFinder: txnManager.Performer,
|
||||
}.Routes())
|
||||
r.Mount("/scene", sceneRoutes{
|
||||
txnManager: txnManager,
|
||||
sceneFinder: txnManager.Scene,
|
||||
fileGetter: txnManager.File,
|
||||
captionFinder: txnManager.File,
|
||||
sceneMarkerFinder: txnManager.SceneMarker,
|
||||
tagFinder: txnManager.Tag,
|
||||
}.Routes())
|
||||
r.Mount("/image", imageRoutes{
|
||||
txnManager: txnManager,
|
||||
imageFinder: txnManager.Image,
|
||||
fileGetter: txnManager.File,
|
||||
}.Routes())
|
||||
r.Mount("/studio", studioRoutes{
|
||||
txnManager: txnManager,
|
||||
studioFinder: txnManager.Studio,
|
||||
}.Routes())
|
||||
r.Mount("/movie", movieRoutes{
|
||||
txnManager: txnManager,
|
||||
movieFinder: txnManager.Movie,
|
||||
}.Routes())
|
||||
r.Mount("/tag", tagRoutes{
|
||||
txnManager: txnManager,
|
||||
tagFinder: txnManager.Tag,
|
||||
}.Routes())
|
||||
r.Mount("/downloads", downloadsRoutes{}.Routes())
|
||||
r.Mount("/performer", server.getPerformerRoutes())
|
||||
r.Mount("/scene", server.getSceneRoutes())
|
||||
r.Mount("/image", server.getImageRoutes())
|
||||
r.Mount("/studio", server.getStudioRoutes())
|
||||
r.Mount("/movie", server.getMovieRoutes())
|
||||
r.Mount("/tag", server.getTagRoutes())
|
||||
r.Mount("/downloads", server.getDownloadsRoutes())
|
||||
r.Mount("/plugin", server.getPluginRoutes())
|
||||
|
||||
r.HandleFunc("/css", cssHandler(c, pluginCache))
|
||||
r.HandleFunc("/javascript", javascriptHandler(c, pluginCache))
|
||||
r.HandleFunc("/customlocales", customLocalesHandler(c))
|
||||
r.HandleFunc("/css", cssHandler(cfg))
|
||||
r.HandleFunc("/javascript", javascriptHandler(cfg))
|
||||
r.HandleFunc("/customlocales", customLocalesHandler(cfg))
|
||||
|
||||
staticLoginUI := statigz.FileServer(loginUIBox.(fs.ReadDirFS))
|
||||
staticLoginUI := statigz.FileServer(ui.LoginUIBox.(fs.ReadDirFS))
|
||||
|
||||
r.Get(loginEndpoint, handleLogin(loginUIBox))
|
||||
r.Post(loginEndpoint, handleLoginPost(loginUIBox))
|
||||
r.Get(loginEndpoint, handleLogin())
|
||||
r.Post(loginEndpoint, handleLoginPost())
|
||||
r.Get(logoutEndpoint, handleLogout())
|
||||
r.HandleFunc(loginEndpoint+"/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, loginEndpoint)
|
||||
@@ -191,15 +208,13 @@ func Start() error {
|
||||
})
|
||||
|
||||
// Serve static folders
|
||||
customServedFolders := c.GetCustomServedFolders()
|
||||
customServedFolders := cfg.GetCustomServedFolders()
|
||||
if customServedFolders != nil {
|
||||
r.Mount("/custom", customRoutes{
|
||||
servedFolders: customServedFolders,
|
||||
}.Routes())
|
||||
r.Mount("/custom", getCustomRoutes(customServedFolders))
|
||||
}
|
||||
|
||||
customUILocation := c.GetCustomUILocation()
|
||||
staticUI := statigz.FileServer(uiBox.(fs.ReadDirFS))
|
||||
customUILocation := cfg.GetCustomUILocation()
|
||||
staticUI := statigz.FileServer(ui.UIBox.(fs.ReadDirFS))
|
||||
|
||||
// Serve the web app
|
||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -215,8 +230,8 @@ func Start() error {
|
||||
}
|
||||
|
||||
if ext == ".html" || ext == "" {
|
||||
themeColor := c.GetThemeColor()
|
||||
data, err := fs.ReadFile(uiBox, "index.html")
|
||||
themeColor := cfg.GetThemeColor()
|
||||
data, err := fs.ReadFile(ui.UIBox, "index.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -227,7 +242,7 @@ func Start() error {
|
||||
indexHtml = strings.Replace(indexHtml, `<base href="/"`, fmt.Sprintf(`<base href="%s/"`, prefix), 1)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
setPageSecurityHeaders(w, r)
|
||||
setPageSecurityHeaders(w, r, pluginCache.ListPlugins())
|
||||
|
||||
utils.ServeStaticContent(w, r, []byte(indexHtml))
|
||||
} else {
|
||||
@@ -242,51 +257,91 @@ func Start() error {
|
||||
}
|
||||
})
|
||||
|
||||
displayHost := c.GetHost()
|
||||
if displayHost == "0.0.0.0" {
|
||||
displayHost = "localhost"
|
||||
}
|
||||
displayAddress := displayHost + ":" + strconv.Itoa(c.GetPort())
|
||||
|
||||
address := c.GetHost() + ":" + strconv.Itoa(c.GetPort())
|
||||
tlsConfig, err := makeTLSConfig(c)
|
||||
if err != nil {
|
||||
// assume we don't want to start with a broken TLS configuration
|
||||
panic(fmt.Errorf("error loading TLS config: %v", err))
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: address,
|
||||
Handler: r,
|
||||
TLSConfig: tlsConfig,
|
||||
// disable http/2 support by default
|
||||
// when http/2 is enabled, we are unable to hijack and close
|
||||
// the connection/request. This is necessary to stop running
|
||||
// streams when deleting a scene file.
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
}
|
||||
|
||||
logger.Infof("stash version: %s\n", build.VersionString())
|
||||
logger.Infof("stash version: %s", build.VersionString())
|
||||
go printLatestVersion(context.TODO())
|
||||
logger.Infof("stash is listening on " + address)
|
||||
if tlsConfig != nil {
|
||||
displayAddress = "https://" + displayAddress + "/"
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
logger.Infof("stash is listening on " + s.Addr)
|
||||
logger.Infof("stash is running at " + s.displayAddress)
|
||||
|
||||
if s.TLSConfig != nil {
|
||||
return s.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
displayAddress = "http://" + displayAddress + "/"
|
||||
return s.ListenAndServe()
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("stash is running at " + displayAddress)
|
||||
if tlsConfig != nil {
|
||||
err = server.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
err = server.ListenAndServe()
|
||||
func (s *Server) Shutdown() {
|
||||
err := s.Server.Shutdown(context.TODO())
|
||||
if err != nil {
|
||||
logger.Errorf("Error shutting down http server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
func (s *Server) getPerformerRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return performerRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
performerFinder: repo.Performer,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
return nil
|
||||
func (s *Server) getSceneRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return sceneRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
sceneFinder: repo.Scene,
|
||||
fileGetter: repo.File,
|
||||
captionFinder: repo.File,
|
||||
sceneMarkerFinder: repo.SceneMarker,
|
||||
tagFinder: repo.Tag,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getImageRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return imageRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
imageFinder: repo.Image,
|
||||
fileGetter: repo.File,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getStudioRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return studioRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
studioFinder: repo.Studio,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getMovieRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return movieRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
movieFinder: repo.Movie,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getTagRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return tagRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
tagFinder: repo.Tag,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getDownloadsRoutes() chi.Router {
|
||||
return downloadsRoutes{}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getPluginRoutes() chi.Router {
|
||||
return pluginRoutes{
|
||||
pluginCache: s.manager.PluginCache,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func copyFile(w io.Writer, path string) error {
|
||||
@@ -315,15 +370,10 @@ func serveFiles(w http.ResponseWriter, r *http.Request, paths []string) {
|
||||
utils.ServeStaticContent(w, r, buffer.Bytes())
|
||||
}
|
||||
|
||||
func cssHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.ResponseWriter, r *http.Request) {
|
||||
func cssHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// 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()
|
||||
@@ -338,15 +388,10 @@ func cssHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.Respo
|
||||
}
|
||||
}
|
||||
|
||||
func javascriptHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.ResponseWriter, r *http.Request) {
|
||||
func javascriptHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// 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()
|
||||
@@ -361,7 +406,7 @@ func javascriptHandler(c *config.Instance, pluginCache *plugin.Cache) func(w htt
|
||||
}
|
||||
}
|
||||
|
||||
func customLocalesHandler(c *config.Instance) func(w http.ResponseWriter, r *http.Request) {
|
||||
func customLocalesHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
buffer := bytes.Buffer{}
|
||||
|
||||
@@ -386,7 +431,7 @@ func customLocalesHandler(c *config.Instance) func(w http.ResponseWriter, r *htt
|
||||
}
|
||||
}
|
||||
|
||||
func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
|
||||
func makeTLSConfig(c *config.Config) (*tls.Config, error) {
|
||||
c.InitTLS()
|
||||
certFile, keyFile := c.GetTLSFiles()
|
||||
|
||||
@@ -426,31 +471,75 @@ func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func setPageSecurityHeaders(w http.ResponseWriter, r *http.Request) {
|
||||
func isURL(s string) bool {
|
||||
return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://")
|
||||
}
|
||||
|
||||
func setPageSecurityHeaders(w http.ResponseWriter, r *http.Request, plugins []*plugin.Plugin) {
|
||||
c := config.GetInstance()
|
||||
|
||||
defaultSrc := "data: 'self' 'unsafe-inline'"
|
||||
connectSrc := "data: 'self'"
|
||||
connectSrcSlice := []string{
|
||||
"data:",
|
||||
"'self'",
|
||||
}
|
||||
imageSrc := "data: *"
|
||||
scriptSrc := "'self' http://www.gstatic.com https://www.gstatic.com 'unsafe-inline' 'unsafe-eval'"
|
||||
styleSrc := "'self' 'unsafe-inline'"
|
||||
scriptSrcSlice := []string{
|
||||
"'self'",
|
||||
"http://www.gstatic.com",
|
||||
"https://www.gstatic.com",
|
||||
"'unsafe-inline'",
|
||||
"'unsafe-eval'",
|
||||
}
|
||||
styleSrcSlice := []string{
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
}
|
||||
mediaSrc := "blob: 'self'"
|
||||
|
||||
// Workaround Safari bug https://bugs.webkit.org/show_bug.cgi?id=201591
|
||||
// Allows websocket requests to any origin
|
||||
connectSrc += " ws: wss:"
|
||||
connectSrcSlice = append(connectSrcSlice, "ws:", "wss:")
|
||||
|
||||
// The graphql playground pulls its frontend from a cdn
|
||||
if r.URL.Path == playgroundEndpoint {
|
||||
connectSrc += " https://cdn.jsdelivr.net"
|
||||
scriptSrc += " https://cdn.jsdelivr.net"
|
||||
styleSrc += " https://cdn.jsdelivr.net"
|
||||
connectSrcSlice = append(connectSrcSlice, "https://cdn.jsdelivr.net")
|
||||
scriptSrcSlice = append(scriptSrcSlice, "https://cdn.jsdelivr.net")
|
||||
styleSrcSlice = append(styleSrcSlice, "https://cdn.jsdelivr.net")
|
||||
}
|
||||
|
||||
if !c.IsNewSystem() && c.GetHandyKey() != "" {
|
||||
connectSrc += " https://www.handyfeeling.com"
|
||||
connectSrcSlice = append(connectSrcSlice, "https://www.handyfeeling.com")
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if !plugin.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
ui := plugin.UI
|
||||
|
||||
for _, url := range ui.ExternalScript {
|
||||
if isURL(url) {
|
||||
scriptSrcSlice = append(scriptSrcSlice, url)
|
||||
}
|
||||
}
|
||||
|
||||
for _, url := range ui.ExternalCSS {
|
||||
if isURL(url) {
|
||||
styleSrcSlice = append(styleSrcSlice, url)
|
||||
}
|
||||
}
|
||||
|
||||
connectSrcSlice = append(connectSrcSlice, ui.CSP.ConnectSrc...)
|
||||
scriptSrcSlice = append(scriptSrcSlice, ui.CSP.ScriptSrc...)
|
||||
styleSrcSlice = append(styleSrcSlice, ui.CSP.StyleSrc...)
|
||||
}
|
||||
|
||||
connectSrc := strings.Join(connectSrcSlice, " ")
|
||||
scriptSrc := strings.Join(scriptSrcSlice, " ")
|
||||
styleSrc := strings.Join(styleSrcSlice, " ")
|
||||
|
||||
cspDirectives := fmt.Sprintf("default-src %s; connect-src %s; img-src %s; script-src %s; style-src %s; media-src %s;", defaultSrc, connectSrc, imageSrc, scriptSrc, styleSrc, mediaSrc)
|
||||
cspDirectives += " worker-src blob:; child-src 'none'; object-src 'none'; form-action 'self';"
|
||||
|
||||
|
||||
@@ -14,12 +14,13 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/session"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"github.com/stashapp/stash/ui"
|
||||
)
|
||||
|
||||
const returnURLParam = "returnURL"
|
||||
|
||||
func getLoginPage(loginUIBox fs.FS) []byte {
|
||||
data, err := fs.ReadFile(loginUIBox, "login.html")
|
||||
func getLoginPage() []byte {
|
||||
data, err := fs.ReadFile(ui.LoginUIBox, "login.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -31,8 +32,8 @@ type loginTemplateData struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
func serveLoginPage(loginUIBox fs.FS, w http.ResponseWriter, r *http.Request, returnURL string, loginError string) {
|
||||
loginPage := string(getLoginPage(loginUIBox))
|
||||
func serveLoginPage(w http.ResponseWriter, r *http.Request, returnURL string, loginError string) {
|
||||
loginPage := string(getLoginPage())
|
||||
prefix := getProxyPrefix(r)
|
||||
loginPage = strings.ReplaceAll(loginPage, "/%BASE_URL%", prefix)
|
||||
|
||||
@@ -50,12 +51,14 @@ func serveLoginPage(loginUIBox fs.FS, w http.ResponseWriter, r *http.Request, re
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
setPageSecurityHeaders(w, r)
|
||||
|
||||
// we shouldn't need to set plugin exceptions here
|
||||
setPageSecurityHeaders(w, r, nil)
|
||||
|
||||
utils.ServeStaticContent(w, r, buffer.Bytes())
|
||||
}
|
||||
|
||||
func handleLogin(loginUIBox fs.FS) http.HandlerFunc {
|
||||
func handleLogin() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
returnURL := r.URL.Query().Get(returnURLParam)
|
||||
|
||||
@@ -69,11 +72,11 @@ func handleLogin(loginUIBox fs.FS) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
serveLoginPage(loginUIBox, w, r, returnURL, "")
|
||||
serveLoginPage(w, r, returnURL, "")
|
||||
}
|
||||
}
|
||||
|
||||
func handleLoginPost(loginUIBox fs.FS) http.HandlerFunc {
|
||||
func handleLoginPost() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.FormValue(returnURLParam)
|
||||
if url == "" {
|
||||
@@ -90,7 +93,7 @@ func handleLoginPost(loginUIBox fs.FS) http.HandlerFunc {
|
||||
|
||||
if errors.As(err, &invalidCredentialsError) {
|
||||
// serve login page with an error
|
||||
serveLoginPage(loginUIBox, w, r, url, "Username or password is invalid")
|
||||
serveLoginPage(w, r, url, "Username or password is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user