mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
184 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1096fe812e | ||
|
|
5f5cd03929 | ||
|
|
aa50dbf5e2 | ||
|
|
7547a1dd39 | ||
|
|
c571687c99 | ||
|
|
2a5afecc77 | ||
|
|
df7d1427d6 | ||
|
|
0d76fede84 | ||
|
|
2c7e0f0571 | ||
|
|
a7ed0a7004 | ||
|
|
808202ba8a | ||
|
|
07dbc2236d | ||
|
|
225e17f710 | ||
|
|
d316aeca16 | ||
|
|
bd0de23459 | ||
|
|
bcd6d4fb46 | ||
|
|
ffc8043273 | ||
|
|
49b2860909 | ||
|
|
60c4f7e992 | ||
|
|
1f48a9ce95 | ||
|
|
3aa5f657bc | ||
|
|
25274e2596 | ||
|
|
e961ba4459 | ||
|
|
5bb5f6f2ce | ||
|
|
dbfd92f9a8 | ||
|
|
392b28915a | ||
|
|
3671388b8d | ||
|
|
602183cca9 | ||
|
|
206f86e304 | ||
|
|
9134490be2 | ||
|
|
a8a5d547ed | ||
|
|
ed9d80fcfc | ||
|
|
934d685e86 | ||
|
|
32db1dda57 | ||
|
|
0dbee117d8 | ||
|
|
f4ac82f989 | ||
|
|
5f26719d2d | ||
|
|
0d24af4cb4 | ||
|
|
07a1cdd9f7 | ||
|
|
c6a326ca64 | ||
|
|
87036a07bc | ||
|
|
29b14ab4fc | ||
|
|
ad7bb9a46f | ||
|
|
b01c4468d8 | ||
|
|
bf59028bcd | ||
|
|
96fce90cc3 | ||
|
|
27cdefbb10 | ||
|
|
0f64954e5b | ||
|
|
c93b5e12b7 | ||
|
|
372ea7218e | ||
|
|
b2897d0cf5 | ||
|
|
5de06d4b62 | ||
|
|
2136ced25c | ||
|
|
bdb8dc94d3 | ||
|
|
e9c7b0aed3 | ||
|
|
a822455a66 | ||
|
|
1fffc0519a | ||
|
|
c8182bdb4c | ||
|
|
35e646b68b | ||
|
|
595e8efb73 | ||
|
|
1e5889ba17 | ||
|
|
4dd56c3d82 | ||
|
|
1b411e3f43 | ||
|
|
db1e5c63d0 | ||
|
|
cb52eb798d | ||
|
|
f904c59532 | ||
|
|
401fc290ef | ||
|
|
d292ed0b34 | ||
|
|
0ab8d32687 | ||
|
|
f80a5e3222 | ||
|
|
7732152c0c | ||
|
|
15acf91b90 | ||
|
|
976038424b | ||
|
|
8b7720e3bf | ||
|
|
e513b6ffa5 | ||
|
|
214a15bc40 | ||
|
|
e14bb8432c | ||
|
|
3e6e830f45 | ||
|
|
95e11d4aa8 | ||
|
|
cf43a825d8 | ||
|
|
5ec70ac3e0 | ||
|
|
39fdde273d | ||
|
|
3d5ee16e90 | ||
|
|
1152e1acac | ||
|
|
602a32bd32 | ||
|
|
655d3ae969 | ||
|
|
41a1fb8aec | ||
|
|
4eeef22c15 | ||
|
|
1217f3fbc1 | ||
|
|
c6f6205e4f | ||
|
|
38384f2c60 | ||
|
|
e9d48683f8 | ||
|
|
b5381ff071 | ||
|
|
46ae4581b8 | ||
|
|
94e8abcb50 | ||
|
|
f4b783871a | ||
|
|
29cd627ed2 | ||
|
|
c31c7c3c99 | ||
|
|
04ca11e62e | ||
|
|
fb5f9162d0 | ||
|
|
47ae1be53c | ||
|
|
11fa8ce581 | ||
|
|
428c6442d5 | ||
|
|
1e89e9dd82 | ||
|
|
f1da6cb1b2 | ||
|
|
dcf58b99a6 | ||
|
|
a5ca8fc678 | ||
|
|
17f5642ebd | ||
|
|
7464454da5 | ||
|
|
52193586de | ||
|
|
ea7a4f8d33 | ||
|
|
73ea195668 | ||
|
|
cca156b5f8 | ||
|
|
ca0a8b00ec | ||
|
|
e3480531a7 | ||
|
|
dabf5acefe | ||
|
|
df2c9e9754 | ||
|
|
9e541956f2 | ||
|
|
bc6d04dc2a | ||
|
|
b94eecae24 | ||
|
|
6ce57a9a43 | ||
|
|
94d192b833 | ||
|
|
479bd438df | ||
|
|
b957a87a78 | ||
|
|
be94e52f21 | ||
|
|
62af723017 | ||
|
|
a2cce0ba77 | ||
|
|
1d04b550b9 | ||
|
|
5fdab995f5 | ||
|
|
f977d0e18a | ||
|
|
ba2a79700a | ||
|
|
2f664fe826 | ||
|
|
b14d5c5650 | ||
|
|
a9e2a590b2 | ||
|
|
9cb1eccadb | ||
|
|
1e8a8efe3e | ||
|
|
cceaff780b | ||
|
|
8d9eb7f1e4 | ||
|
|
4a0c4c4847 | ||
|
|
56111433a1 | ||
|
|
f292238e7f | ||
|
|
25182997f1 | ||
|
|
a4ed9515c7 | ||
|
|
f3c8407c40 | ||
|
|
3e526a49a4 | ||
|
|
b7c229dc70 | ||
|
|
87709fd018 | ||
|
|
af6232ec97 | ||
|
|
919249f851 | ||
|
|
13fda2ad85 | ||
|
|
82e4ad4130 | ||
|
|
b83ce29ac4 | ||
|
|
66f92c5dcc | ||
|
|
d29699fa30 | ||
|
|
501ed7c2c2 | ||
|
|
e0c910d9e8 | ||
|
|
f3119a6c38 | ||
|
|
23f852cd91 | ||
|
|
2274db16b7 | ||
|
|
f5e4e7742e | ||
|
|
3be23999ef | ||
|
|
612ecb72fc | ||
|
|
b78060d361 | ||
|
|
1a3a2f1f83 | ||
|
|
565064b441 | ||
|
|
13a289a4a8 | ||
|
|
04e5ac9c2f | ||
|
|
c91ffe1e58 | ||
|
|
82a41e17c7 | ||
|
|
4b00d24248 | ||
|
|
265d5f4c70 | ||
|
|
4545da9af0 | ||
|
|
e7f6cb22b7 | ||
|
|
d2a0a8fe4c | ||
|
|
b482fbc796 | ||
|
|
a3f38d8edf | ||
|
|
651d2e6373 | ||
|
|
b76283df08 | ||
|
|
489db34db2 | ||
|
|
b2b05fb332 | ||
|
|
7a468413da | ||
|
|
4625e1f955 | ||
|
|
04e146f290 | ||
|
|
ab10cf8251 |
@@ -15,16 +15,10 @@
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Packr2 artifacts
|
||||
**/*-packr.go
|
||||
|
||||
# GraphQL generated output
|
||||
pkg/models/generated_*.go
|
||||
ui/v2.5/src/core/generated-*.tsx
|
||||
|
||||
# packr generated files
|
||||
*-packr.go
|
||||
|
||||
####
|
||||
# Jetbrains
|
||||
####
|
||||
@@ -60,4 +54,4 @@ ui/v2.5/build
|
||||
stash
|
||||
dist
|
||||
|
||||
docker
|
||||
docker
|
||||
|
||||
55
.github/workflows/build.yml
vendored
55
.github/workflows/build.yml
vendored
@@ -8,8 +8,12 @@ on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:4
|
||||
COMPILER_IMAGE: stashapp/compiler:5
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -43,43 +47,52 @@ jobs:
|
||||
- name: Cache go build
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-go-cache
|
||||
# increment the number suffix to bump the cache
|
||||
cache-name: cache-go-cache-1
|
||||
with:
|
||||
path: .go-cache
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/go.sum') }}
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('go.mod', '**/go.sum') }}
|
||||
|
||||
- name: Start build container
|
||||
env:
|
||||
official-build: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/develop') || (github.event_name == 'release' && github.ref != 'refs/tags/latest_develop') }}
|
||||
run: |
|
||||
mkdir -p .go-cache
|
||||
docker run -d --name build --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated --mount type=bind,source="$(pwd)/.go-cache",target=/root/.cache/go-build,consistency=delegated -w /stash $COMPILER_IMAGE tail -f /dev/null
|
||||
|
||||
docker run -d --name build --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated --mount type=bind,source="$(pwd)/.go-cache",target=/root/.cache/go-build,consistency=delegated --env OFFICIAL_BUILD=${{ env.official-build }} -w /stash $COMPILER_IMAGE tail -f /dev/null
|
||||
|
||||
- name: Pre-install
|
||||
run: docker exec -t build /bin/bash -c "make pre-ui"
|
||||
|
||||
- name: Generate
|
||||
run: docker exec -t build /bin/bash -c "make generate"
|
||||
|
||||
|
||||
- name: Validate UI
|
||||
# skip UI validation for pull requests if UI is unchanged
|
||||
if: ${{ github.event_name != 'pull_request' || steps.cache-ui.outputs.cache-hit != 'true' }}
|
||||
run: docker exec -t build /bin/bash -c "make ui-validate"
|
||||
run: docker exec -t build /bin/bash -c "make validate-frontend"
|
||||
|
||||
# TODO: Replace with `make validate` once `revive` is bundled in COMPILER_IMAGE
|
||||
- name: Validate
|
||||
run: docker exec -t build /bin/bash -c "make fmt-check vet it"
|
||||
# Static validation happens in the linter workflow in parallel to this workflow
|
||||
# Run Dynamic validation here, to make sure we pass all the projects integration tests
|
||||
#
|
||||
# create UI file so that the embed doesn't fail
|
||||
- name: Test Backend
|
||||
run: |
|
||||
mkdir -p ui/v2.5/build
|
||||
touch ui/v2.5/build/index.html
|
||||
docker exec -t build /bin/bash -c "make it"
|
||||
|
||||
- name: Build UI
|
||||
# skip UI build for pull requests if UI is unchanged (UI was cached)
|
||||
# this means that the build version/time may be incorrect if the UI is
|
||||
# not changed in a pull request
|
||||
if: ${{ github.event_name != 'pull_request' || steps.cache-ui.outputs.cache-hit != 'true' }}
|
||||
run: docker exec -t build /bin/bash -c "make ui-only"
|
||||
run: docker exec -t build /bin/bash -c "make ui"
|
||||
|
||||
- name: Compile for all supported platforms
|
||||
run: |
|
||||
docker exec -t build /bin/bash -c "make packr"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-windows"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-osx"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-osx-intel"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-osx-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"
|
||||
@@ -94,7 +107,7 @@ jobs:
|
||||
sha1sum dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
|
||||
echo "STASH_VERSION=$(git describe --tags --exclude latest_develop)" >> $GITHUB_ENV
|
||||
echo "RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Upload Windows binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
@@ -118,13 +131,13 @@ jobs:
|
||||
with:
|
||||
name: stash-linux
|
||||
path: dist/stash-linux
|
||||
|
||||
|
||||
- name: Update latest_develop tag
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
run : git tag -f latest_develop; git push -f --tags
|
||||
|
||||
- name: Development Release
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
uses: marvinpinto/action-automatic-releases@v1.1.2
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -133,13 +146,14 @@ jobs:
|
||||
title: "${{ env.STASH_VERSION }}: Latest development build"
|
||||
files: |
|
||||
dist/stash-osx
|
||||
dist/stash-osx-applesilicon
|
||||
dist/stash-win.exe
|
||||
dist/stash-linux
|
||||
dist/stash-linux-arm64v8
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-pi
|
||||
CHECKSUMS_SHA1
|
||||
|
||||
|
||||
- name: Master release
|
||||
if: ${{ github.event_name == 'release' && github.ref != 'refs/tags/latest_develop' }}
|
||||
uses: meeDamian/github-release@2.0
|
||||
@@ -148,6 +162,7 @@ jobs:
|
||||
allow_override: true
|
||||
files: |
|
||||
dist/stash-osx
|
||||
dist/stash-osx-applesilicon
|
||||
dist/stash-win.exe
|
||||
dist/stash-linux
|
||||
dist/stash-linux-arm64v8
|
||||
@@ -155,7 +170,7 @@ jobs:
|
||||
dist/stash-pi
|
||||
CHECKSUMS_SHA1
|
||||
gzip: false
|
||||
|
||||
|
||||
- name: Development Docker
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
env:
|
||||
@@ -163,7 +178,7 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: |
|
||||
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
|
||||
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
|
||||
docker info
|
||||
docker buildx create --name builder --use
|
||||
docker buildx inspect --bootstrap
|
||||
@@ -177,7 +192,7 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: |
|
||||
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
|
||||
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
|
||||
docker info
|
||||
docker buildx create --name builder --use
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
60
.github/workflows/golangci-lint.yml
vendored
Normal file
60
.github/workflows/golangci-lint.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Lint (golangci-lint)
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:5
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Checkout
|
||||
run: git fetch --prune --unshallow --tags
|
||||
|
||||
- name: Pull compiler image
|
||||
run: docker pull $COMPILER_IMAGE
|
||||
|
||||
- name: Start build container
|
||||
run: |
|
||||
mkdir -p .go-cache
|
||||
docker run -d --name build --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated --mount type=bind,source="$(pwd)/.go-cache",target=/root/.cache/go-build,consistency=delegated -w /stash $COMPILER_IMAGE tail -f /dev/null
|
||||
|
||||
- name: Generate Backend
|
||||
run: docker exec -t build /bin/bash -c "make generate-backend"
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||
version: v1.42.1
|
||||
|
||||
# Optional: working directory, useful for monorepos
|
||||
# working-directory: somedir
|
||||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
args: --modules-download-mode=vendor --timeout=3m
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
# Optional: if set to true then the action will use pre-installed Go.
|
||||
# skip-go-installation: true
|
||||
|
||||
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
|
||||
# skip-pkg-cache: true
|
||||
|
||||
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
|
||||
# skip-build-cache: true
|
||||
|
||||
- name: Cleanup build container
|
||||
run: docker rm -f -v build
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -15,16 +15,10 @@
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Packr2 artifacts
|
||||
**/*-packr.go
|
||||
|
||||
# GraphQL generated output
|
||||
pkg/models/generated_*.go
|
||||
ui/v2.5/src/core/generated-*.tsx
|
||||
|
||||
# packr generated files
|
||||
*-packr.go
|
||||
|
||||
####
|
||||
# Jetbrains
|
||||
####
|
||||
|
||||
91
.golangci.yml
Normal file
91
.golangci.yml
Normal file
@@ -0,0 +1,91 @@
|
||||
# options for analysis running
|
||||
run:
|
||||
timeout: 3m
|
||||
modules-download-mode: vendor
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
# Default set of linters from golangci-lint
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- varcheck
|
||||
# Linters added by the stash project.
|
||||
- dogsled
|
||||
- errorlint
|
||||
# - exhaustive
|
||||
- exportloopref
|
||||
- gocritic
|
||||
# - goerr113
|
||||
- gofmt
|
||||
# - gomnd
|
||||
# - ifshort
|
||||
- misspell
|
||||
# - nakedret
|
||||
- noctx
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
|
||||
# Project-specific linter overrides
|
||||
linters-settings:
|
||||
gofmt:
|
||||
simplify: false
|
||||
|
||||
errorlint:
|
||||
# Disable errorf because there are false positives, where you don't want to wrap
|
||||
# an error.
|
||||
errorf: false
|
||||
asserts: true
|
||||
comparison: true
|
||||
|
||||
revive:
|
||||
ignore-generated-header: true
|
||||
severity: error
|
||||
confidence: 0.8
|
||||
error-code: 1
|
||||
warning-code: 1
|
||||
rules:
|
||||
- name: blank-imports
|
||||
disabled: true
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: error-naming
|
||||
- name: exported
|
||||
disabled: true
|
||||
- name: if-return
|
||||
disabled: true
|
||||
- name: increment-decrement
|
||||
- name: var-naming
|
||||
disabled: true
|
||||
- name: var-declaration
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: time-naming
|
||||
- name: unexported-return
|
||||
disabled: true
|
||||
- name: indent-error-flow
|
||||
disabled: true
|
||||
- name: errorf
|
||||
- name: empty-block
|
||||
disabled: true
|
||||
- name: superfluous-else
|
||||
- name: unused-parameter
|
||||
disabled: true
|
||||
- name: unreachable-code
|
||||
- name: redefines-builtin-id
|
||||
|
||||
rowserrcheck:
|
||||
packages:
|
||||
- github.com/jmoiron/sqlx
|
||||
@@ -1,64 +0,0 @@
|
||||
project_name: stash
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
builds:
|
||||
- binary: stash-win
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=x86_64-w64-mingw32-gcc
|
||||
- CXX=x86_64-w64-mingw32-g++
|
||||
flags:
|
||||
- -tags
|
||||
- extended
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- binary: stash-osx
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=o64-clang
|
||||
- CXX=o64-clang++
|
||||
flags:
|
||||
- -tags
|
||||
- extended
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- binary: stash-linux
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
flags:
|
||||
- -tags
|
||||
- extended
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
archive:
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
|
||||
replacements:
|
||||
amd64: 64bit
|
||||
386: 32bit
|
||||
arm: ARM
|
||||
arm64: ARM64
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
openbsd: OpenBSD
|
||||
netbsd: NetBSD
|
||||
freebsd: FreeBSD
|
||||
dragonfly: DragonFlyBSD
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
release:
|
||||
draft: true
|
||||
@@ -1,118 +0,0 @@
|
||||
if: tag != latest_develop # dont build for the latest_develop tagged version
|
||||
|
||||
dist: xenial
|
||||
git:
|
||||
depth: false
|
||||
language: go
|
||||
go:
|
||||
- 1.13.x
|
||||
services:
|
||||
- docker
|
||||
env:
|
||||
global:
|
||||
- GO111MODULE=on
|
||||
before_install:
|
||||
- set -e
|
||||
# Configure environment so changes are picked up when the Docker daemon is restarted after upgrading
|
||||
- echo '{"experimental":true}' | sudo tee /etc/docker/daemon.json
|
||||
- export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
# Upgrade to Docker CE 19.03 for BuildKit support
|
||||
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||
- sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
|
||||
- sudo apt-get update
|
||||
- sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
|
||||
# install binfmt docker container, this container uses qemu to run arm programs transparently allowng docker to build arm 6,7,8 containers.
|
||||
- docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
|
||||
# Show info to simplify debugging and create a builder that can build the platforms we need
|
||||
- docker info
|
||||
- docker buildx create --name builder --use
|
||||
- docker buildx inspect --bootstrap
|
||||
- docker buildx ls
|
||||
|
||||
install:
|
||||
- echo -e "machine github.com\n login $CI_USER_TOKEN" > ~/.netrc
|
||||
- nvm install 12
|
||||
- travis_retry make pre-ui
|
||||
- make generate
|
||||
- CI=false make ui-validate ui-only
|
||||
#- go get -v github.com/mgechev/revive
|
||||
script:
|
||||
# left lint off to avoid getting extra dependency
|
||||
#- make lint
|
||||
- make fmt-check vet it
|
||||
after_success:
|
||||
- docker pull stashapp/compiler:4
|
||||
- sh ./scripts/cross-compile.sh
|
||||
- git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
|
||||
- sha1sum dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
|
||||
- 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then sh ./scripts/upload-pull-request.sh; fi'
|
||||
before_deploy:
|
||||
# push the latest tag when on the develop branch
|
||||
- if [ "$TRAVIS_BRANCH" = "develop" ]; then git tag -f latest_develop; git push -f --tags; fi
|
||||
- export RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')
|
||||
- export STASH_VERSION=$(git describe --tags --exclude latest_develop)
|
||||
# set TRAVIS_TAG explcitly to the version so that it doesn't pick up latest_develop
|
||||
- if [ "$TRAVIS_BRANCH" = "master" ]; then export TRAVIS_TAG=${STASH_VERSION}; fi
|
||||
deploy:
|
||||
# latest develop release
|
||||
- provider: releases
|
||||
# use the v2 release provider for proper release note setting
|
||||
edge: true
|
||||
api_key:
|
||||
secure: tGJ2q62CfPdayid2qEtW2aGRhMgCl3lBXYYQqp3eH0vFgIIf6cs7IDX7YC/x3XKMEQ/iMLZmtCXZvSTqNrD6Sk7MSnt30GIs+4uxIZDnnd8mV5X3K4n4gjD+NAORc4DrQBvUGrYMKJsR5gtkH0nu6diWb1o1If7OiJEuCPRhrmQYcza7NUdABnA9Z2wn2RNUV9Ga33WUCqLMEU5GtNBlfQPiP/khCQrqn/ocR6wUjYut3J6YagzqH4wsfJi3glHyWtowcNIw1LZi5zFxHD/bRBT4Tln7yypkjWNq9eQILA6i6kRUGf7ggyTx26/k8n4tnu+QD0vVh4EcjlThpU/LGyUXzKrrxjRwaDZnM0oYxg5AfHcBuAiAdo0eWnV3lEWRfTJMIVb9MPf4qDmzR4RREfB5OXOxwq3ODeCcJE8sTIMD/wBPZrlqS/QrRpND2gn2X4snkVukN9t9F4CMTFMtVSzFV7TDJW5E5Lq6VEExulteQhs6kcK9NRPNAaLgRQAw7X9kVWfDtiGUP+fE2i8F9Bo8bm7sOT5O5VPMPykx3EgeNg1IqIgMTCsMlhMJT4xBJoQUgmd2wWyf3Ryw+P+sFgdb5Sd7+lFgJBjMUUoOxMxAOiEgdFvCXcr+/Udyz2RdtetU1/6VzXzLPcKOw0wubZeBkISqu7o9gpfdMP9Eq00=
|
||||
file:
|
||||
- dist/stash-osx
|
||||
- dist/stash-win.exe
|
||||
- dist/stash-linux
|
||||
- dist/stash-linux-arm64v8
|
||||
- dist/stash-linux-arm32v7
|
||||
- dist/stash-pi
|
||||
- CHECKSUMS_SHA1
|
||||
skip_cleanup: true
|
||||
overwrite: true
|
||||
name: "${STASH_VERSION}: Latest development build"
|
||||
release_notes: "**${RELEASE_DATE}**\n This is always the latest committed version on the develop branch. Use as your own risk!"
|
||||
prerelease: true
|
||||
on:
|
||||
repo: stashapp/stash
|
||||
branch: develop
|
||||
# docker image build for develop release
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: bash ./docker/ci/x86_64/docker_push.sh development
|
||||
on:
|
||||
repo: stashapp/stash
|
||||
branch: develop
|
||||
# official master release - only build when tagged
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: tGJ2q62CfPdayid2qEtW2aGRhMgCl3lBXYYQqp3eH0vFgIIf6cs7IDX7YC/x3XKMEQ/iMLZmtCXZvSTqNrD6Sk7MSnt30GIs+4uxIZDnnd8mV5X3K4n4gjD+NAORc4DrQBvUGrYMKJsR5gtkH0nu6diWb1o1If7OiJEuCPRhrmQYcza7NUdABnA9Z2wn2RNUV9Ga33WUCqLMEU5GtNBlfQPiP/khCQrqn/ocR6wUjYut3J6YagzqH4wsfJi3glHyWtowcNIw1LZi5zFxHD/bRBT4Tln7yypkjWNq9eQILA6i6kRUGf7ggyTx26/k8n4tnu+QD0vVh4EcjlThpU/LGyUXzKrrxjRwaDZnM0oYxg5AfHcBuAiAdo0eWnV3lEWRfTJMIVb9MPf4qDmzR4RREfB5OXOxwq3ODeCcJE8sTIMD/wBPZrlqS/QrRpND2gn2X4snkVukN9t9F4CMTFMtVSzFV7TDJW5E5Lq6VEExulteQhs6kcK9NRPNAaLgRQAw7X9kVWfDtiGUP+fE2i8F9Bo8bm7sOT5O5VPMPykx3EgeNg1IqIgMTCsMlhMJT4xBJoQUgmd2wWyf3Ryw+P+sFgdb5Sd7+lFgJBjMUUoOxMxAOiEgdFvCXcr+/Udyz2RdtetU1/6VzXzLPcKOw0wubZeBkISqu7o9gpfdMP9Eq00=
|
||||
file:
|
||||
- dist/stash-osx
|
||||
- dist/stash-win.exe
|
||||
- dist/stash-linux
|
||||
- dist/stash-linux-arm64v8
|
||||
- dist/stash-linux-arm32v7
|
||||
- dist/stash-pi
|
||||
- CHECKSUMS_SHA1
|
||||
# make the release a draft so the maintainers can confirm before releasing
|
||||
draft: true
|
||||
skip_cleanup: true
|
||||
overwrite: true
|
||||
# don't write the body. To be done manually for now. In future we might
|
||||
# want to generate the changelog or get it from a file
|
||||
name: ${STASH_VERSION}
|
||||
on:
|
||||
repo: stashapp/stash
|
||||
tags: true
|
||||
# make sure we don't release using the latest_develop tag
|
||||
condition: $TRAVIS_TAG != latest_develop
|
||||
# docker image build for master release
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: bash ./docker/ci/x86_64/docker_push.sh latest
|
||||
on:
|
||||
repo: stashapp/stash
|
||||
tags: true
|
||||
# make sure we don't release using the latest_develop tag
|
||||
condition: $TRAVIS_TAG != latest_develop
|
||||
106
Makefile
106
Makefile
@@ -9,7 +9,7 @@ endif
|
||||
ifdef IS_WIN
|
||||
SEPARATOR := &&
|
||||
SET := set
|
||||
else
|
||||
else
|
||||
SEPARATOR := ;
|
||||
SET := export
|
||||
endif
|
||||
@@ -23,9 +23,8 @@ ifdef OUTPUT
|
||||
endif
|
||||
|
||||
export CGO_ENABLED = 1
|
||||
export GO111MODULE = on
|
||||
|
||||
.PHONY: release pre-build install clean
|
||||
.PHONY: release pre-build
|
||||
|
||||
release: generate ui build-release
|
||||
|
||||
@@ -42,16 +41,22 @@ ifndef STASH_VERSION
|
||||
$(eval STASH_VERSION := $(shell git describe --tags --exclude latest_develop))
|
||||
endif
|
||||
|
||||
ifndef OFFICIAL_BUILD
|
||||
$(eval OFFICIAL_BUILD := false)
|
||||
endif
|
||||
|
||||
build: pre-build
|
||||
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/pkg/api.version=$(STASH_VERSION)' -X 'github.com/stashapp/stash/pkg/api.buildstamp=$(BUILD_DATE)' -X 'github.com/stashapp/stash/pkg/api.githash=$(GITHASH)')
|
||||
go build $(OUTPUT) -mod=vendor -v -tags "sqlite_omit_load_extension osusergo netgo" -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS)"
|
||||
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/pkg/api.officialBuild=$(OFFICIAL_BUILD)')
|
||||
go build $(OUTPUT) -mod=vendor -v -tags "sqlite_omit_load_extension osusergo netgo" $(GO_BUILD_FLAGS) -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS)"
|
||||
|
||||
# strips debug symbols from the release build
|
||||
# consider -trimpath in go build if we move to go 1.13+
|
||||
build-release: EXTRA_LDFLAGS := -s -w
|
||||
build-release: GO_BUILD_FLAGS := -trimpath
|
||||
build-release: build
|
||||
|
||||
build-release-static: EXTRA_LDFLAGS := -extldflags=-static -s -w
|
||||
build-release-static: GO_BUILD_FLAGS := -trimpath
|
||||
build-release-static: build
|
||||
|
||||
# cross-compile- targets should be run within the compiler docker container
|
||||
@@ -62,13 +67,21 @@ cross-compile-windows: export CXX := x86_64-w64-mingw32-g++
|
||||
cross-compile-windows: OUTPUT := -o dist/stash-win.exe
|
||||
cross-compile-windows: build-release-static
|
||||
|
||||
cross-compile-osx: export GOOS := darwin
|
||||
cross-compile-osx: export GOARCH := amd64
|
||||
cross-compile-osx: export CC := o64-clang
|
||||
cross-compile-osx: export CXX := o64-clang++
|
||||
cross-compile-osx: OUTPUT := -o dist/stash-osx
|
||||
cross-compile-osx-intel: export GOOS := darwin
|
||||
cross-compile-osx-intel: export GOARCH := amd64
|
||||
cross-compile-osx-intel: export CC := o64-clang
|
||||
cross-compile-osx-intel: export CXX := o64-clang++
|
||||
cross-compile-osx-intel: OUTPUT := -o dist/stash-osx
|
||||
# can't use static build for OSX
|
||||
cross-compile-osx: build-release
|
||||
cross-compile-osx-intel: build-release
|
||||
|
||||
cross-compile-osx-applesilicon: export GOOS := darwin
|
||||
cross-compile-osx-applesilicon: export GOARCH := arm64
|
||||
cross-compile-osx-applesilicon: export CC := oa64e-clang
|
||||
cross-compile-osx-applesilicon: export CXX := oa64e-clang++
|
||||
cross-compile-osx-applesilicon: OUTPUT := -o dist/stash-osx-applesilicon
|
||||
# can't use static build for OSX
|
||||
cross-compile-osx-applesilicon: build-release
|
||||
|
||||
cross-compile-linux: export GOOS := linux
|
||||
cross-compile-linux: export GOARCH := amd64
|
||||
@@ -95,20 +108,26 @@ cross-compile-pi: export CC := arm-linux-gnueabi-gcc
|
||||
cross-compile-pi: OUTPUT := -o dist/stash-pi
|
||||
cross-compile-pi: build-release-static
|
||||
|
||||
cross-compile-all: cross-compile-windows cross-compile-osx cross-compile-linux cross-compile-linux-arm64v8 cross-compile-linux-arm32v7 cross-compile-pi
|
||||
|
||||
install:
|
||||
packr2 install
|
||||
|
||||
clean:
|
||||
packr2 clean
|
||||
cross-compile-all:
|
||||
make cross-compile-windows
|
||||
make cross-compile-osx-intel
|
||||
make cross-compile-osx-applesilicon
|
||||
make cross-compile-linux
|
||||
make cross-compile-linux-arm64v8
|
||||
make cross-compile-linux-arm32v7
|
||||
make cross-compile-pi
|
||||
|
||||
# Regenerates GraphQL files
|
||||
.PHONY: generate
|
||||
generate:
|
||||
go generate -mod=vendor
|
||||
generate: generate-backend generate-frontend
|
||||
|
||||
.PHONY: generate-frontend
|
||||
generate-frontend:
|
||||
cd ui/v2.5 && yarn run gqlgen
|
||||
|
||||
.PHONY: generate-backend
|
||||
generate-backend:
|
||||
go generate -mod=vendor
|
||||
|
||||
# Regenerates stash-box client files
|
||||
.PHONY: generate-stash-box-client
|
||||
generate-stash-box-client:
|
||||
@@ -119,23 +138,13 @@ generate-stash-box-client:
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
# Ensures that changed files have had gofmt run on them
|
||||
.PHONY: fmt-check
|
||||
fmt-check:
|
||||
sh ./scripts/check-gofmt.sh
|
||||
|
||||
# Runs go vet on the project's source code.
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet -mod=vendor ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
revive -config revive.toml -exclude ./vendor/... ./...
|
||||
golangci-lint run
|
||||
|
||||
# runs unit tests - excluding integration tests
|
||||
.PHONY: test
|
||||
test:
|
||||
test:
|
||||
go test -mod=vendor ./...
|
||||
|
||||
# runs all tests - including integration tests
|
||||
@@ -148,23 +157,19 @@ it:
|
||||
generate-test-mocks:
|
||||
go run -mod=vendor github.com/vektra/mockery/v2 --dir ./pkg/models --name '.*ReaderWriter' --outpkg mocks --output ./pkg/models/mocks
|
||||
|
||||
# installs UI dependencies. Run when first cloning repository, or if UI
|
||||
# installs UI dependencies. Run when first cloning repository, or if UI
|
||||
# dependencies have changed
|
||||
.PHONY: pre-ui
|
||||
pre-ui:
|
||||
cd ui/v2.5 && yarn install --frozen-lockfile
|
||||
|
||||
.PHONY: ui-only
|
||||
ui-only: pre-build
|
||||
.PHONY: ui
|
||||
ui: pre-build
|
||||
$(SET) REACT_APP_DATE="$(BUILD_DATE)" $(SEPARATOR) \
|
||||
$(SET) REACT_APP_GITHASH=$(GITHASH) $(SEPARATOR) \
|
||||
$(SET) REACT_APP_STASH_VERSION=$(STASH_VERSION) $(SEPARATOR) \
|
||||
cd ui/v2.5 && yarn build
|
||||
|
||||
.PHONY: ui
|
||||
ui: ui-only
|
||||
packr2
|
||||
|
||||
.PHONY: ui-start
|
||||
ui-start: pre-build
|
||||
$(SET) REACT_APP_DATE="$(BUILD_DATE)" $(SEPARATOR) \
|
||||
@@ -181,12 +186,19 @@ fmt-ui:
|
||||
ui-validate:
|
||||
cd ui/v2.5 && yarn run validate
|
||||
|
||||
# just repacks the packr files - use when updating migrations and packed files without
|
||||
# rebuilding the UI
|
||||
.PHONY: packr
|
||||
packr:
|
||||
packr2
|
||||
|
||||
# runs all of the tests and checks required for a PR to be accepted
|
||||
.PHONY: validate
|
||||
validate: ui-validate fmt-check vet lint it
|
||||
validate: validate-frontend validate-backend
|
||||
|
||||
# runs all of the frontend PR-acceptance steps
|
||||
.PHONY: validate-frontend
|
||||
validate-frontend: ui-validate
|
||||
|
||||
# runs all of the backend PR-acceptance steps
|
||||
.PHONY: validate-backend
|
||||
validate-backend: lint it
|
||||
|
||||
# locally builds and tags a 'stash/build' docker image
|
||||
.PHONY: docker-build
|
||||
docker-build: pre-build
|
||||
docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/build -f docker/build/x86_64/Dockerfile .
|
||||
|
||||
145
README.md
145
README.md
@@ -1,80 +1,46 @@
|
||||
# Stash
|
||||
https://stashapp.cc
|
||||
|
||||
[](https://travis-ci.org/stashapp/stash)
|
||||
[](https://github.com/stashapp/stash/actions/workflows/build.yml)
|
||||
[](https://hub.docker.com/r/stashapp/Stash 'DockerHub')
|
||||
[](https://goreportcard.com/report/github.com/stashapp/stash)
|
||||
[](https://discord.gg/2TsNFKt)
|
||||
|
||||
https://stashapp.cc
|
||||
### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.**
|
||||

|
||||
|
||||
**Stash is a locally hosted web-based app written in Go which organizes and serves your porn.**
|
||||
|
||||
* It can gather information about videos in your collection from the internet, and is extensible through the use of community-built plugins for a large number of content producers.
|
||||
* It supports a wide variety of both video and image formats.
|
||||
* Stash gathers information about videos in your collection from the internet, and is extensible through the use of community-built plugins for a large number of content producers and sites.
|
||||
* Stash supports a wide variety of both video and image formats.
|
||||
* You can tag videos and find them later.
|
||||
* It provides statistics about performers, tags, studios and other things.
|
||||
* Stash provides statistics about performers, tags, studios and more.
|
||||
|
||||
You can [watch a SFW demo video](https://vimeo.com/545323354) to see it in action.
|
||||
|
||||
For further information you can [read the in-app manual](ui/v2.5/src/docs/en).
|
||||
|
||||
# Installing stash
|
||||
# Installing Stash
|
||||
|
||||
## via 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-osx-applesilicon) <br /> <sup><sub>[Development Preview (Apple Silicon)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-osx-applesilicon)</sub></sup> <br>[Latest Release (Intel)](https://github.com/stashapp/stash/releases/latest/download/stash-osx) <br /> <sup><sub>[Development Preview (Intel)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-osx)</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>
|
||||
|
||||
Follow [this README.md in the docker directory.](docker/production/README.md)
|
||||
|
||||
## Pre-Compiled Binaries
|
||||
|
||||
The Stash server runs on macOS, Windows, and Linux. Download the [latest release here](https://github.com/stashapp/stash/releases).
|
||||
|
||||
Run the executable (double click the exe on windows or run `./stash-osx` / `./stash-linux` from the terminal on macOS / Linux) and navigate to either https://localhost:9999 or http://localhost:9999 to get started.
|
||||
## Getting Started
|
||||
Run the executable (double click the exe on windows or run `./stash-osx` / `./stash-linux` from the terminal on macOS / Linux) to get started.
|
||||
|
||||
*Note for Windows users:* 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
|
||||
|
||||
If stash is unable to find or download FFMPEG then download it yourself from the link for your platform:
|
||||
|
||||
* [macOS ffmpeg](https://evermeet.cx/ffmpeg/ffmpeg-4.3.1.zip), [macOS ffprobe](https://evermeet.cx/ffmpeg/ffprobe-4.3.1.zip)
|
||||
* [Windows](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip)
|
||||
* [Linux](https://www.johnvansickle.com/ffmpeg/)
|
||||
|
||||
The `ffmpeg(.exe)` and `ffprobe(.exe)` files should be placed in `~/.stash` on macOS / Linux or `C:\Users\YourUsername\.stash` on Windows.
|
||||
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
|
||||
|
||||
## Quickstart Guide
|
||||
1) Download and install Stash and its dependencies
|
||||
2) Run Stash. It will prompt you for some configuration options and a directory to index (you can also do this step afterward)
|
||||
3) After configuration, launch your web browser and navigate to the URL shown within the Stash app.
|
||||
Download and run Stash. It will prompt you for some configuration options and a directory to index (you can also do this step afterward)
|
||||
|
||||
**Note that Stash does not currently retrieve and organize information about your entire library automatically.** You will need to help it along through the use of [scrapers](blob/develop/ui/v2.5/src/docs/en/Scraping.md). The Stash community has developed scrapers for many popular data sources which can be downloaded and installed from [this repository](https://github.com/stashapp/CommunityScrapers).
|
||||
**If you'd like to automatically retrieve and organize information about your entire library,** You will need to download some [scrapers](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en/Scraping.md). The Stash community has developed scrapers for many popular data sources which can be downloaded and installed from [this repository](https://github.com/stashapp/CommunityScrapers).
|
||||
|
||||
The simplest way to tag a large number of files is by using the [Tagger](https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/docs/en/Tagger.md) which uses filename keywords to help identify the file and pull in scene and performer information from our stash-box database. Note that this data source is not comprehensive and you may need to use the scrapers to identify some of your media.
|
||||
|
||||
## CLI
|
||||
|
||||
Stash runs as a command-line app and local web server. There are some command-line options available, which you can see by running `stash --help`.
|
||||
|
||||
For example, to run stash locally on port 80 run it like this (OSX / Linux) `stash --host 127.0.0.1 --port 80`
|
||||
|
||||
## SSL (HTTPS)
|
||||
|
||||
Stash can run over HTTPS with some additional work. First you must generate a SSL certificate and key combo. Here is an example using openssl:
|
||||
|
||||
`openssl req -x509 -newkey rsa:4096 -sha256 -days 7300 -nodes -keyout stash.key -out stash.crt -extensions san -config <(echo "[req]"; echo distinguished_name=req; echo "[san]"; echo subjectAltName=DNS:stash.server,IP:127.0.0.1) -subj /CN=stash.server`
|
||||
|
||||
This command would need customizing for your environment. [This link](https://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl) might be useful.
|
||||
|
||||
Once you have a certificate and key file name them `stash.crt` and `stash.key` and place them in the same directory as the `config.yml` file, or the `~/.stash` directory. Stash detects these and starts up using HTTPS rather than HTTP.
|
||||
|
||||
# Customization
|
||||
|
||||
## Themes and CSS Customization
|
||||
There is a [directory of community-created themes](https://github.com/stashapp/stash/wiki/Themes) on our Wiki, along with instructions on how to install them.
|
||||
|
||||
You can also make Stash interface fit your desired style with [Custom CSS snippets](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets) and [CSS Tweaks](https://github.com/stashapp/stash/wiki/CSS-Tweaks).
|
||||
|
||||
# Support (FAQ)
|
||||
|
||||
Answers to other Frequently Asked Questions can be found [on our Wiki](https://github.com/stashapp/stash/wiki/FAQ)
|
||||
@@ -82,79 +48,18 @@ Answers to other Frequently Asked Questions can be found [on our Wiki](https://g
|
||||
For issues not addressed there, there are a few options.
|
||||
|
||||
* Read the [Wiki](https://github.com/stashapp/stash/wiki)
|
||||
* Check the in-app documentation (also available [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en)
|
||||
* Check the in-app documentation, in the top right corner of the app (also available [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en)
|
||||
* Join the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support.
|
||||
|
||||
# Compiling From Source Code
|
||||
# Customization
|
||||
|
||||
## Pre-requisites
|
||||
## Themes and CSS Customization
|
||||
There is a [directory of community-created themes](https://github.com/stashapp/stash/wiki/Themes) on our Wiki, along with instructions on how to install them.
|
||||
|
||||
* [Go](https://golang.org/dl/)
|
||||
* [Revive](https://github.com/mgechev/revive) - Configurable linter
|
||||
* Go Install: `go get github.com/mgechev/revive`
|
||||
* [Packr2](https://github.com/gobuffalo/packr/) - Static asset bundler
|
||||
* Go Install: `go get github.com/gobuffalo/packr/v2/packr2`
|
||||
* [Binary Download](https://github.com/gobuffalo/packr/releases)
|
||||
* [Yarn](https://yarnpkg.com/en/docs/install) - Yarn package manager
|
||||
* Run `yarn install --frozen-lockfile` in the `stash/ui/v2.5` folder (before running make generate for first time).
|
||||
You can also make Stash interface fit your desired style with [Custom CSS snippets](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets).
|
||||
|
||||
NOTE: You may need to run the `go get` commands outside the project directory to avoid modifying the projects module file.
|
||||
# For Developers
|
||||
|
||||
## Environment
|
||||
Pull requests are welcome!
|
||||
|
||||
### macOS
|
||||
|
||||
TODO
|
||||
|
||||
### Windows
|
||||
|
||||
1. Download and install [Go for Windows](https://golang.org/dl/)
|
||||
2. Download and install [MingW](https://sourceforge.net/projects/mingw-w64/)
|
||||
3. Search for "advanced system settings" and open the system properties dialog.
|
||||
1. Click the `Environment Variables` button
|
||||
2. Add `GO111MODULE=on`
|
||||
3. Under system variables find the `Path`. Edit and add `C:\Program Files\mingw-w64\*\mingw64\bin` (replace * with the correct path).
|
||||
|
||||
NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
|
||||
|
||||
## Commands
|
||||
|
||||
* `make generate` - Generate Go and UI GraphQL files
|
||||
* `make build` - Builds the binary (make sure to build the UI as well... see below)
|
||||
* `make pre-ui` - Installs the UI dependencies. Only needs to be run once before building the UI for the first time, or if the dependencies are updated
|
||||
* `make fmt-ui` - Formats the UI source code.
|
||||
* `make ui` - Builds the frontend and the packr2 files
|
||||
* `make packr` - Generate packr2 files (sub-target of `ui`. Use to regenerate packr2 files without rebuilding UI)
|
||||
* `make vet` - Run `go vet`
|
||||
* `make lint` - Run the linter
|
||||
* `make fmt` - Run `go fmt`
|
||||
* `make fmt-check` - Ensure changed files are formatted correctly
|
||||
* `make it` - Run the unit and integration tests
|
||||
* `make validate` - Run all of the tests and checks required to submit a PR
|
||||
* `make ui-start` - Runs the UI in development mode. Requires a running stash server to connect to. Stash port can be changed from the default of `9999` with environment variable `REACT_APP_PLATFORM_PORT`.
|
||||
|
||||
## Building a release
|
||||
|
||||
1. Run `make generate` to create generated files
|
||||
2. Run `make ui` to compile the frontend
|
||||
3. Run `make build` to build the executable for your current platform
|
||||
|
||||
## 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:
|
||||
|
||||
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashappdev/compiler:latest /bin/bash`
|
||||
|
||||
## Profiling
|
||||
|
||||
Stash can be profiled using the `--cpuprofile <output profile filename>` command line flag.
|
||||
|
||||
The resulting file can then be used with pprof as follows:
|
||||
|
||||
`go tool pprof <path to binary> <path to profile filename>`
|
||||
|
||||
With `graphviz` installed and in the path, a call graph can be generated with:
|
||||
|
||||
`go tool pprof -svg <path to binary> <path to profile filename> > <output svg file>`
|
||||
See [Development](docs/DEVELOPMENT.md) and [Contributing](docs/CONTRIBUTING.md) for information on working with the codebase, getting a local development setup, and contributing changes.
|
||||
|
||||
@@ -1,61 +1,38 @@
|
||||
# this dockerfile must be built from the top-level stash directory
|
||||
# ie from top=level stash:
|
||||
# docker build -t stash/build -f docker/build/x86_64/Dockerfile .
|
||||
# This dockerfile should be built with `make docker-build` from the stash root.
|
||||
|
||||
FROM golang:1.13.15 as compiler
|
||||
|
||||
RUN apt-get update && apt-get install -y apt-transport-https
|
||||
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
|
||||
# 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 apt-get update && \
|
||||
apt-get install -y nodejs yarn xz-utils --no-install-recommends || exit 1; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
ENV PACKR2_VERSION=2.0.2
|
||||
ENV PACKR2_SHA=f95ff4c96d7a28813220df030ad91700b8464fe292ab3e1dc9582305c2a338d2
|
||||
ENV PACKR2_DOWNLOAD_FILE=packr_${PACKR2_VERSION}_linux_amd64.tar.gz
|
||||
ENV PACKR2_DOWNLOAD_URL=https://github.com/gobuffalo/packr/releases/download/v${PACKR2_VERSION}/${PACKR2_DOWNLOAD_FILE}
|
||||
|
||||
WORKDIR /
|
||||
RUN wget ${PACKR2_DOWNLOAD_URL}; \
|
||||
echo "$PACKR2_SHA $PACKR2_DOWNLOAD_FILE" | sha256sum -c - || exit 1; \
|
||||
tar -xzf $PACKR2_DOWNLOAD_FILE -C /usr/bin/ packr2; \
|
||||
rm $PACKR2_DOWNLOAD_FILE;
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN wget -O /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
|
||||
tar xf /ffmpeg.tar.xz && \
|
||||
rm ffmpeg.tar.xz && \
|
||||
mv /ffmpeg*/ /ffmpeg/
|
||||
|
||||
# copy the ui yarn stuff so that it doesn't get rebuilt every time
|
||||
# Build Frontend
|
||||
FROM node:alpine as frontend
|
||||
RUN apk add --no-cache make
|
||||
## cache node_modules separately
|
||||
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
|
||||
|
||||
WORKDIR /stash
|
||||
RUN yarn --cwd ui/v2.5 install --frozen-lockfile
|
||||
RUN yarn --cwd ui/v2.5 install --frozen-lockfile.
|
||||
COPY Makefile /stash/
|
||||
COPY ./graphql /stash/graphql/
|
||||
COPY ./ui /stash/ui/
|
||||
RUN make generate-frontend
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
|
||||
|
||||
COPY . /stash/
|
||||
ENV GO111MODULE=on
|
||||
|
||||
RUN make generate
|
||||
RUN make ui
|
||||
# Build Backend
|
||||
FROM golang:1.17-alpine as backend
|
||||
RUN apk add --no-cache make alpine-sdk
|
||||
WORKDIR /stash
|
||||
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
||||
COPY ./scripts /stash/scripts/
|
||||
COPY ./vendor /stash/vendor/
|
||||
COPY ./pkg /stash/pkg/
|
||||
COPY --from=frontend /stash /stash/
|
||||
RUN make generate-backend
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN make build
|
||||
|
||||
FROM ubuntu:20.04 as app
|
||||
|
||||
RUN apt-get update && apt-get -y install ca-certificates
|
||||
COPY --from=compiler /stash/stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
|
||||
|
||||
# Final Runnable Image
|
||||
FROM alpine:latest
|
||||
RUN apk add --no-cache ca-certificates vips-tools ffmpeg
|
||||
COPY --from=backend /stash/stash /usr/bin/
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
EXPOSE 9999
|
||||
CMD ["stash"]
|
||||
|
||||
|
||||
ENTRYPOINT ["stash"]
|
||||
@@ -1,13 +1,13 @@
|
||||
# Introduction
|
||||
|
||||
This dockerfile is used to build a stash docker container using the current source code.
|
||||
This dockerfile is used to build a stash docker container using the current source code. This is ideal for testing your current branch in docker. Note that it does not include python, so python-based scrapers will not work in this image. The production docker images distributed by the project contain python and the necessary packages.
|
||||
|
||||
# Building the docker container
|
||||
|
||||
From the top-level directory (should contain `main.go` file):
|
||||
|
||||
```
|
||||
docker build -t stash/build -f ./docker/build/x86_64/Dockerfile .
|
||||
make docker-build
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM ubuntu:20.04 AS prep
|
||||
FROM --platform=$BUILDPLATFORM alpine:latest AS binary
|
||||
ARG TARGETPLATFORM
|
||||
WORKDIR /
|
||||
COPY stash-* /
|
||||
@@ -8,15 +8,12 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-pi; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then BIN=stash-linux; \
|
||||
fi; \
|
||||
mv $BIN /stash
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt update && apt install -y python3 python-is-python3 python3-requests python3-requests-toolbelt python3-lxml python3-pip && pip3 install cloudscraper
|
||||
FROM ubuntu:20.04 as app
|
||||
run apt update && apt install -y python3 python-is-python3 python3-requests python3-requests-toolbelt python3-lxml python3-mechanicalsoup ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=prep /stash /usr/bin/
|
||||
COPY --from=prep /usr/local/lib/python3.8/dist-packages /usr/local/lib/python3.8/dist-packages
|
||||
|
||||
FROM --platform=$TARGETPLATFORM alpine:latest AS app
|
||||
COPY --from=binary /stash /usr/bin/
|
||||
RUN apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg vips-tools && pip install --no-cache-dir mechanicalsoup cloudscraper
|
||||
RUN ln -s /usr/bin/python3 /usr/bin/python
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
EXPOSE 9999
|
||||
CMD ["stash"]
|
||||
|
||||
|
||||
@@ -10,5 +10,5 @@ done
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
|
||||
# must build the image from dist directory
|
||||
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push $DOCKER_TAGS -f docker/ci/x86_64/Dockerfile dist/
|
||||
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 --push $DOCKER_TAGS -f docker/ci/x86_64/Dockerfile dist/
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
FROM golang:1.13.15
|
||||
FROM golang:1.17
|
||||
|
||||
LABEL maintainer="stashappdev@gmail.com"
|
||||
|
||||
ENV PACKR2_VERSION=2.0.2
|
||||
ENV PACKR2_SHA=f95ff4c96d7a28813220df030ad91700b8464fe292ab3e1dc9582305c2a338d2
|
||||
ENV PACKR2_DOWNLOAD_FILE=packr_${PACKR2_VERSION}_linux_amd64.tar.gz
|
||||
ENV PACKR2_DOWNLOAD_URL=https://github.com/gobuffalo/packr/releases/download/v${PACKR2_VERSION}/${PACKR2_DOWNLOAD_FILE}
|
||||
LABEL maintainer="https://discord.gg/2TsNFKt"
|
||||
|
||||
# Install tools
|
||||
RUN apt-get update && apt-get install -y apt-transport-https
|
||||
@@ -18,10 +13,10 @@ RUN cat yarn.gpg | apt-key add - && \
|
||||
rm yarn.gpg
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y automake autogen \
|
||||
apt-get install -y automake autogen cmake \
|
||||
libtool libxml2-dev uuid-dev libssl-dev bash \
|
||||
patch make tar xz-utils bzip2 gzip sed cpio \
|
||||
gcc-8-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
|
||||
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 \
|
||||
@@ -29,21 +24,22 @@ RUN apt-get update && \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
# Cross compile setup
|
||||
ENV OSX_SDK_VERSION 10.11
|
||||
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/ndeloof/golang-cross/raw/113fix/${OSX_SDK_DOWNLOAD_FILE}
|
||||
ENV OSX_SDK_SHA=98cdd56e0f6c1f9e1af25e11dd93d2e7d306a4aa50430a2bc6bc083ac67efbb8
|
||||
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
|
||||
|
||||
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 && \
|
||||
git -C osxcross checkout a9317c18a3a457ca0a657f08cc4d0d43c6cf8953 || exit 1; \
|
||||
mv $OSX_SDK_DOWNLOAD_FILE osxcross/tarballs/ && \
|
||||
UNATTENDED=yes SDK_VERSION=${OSX_SDK_VERSION} OSX_VERSION_MIN=10.9 osxcross/build.sh || exit 1; \
|
||||
mv osxcross/target $OSX_NDK_X86; \
|
||||
rm -rf osxcross;
|
||||
git clone https://github.com/tpoechtrager/osxcross.git; \
|
||||
mv $OSX_SDK_DOWNLOAD_FILE osxcross/tarballs/
|
||||
|
||||
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;
|
||||
|
||||
ENV PATH $OSX_NDK_X86/bin:$PATH
|
||||
|
||||
@@ -51,19 +47,21 @@ RUN mkdir -p /root/.ssh; \
|
||||
chmod 0700 /root/.ssh; \
|
||||
ssh-keyscan github.com > /root/.ssh/known_hosts;
|
||||
|
||||
RUN wget ${PACKR2_DOWNLOAD_URL}; \
|
||||
echo "$PACKR2_SHA $PACKR2_DOWNLOAD_FILE" | sha256sum -c - || exit 1; \
|
||||
tar -xzf $PACKR2_DOWNLOAD_FILE -C /usr/bin/ packr2; \
|
||||
rm $PACKR2_DOWNLOAD_FILE;
|
||||
|
||||
CMD ["packr2", "version"]
|
||||
|
||||
|
||||
# Notes for self:
|
||||
|
||||
# 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
|
||||
# # 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 GO111MODULE=on goreleaser --config=goreleaser-extended.yml --skip-publish --skip-validate --rm-dist --release-notes=temp/0.48-relnotes-ready.md
|
||||
# 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=4
|
||||
version=5
|
||||
|
||||
latest:
|
||||
docker build -t ${user}/${repo}:latest .
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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 `.travis.yml` needs 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 `scripts/cross-compile.sh` script should also be updated to use the new version number tag, and `.travis.yml` needs to be updated to pull the correct image tag.
|
||||
|
||||
A MacOS univeral binary can be created using `lipo -create -output stash-osx-universal stash-osx stash-osx-applesilicon`, available in the image.
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
TMP=$(mktemp -d /tmp/XXXXXXXXXXX)
|
||||
SDK="MacOSX10.11.sdk"
|
||||
|
||||
mkdir -p $TMP/$SDK/usr/include/c++
|
||||
|
||||
cp -rf /Applications/Xcode7.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/$SDK $TMP &>/dev/null || true
|
||||
cp -rf /Applications/Xcode7.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1 $TMP/$SDK/usr/include/c++ || exit -1
|
||||
|
||||
tar -C $TMP -czf $SDK.tar.gz $SDK
|
||||
@@ -1,27 +0,0 @@
|
||||
FROM ubuntu:20.04 as prep
|
||||
LABEL MAINTAINER="https://discord.gg/Uz29ny"
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install curl xz-utils && \
|
||||
apt-get autoclean -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# added " to end of stash-linux clause so that it doesn't pick up the arm builds
|
||||
RUN curl -L -o /stash $(curl -s https://api.github.com/repos/stashapp/stash/releases/tags/latest_develop | awk '/browser_download_url/ && /stash-linux"/' | sed -e 's/.*: "\(.*\)"/\1/') && \
|
||||
chmod +x /stash
|
||||
|
||||
RUN curl --http1.1 -o /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
|
||||
tar xf /ffmpeg.tar.xz && \
|
||||
rm ffmpeg.tar.xz && \
|
||||
mv /ffmpeg*/ /ffmpeg/
|
||||
|
||||
FROM ubuntu:20.04 as app
|
||||
RUN apt-get update && apt-get -y install ca-certificates
|
||||
COPY --from=prep /stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
|
||||
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
EXPOSE 9999
|
||||
CMD ["stash"]
|
||||
@@ -1,53 +1,37 @@
|
||||
# Docker install on Ubuntu 18.04
|
||||
Installing StashApp can likely work on others if your OS either has it's own package manager or comes shipped with Docker and docker-compose.
|
||||
# Docker Installation (for most 64-bit GNU/Linux systems)
|
||||
StashApp is supported on most systems that support Docker and docker-compose. Your OS likely ships with or makes available the necessary packages.
|
||||
|
||||
## Dependencies
|
||||
The goal is to avoid as many dependencies as possible so for now the only pre-requisites you are required to have are `curl`, `docker`, and `docker-compose` for the most part your understanding of the technologies can be superficial so long as you can follow commands and are open to reading a bit you should be fine.
|
||||
Only `docker` and `docker-compose` are required. For the most part your understanding of the technologies can be superficial. So long as you can follow commands and are open to reading a bit, you should be fine.
|
||||
|
||||
### Docker
|
||||
|
||||
Docker is effectively the cross-platform software package repository it allows you to ship an entire environment in what's referred to as a container. Containers are intended to hold everything that is needed to ship what's required to run an application from one place to another with a degree of a standard that makes it easy for everyone along the way to reproduce the environment for their step in the chain.
|
||||
|
||||
The other side of docker is it brings everything that we would typically have to teach you about the individual components of your soon to be installed StashApp and ffmpeg, docker-compose wraps it up nicely in a handful of easy to follow steps that should result in the same environment on everyone's host.
|
||||
|
||||
The installation method we recommend is via the `docker.com` website however if your specific operating system's repository versions are at the latest along with docker you should be good to launch with you using whatever instructions you wish. The version of Docker we used in our deployment for testing this process was `Docker version 17.05.0-ce, build 89658be` however any versions later than this will be sufficient. At the writing of this tutorial, this was not the latest version of Docker.
|
||||
|
||||
#### Just the link to installation instructions, please
|
||||
Instructions for installing on Ubuntu are at the link that follows:
|
||||
https://docs.docker.com/install/linux/docker-ce/ubuntu/
|
||||
|
||||
If you plan on using other versions of OS you should at least aim to be a Linux base with an x86_64 CPU and the appropriate minimum version of the dependencies.
|
||||
|
||||
### Docker-compose
|
||||
Docker Compose's role in this deployment is to get you a fully working instance of StashApp exactly as you would need it to have a reasonable instance for testing / developing on, you could technically deploy a live instance with this, but without a reverse proxy, is not recommended. You are encouraged to learn how to use the Docker-Compose format, but it's not a required prerequisite for getting this running you need to have it installed successfully.
|
||||
|
||||
Install Docker Compose via this guide below, and it is essential if you're using an older version of Linux to use the official documentation from Docker.com because you require the more recent version of docker-compose at least version 3.4 aka 1.22.0 or newer.
|
||||
|
||||
#### Just the link to installation instructions, please
|
||||
https://docs.docker.com/compose/install/
|
||||
|
||||
### Install curl
|
||||
This one's easy, copy paste.
|
||||
|
||||
```
|
||||
apt update -y && \
|
||||
apt install -f curl
|
||||
```
|
||||
Installation instructions are available below, and if your distrobution's repository ships a current version of docker, you may use that.
|
||||
https://docs.docker.com/engine/install/
|
||||
|
||||
### 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 you can make your Linux console do it for you with this.
|
||||
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:
|
||||
|
||||
```
|
||||
curl -o ~/docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml
|
||||
mkdir stashapp && cd stashapp
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml
|
||||
```
|
||||
|
||||
Once you have that file where you want it, you can either modify the settings as you please OR you can run the following to get it up and running instantly.
|
||||
Once you have that file where you want it, modify the settings as you please, and then run:
|
||||
|
||||
```
|
||||
cd ~ && docker-compose up -d
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Installing StashApp this way will by default bind stash to port 9999 or in web browser terms. http://YOURIP:9999 or if you're doing this on your machine locally which is the only recommended production version of this container as is with no security configurations set at all is http://localhost:9999
|
||||
Installing StashApp this way will by default bind stash to port 9999. This is available in your web browser locally at http://localhost:9999 or on your network as http://YOUR-LOCAL-IP:9999
|
||||
|
||||
Good luck and have fun!
|
||||
|
||||
### Docker
|
||||
Docker is effectively a cross-platform software package repository. It allows you to ship an entire environment in what's referred to as a container. Containers are intended to hold everything that is needed to run an application from one place to another, making it easy for everyone along the way to reproduce the environment.
|
||||
|
||||
The StashApp docker container ships with everything you need to automatically build and run stash, including ffmpeg.
|
||||
|
||||
### docker-compose
|
||||
Docker Compose lets you specify how and where to run your containers, and to manage their environment. The docker-compose.yml file in this folder gets you a fully working instance of StashApp exactly as you would need it to have a reasonable instance for testing / developing on. If you are deploying a live instance for production, a reverse proxy (such as NGINX or Traefik) is recommended, but not required.
|
||||
|
||||
The latest version is always recommended.
|
||||
|
||||
@@ -4,9 +4,13 @@ version: '3.4'
|
||||
services:
|
||||
stash:
|
||||
image: stashapp/stash:latest
|
||||
container_name: stash
|
||||
restart: unless-stopped
|
||||
## the container's port must be the same with the STASH_PORT in the environment section
|
||||
ports:
|
||||
- "9999:9999"
|
||||
## If you intend to use stash's DLNA functionality uncomment the below network mode and comment out the above ports section
|
||||
# network_mode: host
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
@@ -17,12 +21,14 @@ services:
|
||||
- STASH_GENERATED=/generated/
|
||||
- STASH_METADATA=/metadata/
|
||||
- STASH_CACHE=/cache/
|
||||
## Adjust below to change default port (9999)
|
||||
- STASH_PORT=9999
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
## Adjust below paths (the left part) to your liking.
|
||||
## E.g. you can change ./config:/root/.stash to ./stash:/root/.stash
|
||||
|
||||
## Keep configs here.
|
||||
## Keep configs, scrapers, and plugins here.
|
||||
- ./config:/root/.stash
|
||||
## Point this at your collection.
|
||||
- ./data:/data
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
FROM ubuntu:20.04 as prep
|
||||
LABEL MAINTAINER="leopere [at] nixc [dot] us"
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install curl xz-utils && \
|
||||
apt-get autoclean -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# added " to end of stash-linux clause so that it doesn't pick up the arm builds
|
||||
RUN curl -L -o /stash $(curl -s https://api.github.com/repos/stashapp/stash/releases/latest | awk '/browser_download_url/ && /stash-linux/"' | sed -e 's/.*: "\(.*\)"/\1/') && \
|
||||
chmod +x /stash
|
||||
|
||||
RUN curl --http1.1 -o /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
|
||||
tar xf /ffmpeg.tar.xz && \
|
||||
rm ffmpeg.tar.xz && \
|
||||
mv /ffmpeg*/ /ffmpeg/
|
||||
|
||||
FROM ubuntu:20.04 as app
|
||||
RUN apt-get update && apt-get -y install ca-certificates
|
||||
COPY --from=prep /stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
|
||||
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
EXPOSE 9999
|
||||
CMD ["stash"]
|
||||
68
docs/DEVELOPMENT.md
Normal file
68
docs/DEVELOPMENT.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Building from Source
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
* [Go](https://golang.org/dl/)
|
||||
* [GolangCI](https://golangci-lint.run/) - A meta-linter which runs several linters in parallel
|
||||
* To install, follow the [local installation instructions](https://golangci-lint.run/usage/install/#local-installation)
|
||||
* [Yarn](https://yarnpkg.com/en/docs/install) - Yarn package manager
|
||||
* Run `yarn install --frozen-lockfile` in the `stash/ui/v2.5` folder (before running make generate for first time).
|
||||
|
||||
NOTE: You may need to run the `go get` commands outside the project directory to avoid modifying the projects module file.
|
||||
|
||||
## Environment
|
||||
|
||||
### Windows
|
||||
|
||||
1. Download and install [Go for Windows](https://golang.org/dl/)
|
||||
2. Download and install [MingW](https://sourceforge.net/projects/mingw-w64/)
|
||||
3. Search for "advanced system settings" and open the system properties dialog.
|
||||
1. Click the `Environment Variables` button
|
||||
2. Under system variables find the `Path`. Edit and add `C:\Program Files\mingw-w64\*\mingw64\bin` (replace * with the correct path).
|
||||
|
||||
NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
|
||||
|
||||
### macOS
|
||||
|
||||
TODO
|
||||
|
||||
|
||||
## Commands
|
||||
|
||||
* `make generate` - Generate Go and UI GraphQL files
|
||||
* `make build` - Builds the binary (make sure to build the UI as well... see below)
|
||||
* `make docker-build` - Locally builds and tags a complete 'stash/build' docker image
|
||||
* `make pre-ui` - Installs the UI dependencies. Only needs to be run once before building the UI for the first time, or if the dependencies are updated
|
||||
* `make fmt-ui` - Formats the UI source code
|
||||
* `make ui` - Builds the frontend
|
||||
* `make lint` - Run the linter on the backend
|
||||
* `make fmt` - Run `go fmt`
|
||||
* `make it` - Run the unit and integration tests
|
||||
* `make validate` - Run all of the tests and checks required to submit a PR
|
||||
* `make ui-start` - Runs the UI in development mode. Requires a running stash server to connect to. Stash port can be changed from the default of `9999` with environment variable `REACT_APP_PLATFORM_PORT`.
|
||||
|
||||
## Building a release
|
||||
|
||||
1. Run `make generate` to create generated files
|
||||
2. Run `make ui` to compile the frontend
|
||||
3. Run `make build` to build the executable for your current platform
|
||||
|
||||
## 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:
|
||||
|
||||
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashappdev/compiler:latest /bin/bash`
|
||||
|
||||
## Profiling
|
||||
|
||||
Stash can be profiled using the `--cpuprofile <output profile filename>` command line flag.
|
||||
|
||||
The resulting file can then be used with pprof as follows:
|
||||
|
||||
`go tool pprof <path to binary> <path to profile filename>`
|
||||
|
||||
With `graphviz` installed and in the path, a call graph can be generated with:
|
||||
|
||||
`go tool pprof -svg <path to binary> <path to profile filename> > <output svg file>`
|
||||
BIN
docs/readme_assets/demo_image.png
Normal file
BIN
docs/readme_assets/demo_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 621 KiB |
6
docs/readme_assets/docker_logo.svg
Normal file
6
docs/readme_assets/docker_logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="256px" height="185px" viewBox="0 0 256 185" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M250.715745,70.4971666 C244.951102,66.4973277 231.740464,64.997388 221.412146,66.9973071 C220.211179,56.9977092 214.68673,48.2480609 205.078993,40.4983724 L199.554544,36.4985331 L195.711449,42.248302 C190.90758,49.7480006 188.505646,60.2475786 189.226226,70.2471769 C189.46642,73.7470364 190.667387,79.9967847 194.270289,85.496564 C190.90758,87.4964838 183.941971,89.996383 174.814621,89.996383 L1.15476998,89.996383 L0.674383104,91.9963028 C-1.00697093,101.9959 -1.00697093,133.244645 18.6888904,157.243681 C33.5808831,175.492947 55.6786788,184.742575 84.7420842,184.742575 C147.672763,184.742575 194.270289,154.493791 216.127891,99.7459909 C224.774854,99.9959813 243.269748,99.7459909 252.637292,80.996745 C252.877486,80.4967649 253.357872,79.4968046 255.039227,75.7469554 L256,73.7470364 L250.715745,70.4971666 L250.715745,70.4971666 Z M139.986573,0 L113.565295,0 L113.565295,24.9989952 L139.986573,24.9989952 L139.986573,0 L139.986573,0 Z M139.986573,29.9987943 L113.565295,29.9987943 L113.565295,54.9977896 L139.986573,54.9977896 L139.986573,29.9987943 L139.986573,29.9987943 Z M108.761427,29.9987943 L82.3401495,29.9987943 L82.3401495,54.9977896 L108.761427,54.9977896 L108.761427,29.9987943 L108.761427,29.9987943 Z M77.5362814,29.9987943 L51.1150037,29.9987943 L51.1150037,54.9977896 L77.5362814,54.9977896 L77.5362814,29.9987943 L77.5362814,29.9987943 Z M46.311135,59.9975886 L19.8898576,59.9975886 L19.8898576,84.9965839 L46.311135,84.9965839 L46.311135,59.9975886 L46.311135,59.9975886 Z M77.5362814,59.9975886 L51.1150037,59.9975886 L51.1150037,84.9965839 L77.5362814,84.9965839 L77.5362814,59.9975886 L77.5362814,59.9975886 Z M108.761427,59.9975886 L82.3401495,59.9975886 L82.3401495,84.9965839 L108.761427,84.9965839 L108.761427,59.9975886 L108.761427,59.9975886 Z M139.986573,59.9975886 L113.565295,59.9975886 L113.565295,84.9965839 L139.986573,84.9965839 L139.986573,59.9975886 L139.986573,59.9975886 Z M171.211719,59.9975886 L144.790441,59.9975886 L144.790441,84.9965839 L171.211719,84.9965839 L171.211719,59.9975886 L171.211719,59.9975886 Z" fill="#2396ED" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
121
docs/readme_assets/linux_logo.svg
Normal file
121
docs/readme_assets/linux_logo.svg
Normal file
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="256px" height="295px" viewBox="0 0 256 295" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<defs>
|
||||
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="6.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
</filter>
|
||||
<linearGradient x1="48.5477412%" y1="115.276174%" x2="51.0473804%" y2="41.3637237%" id="linearGradient-2">
|
||||
<stop stop-color="#FFEED7" offset="0%"></stop>
|
||||
<stop stop-color="#BDBFC2" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="54.4065463%" y1="2.40410545%" x2="46.1753957%" y2="90.5422349%" id="linearGradient-3">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.8" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="51.859653%" y1="88.2477484%" x2="47.9469396%" y2="9.74782136%" id="linearGradient-4">
|
||||
<stop stop-color="#FFEED7" offset="0%"></stop>
|
||||
<stop stop-color="#BDBFC2" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="49.9251097%" y1="85.4900173%" x2="49.9236843%" y2="13.8109272%" id="linearGradient-5">
|
||||
<stop stop-color="#FFEED7" offset="0%"></stop>
|
||||
<stop stop-color="#BDBFC2" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="53.9014071%" y1="3.10177585%" x2="45.9555354%" y2="93.8949571%" id="linearGradient-6">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="45.5928761%" y1="5.47459052%" x2="54.811359%" y2="93.5235162%" id="linearGradient-7">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="49.9844987%" y1="89.8452442%" x2="49.9844987%" y2="40.6316864%" id="linearGradient-8">
|
||||
<stop stop-color="#FFEED7" offset="0%"></stop>
|
||||
<stop stop-color="#BDBFC2" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="53.5047131%" y1="99.97524%" x2="42.7455968%" y2="23.5451715%" id="linearGradient-9">
|
||||
<stop stop-color="#FFEED7" offset="0%"></stop>
|
||||
<stop stop-color="#BDBFC2" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="49.8413363%" y1="13.2289558%" x2="50.2412612%" y2="94.6729694%" id="linearGradient-10">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.8" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="49.9272298%" y1="37.3270337%" x2="50.7270446%" y2="92.7824735%" id="linearGradient-11">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="49.8755597%" y1="2.29900584%" x2="49.8755597%" y2="81.203617%" id="linearGradient-12">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="49.8334391%" y1="2.27189065%" x2="49.8240398%" y2="71.7989617%" id="linearGradient-13">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="53.4670683%" y1="48.9213861%" x2="38.9488708%" y2="98.0999776%" id="linearGradient-14">
|
||||
<stop stop-color="#FFA63F" offset="0%"></stop>
|
||||
<stop stop-color="#FFFF00" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="52.3731508%" y1="143.008909%" x2="47.57909%" y2="-64.6215389%" id="linearGradient-15">
|
||||
<stop stop-color="#FFEED7" offset="0%"></stop>
|
||||
<stop stop-color="#BDBFC2" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="30.580815%" y1="34.0241079%" x2="65.8867024%" y2="89.175349%" id="linearGradient-16">
|
||||
<stop stop-color="#FFA63F" offset="0%"></stop>
|
||||
<stop stop-color="#FFFF00" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="59.5715091%" y1="-17.2155207%" x2="48.3608522%" y2="66.1184465%" id="linearGradient-17">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="47.7689553%" y1="1.56481301%" x2="51.3733028%" y2="104.312856%" id="linearGradient-18">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="43.5495626%" y1="4.5334861%" x2="57.1143288%" y2="92.8267174%" id="linearGradient-19">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="49.7328042%" y1="17.6085216%" x2="50.5582487%" y2="99.3854667%" id="linearGradient-20">
|
||||
<stop stop-color="#FFA63F" offset="0%"></stop>
|
||||
<stop stop-color="#FFFF00" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50.1697217%" y1="2.89048531%" x2="49.6802359%" y2="94.1704279%" id="linearGradient-21">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill="none">
|
||||
<g transform="translate(10.000000, 0.000000)">
|
||||
<path d="M235.125423,249.358628 C235.125423,266.714271 182.507524,280.855905 117.584567,280.855905 C52.6616093,280.855905 0.0437105058,266.806099 0.0437105058,249.358628 L0.0437105058,249.358628 C0.0437105058,232.002986 52.6616093,217.861352 117.584567,217.861352 C182.507524,217.861352 235.033594,232.002986 235.125423,249.358628 L235.125423,249.358628 L235.125423,249.358628 Z" fill="#000" fill-opacity="0.2" filter="url(#filter-1)"></path>
|
||||
<path d="M53.2125821,215.473804 C41.8258117,199.128278 39.6219206,145.867578 66.160442,113.084699 C79.2919595,97.3819748 82.6896249,86.4543483 83.6997416,71.6699125 C84.434372,54.8652433 71.8538272,4.81855066 119.237485,1.05357012 C167.263944,-2.80323922 164.600909,44.5804184 164.325423,69.6496791 C164.141765,90.7703016 179.844489,102.799874 190.680286,119.329056 C210.607135,149.632558 208.954216,201.791313 186.915306,230.074582 C158.999353,265.428667 135.123866,250.093259 119.237485,251.378862 C89.4849556,253.123609 88.4748389,268.918162 53.2125821,215.473804 L53.2125821,215.473804 Z" fill="#000000"></path>
|
||||
<path d="M169.10052,122.451235 C177.365111,130.073025 198.76122,164.141508 164.876395,185.445788 C152.938652,192.88392 175.528535,221.167189 186.364333,207.484699 C205.556551,182.874582 193.343321,143.571858 181.772893,129.522053 C174.059275,119.604543 162.121532,115.747734 169.10052,122.451235 L169.10052,122.451235 Z" fill="url(#linearGradient-2)"></path>
|
||||
<path d="M166.8048,117.859796 C180.395461,128.879251 205.097407,167.447344 169.008691,192.608434 C157.162777,200.413881 179.477174,225.115827 192.057718,212.535282 C235.676395,168.641119 190.955773,118.227111 175.528535,100.871469 C161.754216,85.719718 149.540987,104.360963 166.8048,117.859796 L166.8048,117.859796 Z" stroke="#000000" stroke-width="0.9773" fill="#000000"></path>
|
||||
<path d="M147.245267,25.0208853 C146.786123,37.60143 132.919975,48.5290565 116.298963,49.5391732 C99.6779518,50.54929 86.638263,40.9990954 87.097407,28.4185507 L87.097407,28.4185507 C87.556551,15.8380059 101.422699,4.91037946 118.043711,3.90026272 C134.664722,2.98197479 147.704411,12.4403405 147.245267,25.0208853 L147.245267,25.0208853 L147.245267,25.0208853 Z" fill="url(#linearGradient-3)"></path>
|
||||
<path d="M107.483399,54.9570721 C107.942543,63.1298347 104.085734,70.0169942 98.7596638,70.2924806 C93.4335938,70.567967 88.7503253,64.2317802 88.2911813,56.0590176 L88.2911813,56.0590176 C87.8320374,47.8862549 91.6888467,40.9990954 97.0149167,40.723609 C102.340987,40.4481226 107.024255,46.7843094 107.483399,54.9570721 L107.483399,54.9570721 L107.483399,54.9570721 Z" fill="url(#linearGradient-4)"></path>
|
||||
<path d="M117.125423,55.5998736 C117.30908,65.0582394 123.461609,72.5882005 130.807913,72.4045429 C138.154216,72.2208853 143.93943,64.4154378 143.755773,54.8652433 L143.755773,54.8652433 C143.572115,45.4068775 137.419586,37.8769164 130.073282,38.060574 C122.726979,38.2442316 116.849936,46.1415079 117.125423,55.5998736 L117.125423,55.5998736 L117.125423,55.5998736 Z" fill="url(#linearGradient-5)"></path>
|
||||
<path d="M123.186123,57.7119359 C123.094294,62.9461771 125.6655,67.1703016 129.063166,67.1703016 C132.369002,67.1703016 135.215695,62.9461771 135.307524,57.8037647 L135.307524,57.8037647 C135.399353,52.5695234 132.828146,48.3453989 129.430481,48.3453989 C126.032816,48.3453989 123.277952,52.5695234 123.186123,57.7119359 L123.186123,57.7119359 L123.186123,57.7119359 Z" fill="#000000"></path>
|
||||
<path d="M101.973672,57.8037647 C102.432816,62.119718 100.779897,65.7928697 98.3923486,66.1601849 C96.0048,66.4356713 93.7090802,63.2216635 93.2499362,58.9057102 L93.2499362,58.9057102 C92.7907922,54.5897569 94.4437105,50.9166051 96.8312591,50.54929 C99.2188078,50.2738036 101.514528,53.4878114 101.973672,57.8037647 L101.973672,57.8037647 L101.973672,57.8037647 Z" fill="#000000"></path>
|
||||
<path d="M124.563555,54.7734145 C124.288068,57.7119359 125.6655,60.0994845 127.593905,60.2831421 C129.52231,60.4667997 131.358886,58.1710798 131.634372,55.3243872 L131.634372,55.3243872 C131.909858,52.3858658 130.532426,49.9983172 128.604022,49.8146596 C126.675617,49.631002 124.839041,51.9267219 124.563555,54.7734145 L124.563555,54.7734145 L124.563555,54.7734145 Z" fill="url(#linearGradient-6)"></path>
|
||||
<path d="M99.9534381,55.5080448 C100.228925,57.8955935 99.2188078,60.0076557 97.7495471,60.1913133 C96.2802864,60.3749709 94.9028545,58.538395 94.6273681,56.0590176 L94.6273681,56.0590176 C94.3518817,53.6714689 95.3619984,51.5594067 96.8312591,51.3757491 C98.3005198,51.1920915 99.6779518,53.1204962 99.9534381,55.5080448 L99.9534381,55.5080448 L99.9534381,55.5080448 Z" fill="url(#linearGradient-7)"></path>
|
||||
<path d="M71.0273681,145.68392 C77.5472125,130.899485 91.4133603,104.911936 91.6888467,84.80143 C91.6888467,68.8232199 139.531648,64.9664106 143.388458,80.9446207 C147.245267,96.9228308 156.979119,120.798317 163.223477,132.368745 C169.467835,143.847344 187.558107,180.487033 168.274061,212.443453 C150.918419,240.726722 98.3005198,263.132948 70.2009089,208.586644 C60.6507144,189.669913 62.3954615,166.25357 71.0273681,145.68392 L71.0273681,145.68392 Z" fill="url(#linearGradient-8)"></path>
|
||||
<path d="M65.1503253,134.664465 C59.5487689,145.224776 47.9783409,172.957072 76.2616093,188.108823 C106.65694,204.270691 106.565111,237.420885 70.0172514,221.626333 C36.5915704,207.39287 51.3760062,149.724387 60.7425432,135.950068 C66.8032436,126.308045 75.986123,114.46213 65.1503253,134.664465 L65.1503253,134.664465 Z" fill="url(#linearGradient-9)"></path>
|
||||
<path d="M69.9254226,122.726722 C61.0180296,137.235671 39.7137494,171.395983 68.2725043,189.210769 C106.65694,212.810769 95.8211424,236.31894 60.7425432,215.106488 C11.3386521,185.537617 54.7736716,125.848901 74.5168623,103.07536 C97.1067455,77.5469553 78.8328156,107.758628 69.9254226,122.726722 L69.9254226,122.726722 Z" stroke="#000000" stroke-width="1.25" fill="#000000"></path>
|
||||
<path d="M156.428146,151.285477 C156.428146,167.447344 140.90908,188.384309 114.27873,188.200652 C86.8219206,188.384309 75.1596638,167.447344 75.1596638,151.285477 C75.1596638,135.123609 93.341765,121.992092 115.747991,121.992092 C138.246045,122.08392 156.428146,135.123609 156.428146,151.285477 L156.428146,151.285477 Z" fill="url(#linearGradient-10)"></path>
|
||||
<path d="M141.919197,100.504154 C141.643711,117.216994 130.716084,121.165632 116.941765,121.165632 C103.167446,121.165632 93.1581074,118.686255 91.9643331,100.504154 C91.9643331,89.1173833 103.167446,82.5057102 116.941765,82.5057102 C130.716084,82.4138814 141.919197,89.0255546 141.919197,100.504154 L141.919197,100.504154 Z" fill="url(#linearGradient-11)"></path>
|
||||
<path d="M58.6304809,126.216216 C67.6297027,112.533726 86.638263,91.504932 62.2118039,129.154737 C42.3767844,160.19287 54.8655004,180.119718 61.293516,185.629446 C79.8429323,202.158628 79.1083019,213.269913 64.5075237,204.546177 C33.1939051,185.904932 39.7137494,154.499485 58.6304809,126.216216 L58.6304809,126.216216 Z" fill="url(#linearGradient-12)"></path>
|
||||
<path d="M188.935539,131.817772 C181.130092,115.747734 156.336318,74.9757491 190.129314,122.359407 C220.89196,165.243453 199.312193,195.087811 195.455384,198.026333 C191.598574,200.964854 178.650714,206.933726 182.415695,196.557072 C186.272504,186.180418 205.372893,166.529056 188.935539,131.817772 L188.935539,131.817772 Z" fill="url(#linearGradient-13)"></path>
|
||||
<path d="M51.8351502,258.541508 C31.2655004,247.613881 1.42114241,260.65357 12.2569401,231.084699 C14.4608311,224.381197 9.0429323,214.280029 12.5324265,207.760185 C16.6647222,199.77108 25.5721152,201.515827 30.8981852,196.189757 C36.1324265,190.680029 39.438263,181.129835 49.263944,182.599095 C58.9977961,184.068356 65.5176405,196.006099 72.3129712,210.698706 C77.3635549,221.167189 95.1783409,235.951625 93.9845665,247.70571 C92.5153058,265.704154 72.0374848,269.101819 51.8351502,258.541508 L51.8351502,258.541508 Z" stroke="#E68C3F" stroke-width="6.25" fill="url(#linearGradient-14)"></path>
|
||||
<path d="M201.607913,189.11894 C198.485734,194.995983 185.446045,204.454348 176.72231,201.974971 C167.906746,199.587422 163.866279,186.180418 165.611026,175.987422 C167.263944,164.600652 176.72231,163.95785 188.660053,169.651235 C201.516084,175.987422 205.372893,181.313492 201.607913,189.11894 L201.607913,189.11894 Z" fill="url(#linearGradient-15)"></path>
|
||||
<path d="M194.445267,253.490924 C209.505189,235.216994 243.022699,238.981975 220.432816,213.912714 C215.657718,208.494815 217.126979,196.924387 211.249936,191.965632 C204.362777,185.904932 196.740987,190.863687 189.761998,187.741508 C182.78301,184.343842 175.436707,177.823998 166.896629,182.415438 C158.356551,187.098706 157.438263,199.220107 156.611804,215.198317 C155.877174,226.676916 145.408691,245.869134 151.010247,256.429446 C159.091181,272.774971 180.119975,270.57108 194.445267,253.490924 L194.445267,253.490924 Z" stroke="#E68C3F" stroke-width="6.2507" fill="url(#linearGradient-16)"></path>
|
||||
<path d="M187.925423,229.064465 C211.249936,194.628667 193.894294,194.904154 188.017251,192.241119 C182.140209,189.486255 175.987679,184.068356 169.10052,187.833337 C162.21336,191.690146 161.846045,201.607656 161.662388,214.647344 C161.386901,224.013881 153.581454,239.716605 158.264722,248.440341 C163.958107,258.633337 177.732426,243.848901 187.925423,229.064465 L187.925423,229.064465 Z" fill="url(#linearGradient-17)"></path>
|
||||
<path d="M47.0600529,234.02322 C12.1651113,211.433337 28.5106366,203.719718 33.7448778,200.138395 C40.0810646,195.546955 40.1728934,186.731391 47.9783409,187.55785 C55.7837883,188.384309 60.375228,198.026333 65.6094693,209.964076 C69.4662786,218.504154 82.8732825,229.890924 81.8631658,239.716605 C80.5775626,251.287033 62.1199751,243.665243 47.0600529,234.02322 L47.0600529,234.02322 Z" fill="url(#linearGradient-18)"></path>
|
||||
<path d="M199.587679,188.843453 C196.832816,193.618551 185.629703,201.148512 178.19157,199.128278 C170.569781,197.199874 167.080286,186.455905 168.641376,178.374971 C170.018808,169.192092 178.19157,168.732948 188.476395,173.324387 C199.404022,178.283142 202.801687,182.507267 199.587679,188.843453 L199.587679,188.843453 Z" fill="#000000"></path>
|
||||
<path d="M192.057718,186.180418 C190.312971,189.486255 182.966668,194.720496 177.824255,193.343064 C172.681843,191.965632 170.110637,184.5275 170.937096,178.925944 C171.671726,172.589757 177.181454,172.222442 184.160442,175.344621 C191.690403,178.834115 194.077952,181.772636 192.057718,186.180418 L192.057718,186.180418 Z" fill="url(#linearGradient-19)"></path>
|
||||
<path d="M97.1067455,66.3438425 C100.779897,62.9461771 109.68729,52.5695234 126.583788,63.4053211 C129.705967,65.4255546 132.277174,65.6092121 138.246045,68.1804184 C150.275617,73.1391732 144.582232,85.0769164 131.726201,89.1173833 C126.216473,90.8621304 121.257718,97.5656324 111.340209,96.9228308 C102.800131,96.4636868 100.59624,90.8621304 95.3619984,87.8317802 C86.0872903,82.597539 84.7098584,75.5267219 89.760442,71.7617413 C94.8110257,67.9967608 96.7394304,66.6193289 97.1067455,66.3438425 L97.1067455,66.3438425 Z" stroke="#E68C3F" stroke-width="3.75" fill="url(#linearGradient-20)"></path>
|
||||
<path d="M138.429703,75.9858658 C133.379119,76.2613522 122.451493,87.1889787 110.972893,87.1889787 C99.4942942,87.1889787 92.6071346,76.5368386 90.8623875,76.5368386" stroke="#E68C3F" stroke-width="2.5"></path>
|
||||
<path d="M102.800131,65.4255546 C104.636707,63.7726363 110.421921,59.2730254 118.043711,63.8644651 C119.696629,64.782753 121.349547,65.7928697 123.737096,67.1703016 C128.604022,70.0169942 126.216473,74.14929 120.33943,76.7204962 C117.676395,77.8224417 113.268613,80.2099904 109.962777,80.0263328 C106.289625,79.6590176 103.810247,77.2714689 101.422699,75.7103795 C96.9230879,72.7718581 97.1985743,70.2924806 99.3106366,68.364076 C100.871726,66.8948153 102.616473,65.5173833 102.800131,65.4255546 L102.800131,65.4255546 Z" fill="url(#linearGradient-21)"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 18 KiB |
2
docs/readme_assets/mac_logo.svg
Normal file
2
docs/readme_assets/mac_logo.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 42 42" xmlns="http://www.w3.org/2000/svg"><path d="m23.091 14.018v-0.342l-1.063 0.073c-0.301 0.019-0.527 0.083-0.679 0.191-0.152 0.109-0.228 0.26-0.228 0.453 0 0.188 0.075 0.338 0.226 0.449 0.15 0.112 0.352 0.167 0.604 0.167 0.161 0 0.312-0.025 0.451-0.074s0.261-0.118 0.363-0.206c0.102-0.087 0.182-0.191 0.239-0.312 0.058-0.121 0.087-0.254 0.087-0.399zm-2.091-13.768c-11.579 0-20.75 9.171-20.75 20.75 0 11.58 9.171 20.75 20.75 20.75s20.75-9.17 20.75-20.75c0-11.579-9.17-20.75-20.75-20.75zm4.028 12.299c0.098-0.275 0.236-0.511 0.415-0.707s0.394-0.347 0.646-0.453 0.533-0.159 0.842-0.159c0.279 0 0.531 0.042 0.755 0.125 0.225 0.083 0.417 0.195 0.578 0.336s0.289 0.305 0.383 0.493 0.15 0.387 0.169 0.596h-0.833c-0.021-0.115-0.059-0.223-0.113-0.322s-0.125-0.185-0.213-0.258c-0.089-0.073-0.193-0.13-0.312-0.171-0.12-0.042-0.254-0.062-0.405-0.062-0.177 0-0.338 0.036-0.481 0.107-0.144 0.071-0.267 0.172-0.369 0.302s-0.181 0.289-0.237 0.475c-0.057 0.187-0.085 0.394-0.085 0.622 0 0.236 0.028 0.448 0.085 0.634 0.056 0.187 0.136 0.344 0.24 0.473 0.103 0.129 0.228 0.228 0.373 0.296s0.305 0.103 0.479 0.103c0.285 0 0.517-0.067 0.697-0.201s0.296-0.33 0.35-0.588h0.834c-0.024 0.228-0.087 0.436-0.189 0.624s-0.234 0.348-0.396 0.481c-0.163 0.133-0.354 0.236-0.574 0.308s-0.462 0.109-0.725 0.109c-0.312 0-0.593-0.052-0.846-0.155-0.252-0.103-0.469-0.252-0.649-0.445s-0.319-0.428-0.417-0.705-0.147-0.588-0.147-0.935c-2e-3 -0.339 0.047-0.647 0.145-0.923zm-11.853-1.262h0.834v0.741h0.016c0.051-0.123 0.118-0.234 0.2-0.33 0.082-0.097 0.176-0.179 0.284-0.248 0.107-0.069 0.226-0.121 0.354-0.157 0.129-0.036 0.265-0.054 0.407-0.054 0.306 0 0.565 0.073 0.775 0.219 0.211 0.146 0.361 0.356 0.449 0.63h0.021c0.056-0.132 0.13-0.25 0.221-0.354s0.196-0.194 0.314-0.268 0.248-0.13 0.389-0.169 0.289-0.058 0.445-0.058c0.215 0 0.41 0.034 0.586 0.103s0.326 0.165 0.451 0.29 0.221 0.277 0.288 0.455 0.101 0.376 0.101 0.594v2.981h-0.87v-2.772c0-0.287-0.074-0.51-0.222-0.667-0.147-0.157-0.358-0.236-0.632-0.236-0.134 0-0.257 0.024-0.369 0.071-0.111 0.047-0.208 0.113-0.288 0.198-0.081 0.084-0.144 0.186-0.189 0.304-0.046 0.118-0.069 0.247-0.069 0.387v2.715h-0.858v-2.844c0-0.126-0.02-0.24-0.059-0.342s-0.094-0.189-0.167-0.262c-0.072-0.073-0.161-0.128-0.264-0.167-0.104-0.039-0.22-0.059-0.349-0.059-0.134 0-0.258 0.025-0.373 0.075-0.114 0.05-0.212 0.119-0.294 0.207-0.082 0.089-0.146 0.193-0.191 0.314-0.044 0.12-0.116 0.252-0.116 0.394v2.683h-0.825v-4.374zm1.893 20.939c-3.825 0-6.224-2.658-6.224-6.9s2.399-6.909 6.224-6.909 6.215 2.667 6.215 6.909c0 4.241-2.39 6.9-6.215 6.9zm7.082-16.575c-0.141 0.036-0.285 0.054-0.433 0.054-0.218 0-0.417-0.031-0.598-0.093-0.182-0.062-0.337-0.149-0.467-0.262s-0.232-0.249-0.304-0.409c-0.073-0.16-0.109-0.338-0.109-0.534 0-0.384 0.143-0.684 0.429-0.9s0.7-0.342 1.243-0.377l1.18-0.068v-0.338c0-0.252-0.08-0.445-0.24-0.576s-0.386-0.197-0.679-0.197c-0.118 0-0.229 0.015-0.331 0.044-0.102 0.03-0.192 0.072-0.27 0.127s-0.143 0.121-0.193 0.198c-0.051 0.076-0.086 0.162-0.105 0.256h-0.818c5e-3 -0.193 0.053-0.372 0.143-0.536s0.212-0.306 0.367-0.427 0.336-0.215 0.546-0.282 0.438-0.101 0.685-0.101c0.266 0 0.507 0.033 0.723 0.101s0.401 0.163 0.554 0.288 0.271 0.275 0.354 0.451 0.125 0.373 0.125 0.59v3.001h-0.833v-0.729h-0.021c-0.062 0.118-0.14 0.225-0.235 0.32-0.096 0.095-0.203 0.177-0.322 0.244-0.12 0.067-0.25 0.119-0.391 0.155zm5.503 16.575c-2.917 0-4.9-1.528-5.038-3.927h1.899c0.148 1.371 1.473 2.279 3.288 2.279 1.741 0 2.992-0.908 2.992-2.149 0-1.074-0.76-1.723-2.519-2.167l-1.714-0.426c-2.464-0.611-3.584-1.732-3.584-3.575 0-2.269 1.982-3.844 4.807-3.844 2.76 0 4.686 1.584 4.76 3.862h-1.88c-0.13-1.371-1.25-2.214-2.918-2.214-1.658 0-2.806 0.852-2.806 2.084 0 0.972 0.722 1.547 2.482 1.991l1.445 0.361c2.751 0.667 3.881 1.751 3.881 3.696-1e-3 2.482-1.964 4.029-5.095 4.029zm-12.585-12.106c-2.621 0-4.26 2.01-4.26 5.205 0 3.186 1.639 5.196 4.26 5.196 2.612 0 4.26-2.01 4.26-5.196 1e-3 -3.195-1.648-5.205-4.26-5.205z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
6
docs/readme_assets/windows_logo.svg
Normal file
6
docs/readme_assets/windows_logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="256px" height="257px" viewBox="0 0 256 257" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M0,36.3573818 L104.619084,22.1093454 L104.664817,123.02292 L0.0955693151,123.618411 L0,36.3573818 Z M104.569248,134.650129 L104.650452,235.651651 L0.0812046021,221.274919 L0.0753414539,133.972642 L104.569248,134.650129 Z M117.25153,20.2454506 L255.967753,6.21724894e-15 L255.967753,121.739477 L117.25153,122.840723 L117.25153,20.2454506 Z M256,135.599959 L255.96746,256.791232 L117.251237,237.213007 L117.056874,135.373055 L256,135.599959 Z" fill="#00ADEF"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 734 B |
84
go.mod
84
go.mod
@@ -8,46 +8,92 @@ require (
|
||||
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/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.0
|
||||
github.com/fvbommel/sortorder v1.0.2
|
||||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/gobuffalo/logger v1.0.4 // indirect
|
||||
github.com/gobuffalo/packr/v2 v2.8.1
|
||||
github.com/golang-migrate/migrate/v4 v4.3.1
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0
|
||||
github.com/golang-migrate/migrate/v4 v4.15.0-beta.1
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/h2non/filetype v1.0.8
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/json-iterator/go v1.1.9
|
||||
github.com/karrick/godirwalk v1.16.1 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.1
|
||||
github.com/json-iterator/go v1.1.11
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
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/rs/cors v1.6.0
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/afero v1.2.0 // indirect
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/tidwall/gjson v1.8.1
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.9.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tidwall/gjson v1.9.3
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.0.1
|
||||
github.com/vektra/mockery/v2 v2.2.1
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
|
||||
golang.org/x/tools v0.0.0-20200915031644-64986481280e // indirect
|
||||
golang.org/x/text v0.3.6
|
||||
golang.org/x/tools v0.1.5 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require github.com/vektah/gqlparser/v2 v2.0.1
|
||||
|
||||
require (
|
||||
github.com/agnivade/levenshtein v1.1.0 // indirect
|
||||
github.com/antchfx/xpath v1.1.6 // indirect
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // 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/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/zerolog v1.18.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/cobra v1.0.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/tidwall/match v1.1.1 // indirect
|
||||
github.com/urfave/cli/v2 v2.1.1 // indirect
|
||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/ini.v1 v1.63.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
||||
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
|
||||
|
||||
go 1.13
|
||||
go 1.17
|
||||
|
||||
16
gqlgen.yml
16
gqlgen.yml
@@ -34,24 +34,8 @@ models:
|
||||
model: github.com/stashapp/stash/pkg/models.Movie
|
||||
Tag:
|
||||
model: github.com/stashapp/stash/pkg/models.Tag
|
||||
ScrapedPerformer:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedPerformer
|
||||
ScrapedScene:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedScene
|
||||
ScrapedScenePerformer:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedScenePerformer
|
||||
ScrapedSceneStudio:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedSceneStudio
|
||||
ScrapedSceneMovie:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedSceneMovie
|
||||
ScrapedSceneTag:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedSceneTag
|
||||
SceneFileType:
|
||||
model: github.com/stashapp/stash/pkg/models.SceneFileType
|
||||
ScrapedMovie:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedMovie
|
||||
ScrapedMovieStudio:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedMovieStudio
|
||||
SavedFilter:
|
||||
model: github.com/stashapp/stash/pkg/models.SavedFilter
|
||||
StashID:
|
||||
|
||||
@@ -6,6 +6,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
}
|
||||
databasePath
|
||||
generatedPath
|
||||
metadataPath
|
||||
cachePath
|
||||
calculateMD5
|
||||
videoFileNamingAlgorithm
|
||||
@@ -18,10 +19,12 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
previewPreset
|
||||
maxTranscodeSize
|
||||
maxStreamingTranscodeSize
|
||||
writeImageThumbnails
|
||||
apiKey
|
||||
username
|
||||
password
|
||||
maxSessionAge
|
||||
trustedProxies
|
||||
logFile
|
||||
logOut
|
||||
logLevel
|
||||
@@ -49,12 +52,20 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
wallShowTitle
|
||||
wallPlayback
|
||||
maximumLoopDuration
|
||||
noBrowser
|
||||
autostartVideo
|
||||
autostartVideoOnPlaySelected
|
||||
continuePlaylistDefault
|
||||
showStudioAsText
|
||||
css
|
||||
cssEnabled
|
||||
language
|
||||
slideshowDelay
|
||||
disabledDropdownCreate {
|
||||
performer
|
||||
tag
|
||||
studio
|
||||
}
|
||||
handyKey
|
||||
funscriptOffset
|
||||
}
|
||||
@@ -73,6 +84,46 @@ fragment ConfigScrapingData on ConfigScrapingResult {
|
||||
excludeTagPatterns
|
||||
}
|
||||
|
||||
fragment IdentifyFieldOptionsData on IdentifyFieldOptions {
|
||||
field
|
||||
strategy
|
||||
createMissing
|
||||
}
|
||||
|
||||
fragment IdentifyMetadataOptionsData on IdentifyMetadataOptions {
|
||||
fieldOptions {
|
||||
...IdentifyFieldOptionsData
|
||||
}
|
||||
setCoverImage
|
||||
setOrganized
|
||||
includeMalePerformers
|
||||
}
|
||||
|
||||
fragment ScraperSourceData on ScraperSource {
|
||||
stash_box_index
|
||||
stash_box_endpoint
|
||||
scraper_id
|
||||
}
|
||||
|
||||
fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
|
||||
identify {
|
||||
sources {
|
||||
source {
|
||||
...ScraperSourceData
|
||||
}
|
||||
options {
|
||||
...IdentifyMetadataOptionsData
|
||||
}
|
||||
}
|
||||
options {
|
||||
...IdentifyMetadataOptionsData
|
||||
}
|
||||
}
|
||||
|
||||
deleteFile
|
||||
deleteGenerated
|
||||
}
|
||||
|
||||
fragment ConfigData on ConfigResult {
|
||||
general {
|
||||
...ConfigGeneralData
|
||||
@@ -86,4 +137,7 @@ fragment ConfigData on ConfigResult {
|
||||
scraping {
|
||||
...ConfigScrapingData
|
||||
}
|
||||
defaults {
|
||||
...ConfigDefaultSettingsData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ fragment SlimMovieData on Movie {
|
||||
id
|
||||
name
|
||||
front_image_path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,10 @@ fragment MovieData on Movie {
|
||||
front_image_path
|
||||
back_image_path
|
||||
scene_count
|
||||
|
||||
scenes {
|
||||
id
|
||||
title
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ fragment PerformerData on Performer {
|
||||
scene_count
|
||||
image_count
|
||||
gallery_count
|
||||
movie_count
|
||||
|
||||
tags {
|
||||
...SlimTagData
|
||||
|
||||
@@ -4,6 +4,7 @@ fragment SceneMarkerData on SceneMarker {
|
||||
seconds
|
||||
stream
|
||||
preview
|
||||
screenshot
|
||||
|
||||
scene {
|
||||
id
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
fragment ScrapedPerformerData on ScrapedPerformer {
|
||||
stored_id
|
||||
name
|
||||
gender
|
||||
url
|
||||
@@ -18,7 +19,7 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
||||
tags {
|
||||
...ScrapedSceneTagData
|
||||
}
|
||||
image
|
||||
images
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
@@ -26,7 +27,7 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
||||
remote_site_id
|
||||
}
|
||||
|
||||
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||
fragment ScrapedScenePerformerData on ScrapedPerformer {
|
||||
stored_id
|
||||
name
|
||||
gender
|
||||
@@ -55,8 +56,8 @@ fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||
weight
|
||||
}
|
||||
|
||||
fragment ScrapedMovieStudioData on ScrapedMovieStudio {
|
||||
id
|
||||
fragment ScrapedMovieStudioData on ScrapedStudio {
|
||||
stored_id
|
||||
name
|
||||
url
|
||||
}
|
||||
@@ -78,7 +79,7 @@ fragment ScrapedMovieData on ScrapedMovie {
|
||||
}
|
||||
}
|
||||
|
||||
fragment ScrapedSceneMovieData on ScrapedSceneMovie {
|
||||
fragment ScrapedSceneMovieData on ScrapedMovie {
|
||||
stored_id
|
||||
name
|
||||
aliases
|
||||
@@ -90,14 +91,14 @@ fragment ScrapedSceneMovieData on ScrapedSceneMovie {
|
||||
synopsis
|
||||
}
|
||||
|
||||
fragment ScrapedSceneStudioData on ScrapedSceneStudio {
|
||||
fragment ScrapedSceneStudioData on ScrapedStudio {
|
||||
stored_id
|
||||
name
|
||||
url
|
||||
remote_site_id
|
||||
}
|
||||
|
||||
fragment ScrapedSceneTagData on ScrapedSceneTag {
|
||||
fragment ScrapedSceneTagData on ScrapedTag {
|
||||
stored_id
|
||||
name
|
||||
}
|
||||
@@ -108,6 +109,7 @@ fragment ScrapedSceneData on ScrapedScene {
|
||||
url
|
||||
date
|
||||
image
|
||||
remote_site_id
|
||||
|
||||
file {
|
||||
size
|
||||
@@ -135,6 +137,12 @@ fragment ScrapedSceneData on ScrapedScene {
|
||||
movies {
|
||||
...ScrapedSceneMovieData
|
||||
}
|
||||
|
||||
fingerprints {
|
||||
hash
|
||||
algorithm
|
||||
duration
|
||||
}
|
||||
}
|
||||
|
||||
fragment ScrapedGalleryData on ScrapedGallery {
|
||||
|
||||
@@ -11,4 +11,5 @@ fragment SlimStudioData on Studio {
|
||||
}
|
||||
details
|
||||
rating
|
||||
aliases
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ fragment StudioData on Studio {
|
||||
scene_count
|
||||
image_count
|
||||
gallery_count
|
||||
movie_count
|
||||
stash_ids {
|
||||
stash_id
|
||||
endpoint
|
||||
}
|
||||
details
|
||||
rating
|
||||
aliases
|
||||
}
|
||||
|
||||
@@ -8,4 +8,12 @@ fragment TagData on Tag {
|
||||
image_count
|
||||
gallery_count
|
||||
performer_count
|
||||
|
||||
parents {
|
||||
...SlimTagData
|
||||
}
|
||||
|
||||
children {
|
||||
...SlimTagData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ mutation ConfigureScraping($input: ConfigScrapingInput!) {
|
||||
}
|
||||
}
|
||||
|
||||
mutation ConfigureDefaults($input: ConfigDefaultSettingsInput!) {
|
||||
configureDefaults(input: $input) {
|
||||
...ConfigDefaultSettingsData
|
||||
}
|
||||
}
|
||||
|
||||
mutation GenerateAPIKey($input: GenerateAPIKeyInput!) {
|
||||
generateAPIKey(input: $input)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ mutation MetadataAutoTag($input: AutoTagMetadataInput!) {
|
||||
metadataAutoTag(input: $input)
|
||||
}
|
||||
|
||||
mutation MetadataIdentify($input: IdentifyMetadataInput!) {
|
||||
metadataIdentify(input: $input)
|
||||
}
|
||||
|
||||
mutation MetadataClean($input: CleanMetadataInput!) {
|
||||
metadataClean(input: $input)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
query FindImages($filter: FindFilterType, $image_filter: ImageFilterType, $image_ids: [Int!]) {
|
||||
findImages(filter: $filter, image_filter: $image_filter, image_ids: $image_ids) {
|
||||
count
|
||||
megapixels
|
||||
filesize
|
||||
images {
|
||||
...SlimImageData
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene_ids: [Int!]) {
|
||||
findScenes(filter: $filter, scene_filter: $scene_filter, scene_ids: $scene_ids) {
|
||||
count
|
||||
filesize
|
||||
duration
|
||||
scenes {
|
||||
...SlimSceneData
|
||||
}
|
||||
@@ -10,6 +12,8 @@ query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene
|
||||
query FindScenesByPathRegex($filter: FindFilterType) {
|
||||
findScenesByPathRegex(filter: $filter) {
|
||||
count
|
||||
filesize
|
||||
duration
|
||||
scenes {
|
||||
...SlimSceneData
|
||||
}
|
||||
|
||||
@@ -42,14 +42,14 @@ query ListMovieScrapers {
|
||||
}
|
||||
}
|
||||
|
||||
query ScrapePerformerList($scraper_id: ID!, $query: String!) {
|
||||
scrapePerformerList(scraper_id: $scraper_id, query: $query) {
|
||||
query ScrapeSinglePerformer($source: ScraperSourceInput!, $input: ScrapeSinglePerformerInput!) {
|
||||
scrapeSinglePerformer(source: $source, input: $input) {
|
||||
...ScrapedPerformerData
|
||||
}
|
||||
}
|
||||
|
||||
query ScrapePerformer($scraper_id: ID!, $scraped_performer: ScrapedPerformerInput!) {
|
||||
scrapePerformer(scraper_id: $scraper_id, scraped_performer: $scraped_performer) {
|
||||
query ScrapeMultiPerformers($source: ScraperSourceInput!, $input: ScrapeMultiPerformersInput!) {
|
||||
scrapeMultiPerformers(source: $source, input: $input) {
|
||||
...ScrapedPerformerData
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,14 @@ query ScrapePerformerURL($url: String!) {
|
||||
}
|
||||
}
|
||||
|
||||
query ScrapeScene($scraper_id: ID!, $scene: SceneUpdateInput!) {
|
||||
scrapeScene(scraper_id: $scraper_id, scene: $scene) {
|
||||
query ScrapeSingleScene($source: ScraperSourceInput!, $input: ScrapeSingleSceneInput!) {
|
||||
scrapeSingleScene(source: $source, input: $input) {
|
||||
...ScrapedSceneData
|
||||
}
|
||||
}
|
||||
|
||||
query ScrapeMultiScenes($source: ScraperSourceInput!, $input: ScrapeMultiScenesInput!) {
|
||||
scrapeMultiScenes(source: $source, input: $input) {
|
||||
...ScrapedSceneData
|
||||
}
|
||||
}
|
||||
@@ -72,8 +78,8 @@ query ScrapeSceneURL($url: String!) {
|
||||
}
|
||||
}
|
||||
|
||||
query ScrapeGallery($scraper_id: ID!, $gallery: GalleryUpdateInput!) {
|
||||
scrapeGallery(scraper_id: $scraper_id, gallery: $gallery) {
|
||||
query ScrapeSingleGallery($source: ScraperSourceInput!, $input: ScrapeSingleGalleryInput!) {
|
||||
scrapeSingleGallery(source: $source, input: $input) {
|
||||
...ScrapedGalleryData
|
||||
}
|
||||
}
|
||||
@@ -89,15 +95,3 @@ query ScrapeMovieURL($url: String!) {
|
||||
...ScrapedMovieData
|
||||
}
|
||||
}
|
||||
|
||||
query QueryStashBoxScene($input: StashBoxSceneQueryInput!) {
|
||||
queryStashBoxScene(input: $input) {
|
||||
...ScrapedStashBoxSceneData
|
||||
}
|
||||
}
|
||||
|
||||
query QueryStashBoxPerformer($input: StashBoxPerformerQueryInput!) {
|
||||
queryStashBoxPerformer(input: $input) {
|
||||
...ScrapedStashBoxPerformerData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ type Query {
|
||||
"""Find a scene by ID or Checksum"""
|
||||
findScene(id: ID, checksum: String): Scene
|
||||
findSceneByHash(input: SceneHashInput!): Scene
|
||||
|
||||
|
||||
"""A function which queries Scene objects"""
|
||||
findScenes(scene_filter: SceneFilterType, scene_ids: [Int!], filter: FindFilterType): FindScenesResultType!
|
||||
|
||||
@@ -25,7 +25,7 @@ type Query {
|
||||
findSceneMarkers(scene_marker_filter: SceneMarkerFilterType filter: FindFilterType): FindSceneMarkersResultType!
|
||||
|
||||
findImage(id: ID, checksum: String): Image
|
||||
|
||||
|
||||
"""A function which queries Scene objects"""
|
||||
findImages(image_filter: ImageFilterType, image_ids: [Int!], filter: FindFilterType): FindImagesResultType!
|
||||
|
||||
@@ -72,31 +72,50 @@ type Query {
|
||||
listGalleryScrapers: [Scraper!]!
|
||||
listMovieScrapers: [Scraper!]!
|
||||
|
||||
"""Scrape a list of performers based on name"""
|
||||
scrapePerformerList(scraper_id: ID!, query: String!): [ScrapedPerformer!]!
|
||||
"""Scrapes a complete performer record based on a scrapePerformerList result"""
|
||||
scrapePerformer(scraper_id: ID!, scraped_performer: ScrapedPerformerInput!): ScrapedPerformer
|
||||
"""Scrape for a single scene"""
|
||||
scrapeSingleScene(source: ScraperSourceInput!, input: ScrapeSingleSceneInput!): [ScrapedScene!]!
|
||||
"""Scrape for multiple scenes"""
|
||||
scrapeMultiScenes(source: ScraperSourceInput!, input: ScrapeMultiScenesInput!): [[ScrapedScene!]!]!
|
||||
|
||||
"""Scrape for a single performer"""
|
||||
scrapeSinglePerformer(source: ScraperSourceInput!, input: ScrapeSinglePerformerInput!): [ScrapedPerformer!]!
|
||||
"""Scrape for multiple performers"""
|
||||
scrapeMultiPerformers(source: ScraperSourceInput!, input: ScrapeMultiPerformersInput!): [[ScrapedPerformer!]!]!
|
||||
|
||||
"""Scrape for a single gallery"""
|
||||
scrapeSingleGallery(source: ScraperSourceInput!, input: ScrapeSingleGalleryInput!): [ScrapedGallery!]!
|
||||
|
||||
"""Scrape for a single movie"""
|
||||
scrapeSingleMovie(source: ScraperSourceInput!, input: ScrapeSingleMovieInput!): [ScrapedMovie!]!
|
||||
|
||||
"""Scrapes a complete performer record based on a URL"""
|
||||
scrapePerformerURL(url: String!): ScrapedPerformer
|
||||
"""Scrapes a complete scene record based on an existing scene"""
|
||||
scrapeScene(scraper_id: ID!, scene: SceneUpdateInput!): ScrapedScene
|
||||
"""Scrapes a complete performer record based on a URL"""
|
||||
scrapeSceneURL(url: String!): ScrapedScene
|
||||
"""Scrapes a complete gallery record based on an existing gallery"""
|
||||
scrapeGallery(scraper_id: ID!, gallery: GalleryUpdateInput!): ScrapedGallery
|
||||
"""Scrapes a complete gallery record based on a URL"""
|
||||
scrapeGalleryURL(url: String!): ScrapedGallery
|
||||
"""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 performer using Freeones"""
|
||||
scrapeFreeones(performer_name: String!): ScrapedPerformer
|
||||
scrapeFreeones(performer_name: String!): ScrapedPerformer @deprecated(reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones")
|
||||
"""Scrape a list of performers from a query"""
|
||||
scrapeFreeonesPerformerList(query: String!): [String!]!
|
||||
scrapeFreeonesPerformerList(query: String!): [String!]! @deprecated(reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones")
|
||||
|
||||
"""Query StashBox for scenes"""
|
||||
queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]!
|
||||
queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]!
|
||||
queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]! @deprecated(reason: "use scrapeSingleScene or scrapeMultiScenes")
|
||||
"""Query StashBox for performers"""
|
||||
queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]! @deprecated(reason: "use scrapeSinglePerformer or scrapeMultiPerformers")
|
||||
# === end deprecated methods ===
|
||||
|
||||
# Plugins
|
||||
"""List loaded plugins"""
|
||||
@@ -108,7 +127,12 @@ type Query {
|
||||
"""Returns the current, complete configuration"""
|
||||
configuration: ConfigResult!
|
||||
"""Returns an array of paths for the given path"""
|
||||
directory(path: String): Directory!
|
||||
directory(
|
||||
"The directory path to list"
|
||||
path: String,
|
||||
"Desired collation locale. Determines the order of the directory result. eg. 'en-US', 'pt-BR', ..."
|
||||
locale: String = "en"
|
||||
): Directory!
|
||||
|
||||
# System status
|
||||
systemStatus: SystemStatus!
|
||||
@@ -130,7 +154,7 @@ type Query {
|
||||
|
||||
# Version
|
||||
version: Version!
|
||||
|
||||
|
||||
# LatestVersion
|
||||
latestversion: ShortVersion!
|
||||
}
|
||||
@@ -213,6 +237,7 @@ type Mutation {
|
||||
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
|
||||
configureDLNA(input: ConfigDLNAInput!): ConfigDLNAResult!
|
||||
configureScraping(input: ConfigScrapingInput!): ConfigScrapingResult!
|
||||
configureDefaults(input: ConfigDefaultSettingsInput!): ConfigDefaultSettingsResult!
|
||||
|
||||
"""Generate and set (or clear) API key"""
|
||||
generateAPIKey(input: GenerateAPIKeyInput!): String!
|
||||
@@ -235,6 +260,8 @@ type Mutation {
|
||||
metadataAutoTag(input: AutoTagMetadataInput!): ID!
|
||||
"""Clean metadata. Returns the job ID"""
|
||||
metadataClean(input: CleanMetadataInput!): ID!
|
||||
"""Identifies scenes using scrapers. Returns the job ID"""
|
||||
metadataIdentify(input: IdentifyMetadataInput!): ID!
|
||||
"""Migrate generated files for the current hash naming"""
|
||||
migrateHashNaming: ID!
|
||||
|
||||
@@ -256,7 +283,7 @@ type Mutation {
|
||||
|
||||
"""Run batch performer tag task. Returns the job ID."""
|
||||
stashBoxBatchPerformerTag(input: StashBoxBatchPerformerTagInput!): String!
|
||||
|
||||
|
||||
"""Enables DLNA for an optional duration. Has no effect if DLNA is enabled by default"""
|
||||
enableDLNA(input: EnableDLNAInput!): Boolean!
|
||||
"""Disables DLNA for an optional duration. Has no effect if DLNA is disabled by default"""
|
||||
|
||||
@@ -39,6 +39,8 @@ input ConfigGeneralInput {
|
||||
databasePath: String
|
||||
"""Path to generated files"""
|
||||
generatedPath: String
|
||||
"""Path to import/export files"""
|
||||
metadataPath: String
|
||||
"""Path to cache"""
|
||||
cachePath: String
|
||||
"""Whether to calculate MD5 checksums for scene video files"""
|
||||
@@ -63,12 +65,16 @@ input ConfigGeneralInput {
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"""Max streaming transcode size"""
|
||||
maxStreamingTranscodeSize: StreamingResolutionEnum
|
||||
"""Write image thumbnails to disk when generating on the fly"""
|
||||
writeImageThumbnails: Boolean
|
||||
"""Username"""
|
||||
username: String
|
||||
"""Password"""
|
||||
password: String
|
||||
"""Maximum session cookie age"""
|
||||
maxSessionAge: Int
|
||||
"""Comma separated list of proxies to allow traffic from"""
|
||||
trustedProxies: [String!]
|
||||
"""Name of the log file"""
|
||||
logFile: String
|
||||
"""Whether to also output to stderr"""
|
||||
@@ -108,6 +114,8 @@ type ConfigGeneralResult {
|
||||
databasePath: String!
|
||||
"""Path to generated files"""
|
||||
generatedPath: String!
|
||||
"""Path to import/export files"""
|
||||
metadataPath: String!
|
||||
"""Path to the config file used"""
|
||||
configFilePath: String!
|
||||
"""Path to scrapers"""
|
||||
@@ -136,6 +144,8 @@ type ConfigGeneralResult {
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"""Max streaming transcode size"""
|
||||
maxStreamingTranscodeSize: StreamingResolutionEnum
|
||||
"""Write image thumbnails to disk when generating on the fly"""
|
||||
writeImageThumbnails: Boolean!
|
||||
"""API Key"""
|
||||
apiKey: String!
|
||||
"""Username"""
|
||||
@@ -144,6 +154,8 @@ type ConfigGeneralResult {
|
||||
password: String!
|
||||
"""Maximum session cookie age"""
|
||||
maxSessionAge: Int!
|
||||
"""Comma separated list of proxies to allow traffic from"""
|
||||
trustedProxies: [String!]!
|
||||
"""Name of the log file"""
|
||||
logFile: String
|
||||
"""Whether to also output to stderr"""
|
||||
@@ -176,56 +188,102 @@ type ConfigGeneralResult {
|
||||
stashBoxes: [StashBox!]!
|
||||
}
|
||||
|
||||
input ConfigDisableDropdownCreateInput {
|
||||
performer: Boolean
|
||||
tag: Boolean
|
||||
studio: Boolean
|
||||
}
|
||||
|
||||
input ConfigInterfaceInput {
|
||||
"""Ordered list of items that should be shown in the menu"""
|
||||
menuItems: [String!]
|
||||
|
||||
"""Enable sound on mouseover previews"""
|
||||
soundOnPreview: Boolean
|
||||
|
||||
"""Show title and tags in wall view"""
|
||||
wallShowTitle: Boolean
|
||||
"""Wall playback type"""
|
||||
wallPlayback: String
|
||||
|
||||
"""Maximum duration (in seconds) in which a scene video will loop in the scene player"""
|
||||
maximumLoopDuration: Int
|
||||
"""If true, video will autostart on load in the scene player"""
|
||||
autostartVideo: Boolean
|
||||
"""If true, video will autostart when loading from play random or play selected"""
|
||||
autostartVideoOnPlaySelected: Boolean
|
||||
"""If true, next scene in playlist will be played at video end by default"""
|
||||
continuePlaylistDefault: Boolean
|
||||
|
||||
"""If true, studio overlays will be shown as text instead of logo images"""
|
||||
showStudioAsText: Boolean
|
||||
|
||||
"""Custom CSS"""
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
|
||||
"""Interface language"""
|
||||
language: String
|
||||
|
||||
"""Slideshow Delay"""
|
||||
slideshowDelay: Int
|
||||
|
||||
"""Set to true to disable creating new objects via the dropdown menus"""
|
||||
disableDropdownCreate: ConfigDisableDropdownCreateInput
|
||||
|
||||
"""Handy Connection Key"""
|
||||
handyKey: String
|
||||
"""Funscript Time Offset"""
|
||||
funscriptOffset: Int
|
||||
"""True if we should not auto-open a browser window on startup"""
|
||||
noBrowser: Boolean
|
||||
}
|
||||
|
||||
type ConfigDisableDropdownCreate {
|
||||
performer: Boolean!
|
||||
tag: Boolean!
|
||||
studio: Boolean!
|
||||
}
|
||||
|
||||
type ConfigInterfaceResult {
|
||||
"""Ordered list of items that should be shown in the menu"""
|
||||
menuItems: [String!]
|
||||
|
||||
"""Enable sound on mouseover previews"""
|
||||
soundOnPreview: Boolean
|
||||
|
||||
"""Show title and tags in wall view"""
|
||||
wallShowTitle: Boolean
|
||||
"""Wall playback type"""
|
||||
wallPlayback: String
|
||||
|
||||
"""Maximum duration (in seconds) in which a scene video will loop in the scene player"""
|
||||
maximumLoopDuration: Int
|
||||
""""True if we should not auto-open a browser window on startup"""
|
||||
noBrowser: Boolean
|
||||
"""If true, video will autostart on load in the scene player"""
|
||||
autostartVideo: Boolean
|
||||
"""If true, video will autostart when loading from play random or play selected"""
|
||||
autostartVideoOnPlaySelected: Boolean
|
||||
"""If true, next scene in playlist will be played at video end by default"""
|
||||
continuePlaylistDefault: Boolean
|
||||
|
||||
"""If true, studio overlays will be shown as text instead of logo images"""
|
||||
showStudioAsText: Boolean
|
||||
|
||||
"""Custom CSS"""
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
|
||||
"""Interface language"""
|
||||
language: String
|
||||
|
||||
"""Slideshow Delay"""
|
||||
slideshowDelay: Int
|
||||
|
||||
"""Fields are true if creating via dropdown menus are disabled"""
|
||||
disabledDropdownCreate: ConfigDisableDropdownCreate!
|
||||
|
||||
"""Handy Connection Key"""
|
||||
handyKey: String
|
||||
"""Funscript Time Offset"""
|
||||
@@ -274,12 +332,31 @@ type ConfigScrapingResult {
|
||||
excludeTagPatterns: [String!]!
|
||||
}
|
||||
|
||||
type ConfigDefaultSettingsResult {
|
||||
identify: IdentifyMetadataTaskOptions
|
||||
|
||||
"""If true, delete file checkbox will be checked by default"""
|
||||
deleteFile: Boolean
|
||||
"""If true, delete generated supporting files checkbox will be checked by default"""
|
||||
deleteGenerated: Boolean
|
||||
}
|
||||
|
||||
input ConfigDefaultSettingsInput {
|
||||
identify: IdentifyMetadataInput
|
||||
|
||||
"""If true, delete file checkbox will be checked by default"""
|
||||
deleteFile: Boolean
|
||||
"""If true, delete generated files checkbox will be checked by default"""
|
||||
deleteGenerated: Boolean
|
||||
}
|
||||
|
||||
"""All configuration settings"""
|
||||
type ConfigResult {
|
||||
general: ConfigGeneralResult!
|
||||
interface: ConfigInterfaceResult!
|
||||
dlna: ConfigDLNAResult!
|
||||
scraping: ConfigScrapingResult!
|
||||
defaults: ConfigDefaultSettingsResult!
|
||||
}
|
||||
|
||||
"""Directory structure of a path"""
|
||||
|
||||
@@ -72,7 +72,7 @@ input PerformerFilterType {
|
||||
"""Filter to only include performers missing this property"""
|
||||
is_missing: String
|
||||
"""Filter to only include performers with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"""Filter by tag count"""
|
||||
tag_count: IntCriterionInput
|
||||
"""Filter by scene count"""
|
||||
@@ -99,11 +99,11 @@ input PerformerFilterType {
|
||||
|
||||
input SceneMarkerFilterType {
|
||||
"""Filter to only include scene markers with this tag"""
|
||||
tag_id: ID
|
||||
tag_id: ID @deprecated(reason: "use tags filter instead")
|
||||
"""Filter to only include scene markers with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"""Filter to only include scene markers attached to a scene with these tags"""
|
||||
scene_tags: MultiCriterionInput
|
||||
scene_tags: HierarchicalMultiCriterionInput
|
||||
"""Filter to only include scene markers with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
}
|
||||
@@ -143,11 +143,11 @@ input SceneFilterType {
|
||||
"""Filter to only include scenes with this movie"""
|
||||
movies: MultiCriterionInput
|
||||
"""Filter to only include scenes with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"""Filter by tag count"""
|
||||
tag_count: IntCriterionInput
|
||||
"""Filter to only include scenes with performers with these tags"""
|
||||
performer_tags: MultiCriterionInput
|
||||
performer_tags: HierarchicalMultiCriterionInput
|
||||
"""Filter to only include scenes with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter by performer count"""
|
||||
@@ -176,9 +176,15 @@ input MovieFilterType {
|
||||
is_missing: String
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
"""Filter to only include movies where performer appears in a scene"""
|
||||
performers: MultiCriterionInput
|
||||
}
|
||||
|
||||
input StudioFilterType {
|
||||
AND: StudioFilterType
|
||||
OR: StudioFilterType
|
||||
NOT: StudioFilterType
|
||||
|
||||
name: StringCriterionInput
|
||||
details: StringCriterionInput
|
||||
"""Filter to only include studios with this parent studio"""
|
||||
@@ -197,6 +203,8 @@ input StudioFilterType {
|
||||
gallery_count: IntCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
"""Filter by studio aliases"""
|
||||
aliases: StringCriterionInput
|
||||
}
|
||||
|
||||
input GalleryFilterType {
|
||||
@@ -224,11 +232,11 @@ input GalleryFilterType {
|
||||
"""Filter to only include galleries with this studio"""
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"""Filter to only include galleries with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"""Filter by tag count"""
|
||||
tag_count: IntCriterionInput
|
||||
"""Filter to only include galleries with performers with these tags"""
|
||||
performer_tags: MultiCriterionInput
|
||||
performer_tags: HierarchicalMultiCriterionInput
|
||||
"""Filter to only include galleries with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter by performer count"""
|
||||
@@ -267,6 +275,18 @@ input TagFilterType {
|
||||
|
||||
"""Filter by number of markers with this tag"""
|
||||
marker_count: IntCriterionInput
|
||||
|
||||
"""Filter by parent tags"""
|
||||
parents: HierarchicalMultiCriterionInput
|
||||
|
||||
"""Filter by child tags"""
|
||||
children: HierarchicalMultiCriterionInput
|
||||
|
||||
"""Filter by number of parent tags the tag has"""
|
||||
parent_count: IntCriterionInput
|
||||
|
||||
"""Filter by number f child tags the tag has"""
|
||||
child_count: IntCriterionInput
|
||||
}
|
||||
|
||||
input ImageFilterType {
|
||||
@@ -293,11 +313,11 @@ input ImageFilterType {
|
||||
"""Filter to only include images with this studio"""
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"""Filter to only include images with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"""Filter by tag count"""
|
||||
tag_count: IntCriterionInput
|
||||
"""Filter to only include images with performers with these tags"""
|
||||
performer_tags: MultiCriterionInput
|
||||
performer_tags: HierarchicalMultiCriterionInput
|
||||
"""Filter to only include images with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter by performer count"""
|
||||
@@ -357,7 +377,7 @@ input GenderCriterionInput {
|
||||
input HierarchicalMultiCriterionInput {
|
||||
value: [ID!]
|
||||
modifier: CriterionModifier!
|
||||
depth: Int!
|
||||
depth: Int
|
||||
}
|
||||
|
||||
enum FilterMode {
|
||||
|
||||
@@ -74,6 +74,11 @@ input BulkGalleryUpdateInput {
|
||||
|
||||
input GalleryDestroyInput {
|
||||
ids: [ID!]!
|
||||
"""
|
||||
If true, then the zip file will be deleted if the gallery is zip-file-based.
|
||||
If gallery is folder-based, then any files not associated with other
|
||||
galleries will be deleted, along with the folder, if it is not empty.
|
||||
"""
|
||||
delete_file: Boolean
|
||||
delete_generated: Boolean
|
||||
}
|
||||
|
||||
@@ -70,5 +70,9 @@ input ImagesDestroyInput {
|
||||
|
||||
type FindImagesResultType {
|
||||
count: Int!
|
||||
"""Total megapixels of the images"""
|
||||
megapixels: Float!
|
||||
"""Total file size in bytes"""
|
||||
filesize: Float!
|
||||
images: [Image!]!
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
scalar Time
|
||||
|
||||
enum LogLevel {
|
||||
Trace
|
||||
Debug
|
||||
Info
|
||||
Progress
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
scalar Upload
|
||||
|
||||
input GenerateMetadataInput {
|
||||
sprites: Boolean!
|
||||
previews: Boolean!
|
||||
imagePreviews: Boolean!
|
||||
sprites: Boolean
|
||||
previews: Boolean
|
||||
imagePreviews: Boolean
|
||||
previewOptions: GeneratePreviewOptionsInput
|
||||
markers: Boolean!
|
||||
transcodes: Boolean!
|
||||
phashes: Boolean!
|
||||
markers: Boolean
|
||||
markerImagePreviews: Boolean
|
||||
markerScreenshots: Boolean
|
||||
transcodes: Boolean
|
||||
phashes: Boolean
|
||||
|
||||
"""scene ids to generate for"""
|
||||
sceneIDs: [ID!]
|
||||
@@ -45,6 +47,8 @@ input ScanMetadataInput {
|
||||
scanGenerateSprites: Boolean
|
||||
"""Generate phashes during scan"""
|
||||
scanGeneratePhashes: Boolean
|
||||
"""Generate image thumbnails during scan"""
|
||||
scanGenerateThumbnails: Boolean
|
||||
}
|
||||
|
||||
input CleanMetadataInput {
|
||||
@@ -63,6 +67,88 @@ input AutoTagMetadataInput {
|
||||
tags: [String!]
|
||||
}
|
||||
|
||||
enum IdentifyFieldStrategy {
|
||||
"""Never sets the field value"""
|
||||
IGNORE
|
||||
"""
|
||||
For multi-value fields, merge with existing.
|
||||
For single-value fields, ignore if already set
|
||||
"""
|
||||
MERGE
|
||||
"""Always replaces the value if a value is found.
|
||||
For multi-value fields, any existing values are removed and replaced with the
|
||||
scraped values.
|
||||
"""
|
||||
OVERWRITE
|
||||
}
|
||||
|
||||
input IdentifyFieldOptionsInput {
|
||||
field: String!
|
||||
strategy: IdentifyFieldStrategy!
|
||||
"""creates missing objects if needed - only applicable for performers, tags and studios"""
|
||||
createMissing: Boolean
|
||||
}
|
||||
|
||||
input IdentifyMetadataOptionsInput {
|
||||
"""any fields missing from here are defaulted to MERGE and createMissing false"""
|
||||
fieldOptions: [IdentifyFieldOptionsInput!]
|
||||
"""defaults to true if not provided"""
|
||||
setCoverImage: Boolean
|
||||
setOrganized: Boolean
|
||||
"""defaults to true if not provided"""
|
||||
includeMalePerformers: Boolean
|
||||
}
|
||||
|
||||
input IdentifySourceInput {
|
||||
source: ScraperSourceInput!
|
||||
"""Options defined for a source override the defaults"""
|
||||
options: IdentifyMetadataOptionsInput
|
||||
}
|
||||
|
||||
input IdentifyMetadataInput {
|
||||
"""An ordered list of sources to identify items with. Only the first source that finds a match is used."""
|
||||
sources: [IdentifySourceInput!]!
|
||||
"""Options defined here override the configured defaults"""
|
||||
options: IdentifyMetadataOptionsInput
|
||||
|
||||
"""scene ids to identify"""
|
||||
sceneIDs: [ID!]
|
||||
|
||||
"""paths of scenes to identify - ignored if scene ids are set"""
|
||||
paths: [String!]
|
||||
}
|
||||
|
||||
# types for default options
|
||||
type IdentifyFieldOptions {
|
||||
field: String!
|
||||
strategy: IdentifyFieldStrategy!
|
||||
"""creates missing objects if needed - only applicable for performers, tags and studios"""
|
||||
createMissing: Boolean
|
||||
}
|
||||
|
||||
type IdentifyMetadataOptions {
|
||||
"""any fields missing from here are defaulted to MERGE and createMissing false"""
|
||||
fieldOptions: [IdentifyFieldOptions!]
|
||||
"""defaults to true if not provided"""
|
||||
setCoverImage: Boolean
|
||||
setOrganized: Boolean
|
||||
"""defaults to true if not provided"""
|
||||
includeMalePerformers: Boolean
|
||||
}
|
||||
|
||||
type IdentifySource {
|
||||
source: ScraperSource!
|
||||
"""Options defined for a source override the defaults"""
|
||||
options: IdentifyMetadataOptions
|
||||
}
|
||||
|
||||
type IdentifyMetadataTaskOptions {
|
||||
"""An ordered list of sources to identify items with. Only the first source that finds a match is used."""
|
||||
sources: [IdentifySource!]!
|
||||
"""Options defined here override the configured defaults"""
|
||||
options: IdentifyMetadataOptions
|
||||
}
|
||||
|
||||
input ExportObjectTypeInput {
|
||||
ids: [String!]
|
||||
all: Boolean
|
||||
|
||||
@@ -17,6 +17,7 @@ type Movie {
|
||||
front_image_path: String # Resolver
|
||||
back_image_path: String # Resolver
|
||||
scene_count: Int # Resolver
|
||||
scenes: [Scene!]!
|
||||
}
|
||||
|
||||
input MovieCreateInput {
|
||||
@@ -60,4 +61,4 @@ input MovieDestroyInput {
|
||||
type FindMoviesResultType {
|
||||
count: Int!
|
||||
movies: [Movie!]!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ type Performer {
|
||||
weight: Int
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
movie_count: Int
|
||||
movies: [Movie!]!
|
||||
}
|
||||
|
||||
input PerformerCreateInput {
|
||||
|
||||
@@ -12,6 +12,8 @@ type SceneMarker {
|
||||
stream: String! # Resolver
|
||||
"""The path to the preview image for this marker"""
|
||||
preview: String! # Resolver
|
||||
"""The path to the screenshot image for this marker"""
|
||||
screenshot: String! # Resolver
|
||||
}
|
||||
|
||||
input SceneMarkerCreateInput {
|
||||
|
||||
@@ -103,6 +103,7 @@ input BulkSceneUpdateInput {
|
||||
gallery_ids: BulkUpdateIds
|
||||
performer_ids: BulkUpdateIds
|
||||
tag_ids: BulkUpdateIds
|
||||
movie_ids: BulkUpdateIds
|
||||
}
|
||||
|
||||
input SceneDestroyInput {
|
||||
@@ -119,6 +120,10 @@ input ScenesDestroyInput {
|
||||
|
||||
type FindScenesResultType {
|
||||
count: Int!
|
||||
"""Total duration in seconds"""
|
||||
duration: Float!
|
||||
"""Total file size in bytes"""
|
||||
filesize: Float!
|
||||
scenes: [Scene!]!
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
type ScrapedMovieStudio {
|
||||
"""Set if studio matched"""
|
||||
id: ID
|
||||
name: String!
|
||||
url: String
|
||||
}
|
||||
|
||||
"""A movie from a scraping operation..."""
|
||||
type ScrapedMovie {
|
||||
stored_id: ID
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
@@ -15,7 +9,7 @@ type ScrapedMovie {
|
||||
director: String
|
||||
url: String
|
||||
synopsis: String
|
||||
studio: ScrapedMovieStudio
|
||||
studio: ScrapedStudio
|
||||
|
||||
"""This should be a base64 encoded data URL"""
|
||||
front_image: String
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""A performer from a scraping operation..."""
|
||||
type ScrapedPerformer {
|
||||
"""Set if performer matched"""
|
||||
stored_id: ID
|
||||
name: String
|
||||
gender: String
|
||||
url: String
|
||||
@@ -16,11 +18,11 @@ type ScrapedPerformer {
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
# Should be ScrapedPerformerTag - but would be identical types
|
||||
tags: [ScrapedSceneTag!]
|
||||
tags: [ScrapedTag!]
|
||||
|
||||
"""This should be a base64 encoded data URL"""
|
||||
image: String
|
||||
image: String @deprecated(reason: "use images instead")
|
||||
images: [String!]
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
@@ -29,6 +31,8 @@ type ScrapedPerformer {
|
||||
}
|
||||
|
||||
input ScrapedPerformerInput {
|
||||
"""Set if performer matched"""
|
||||
stored_id: ID
|
||||
name: String
|
||||
gender: String
|
||||
url: String
|
||||
|
||||
@@ -26,58 +26,17 @@ type Scraper {
|
||||
movie: ScraperSpec
|
||||
}
|
||||
|
||||
type ScrapedScenePerformer {
|
||||
"""Set if performer matched"""
|
||||
stored_id: ID
|
||||
name: String!
|
||||
gender: String
|
||||
url: String
|
||||
twitter: String
|
||||
instagram: String
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
height: String
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
tags: [ScrapedSceneTag!]
|
||||
|
||||
remote_site_id: String
|
||||
images: [String!]
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
weight: String
|
||||
}
|
||||
|
||||
type ScrapedSceneMovie {
|
||||
"""Set if movie matched"""
|
||||
stored_id: ID
|
||||
name: String!
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
}
|
||||
|
||||
type ScrapedSceneStudio {
|
||||
type ScrapedStudio {
|
||||
"""Set if studio matched"""
|
||||
stored_id: ID
|
||||
name: String!
|
||||
url: String
|
||||
image: String
|
||||
|
||||
remote_site_id: String
|
||||
}
|
||||
|
||||
type ScrapedSceneTag {
|
||||
type ScrapedTag {
|
||||
"""Set if tag matched"""
|
||||
stored_id: ID
|
||||
name: String!
|
||||
@@ -94,25 +53,109 @@ type ScrapedScene {
|
||||
|
||||
file: SceneFileType # Resolver
|
||||
|
||||
studio: ScrapedSceneStudio
|
||||
tags: [ScrapedSceneTag!]
|
||||
performers: [ScrapedScenePerformer!]
|
||||
movies: [ScrapedSceneMovie!]
|
||||
studio: ScrapedStudio
|
||||
tags: [ScrapedTag!]
|
||||
performers: [ScrapedPerformer!]
|
||||
movies: [ScrapedMovie!]
|
||||
|
||||
remote_site_id: String
|
||||
duration: Int
|
||||
fingerprints: [StashBoxFingerprint!]
|
||||
}
|
||||
|
||||
input ScrapedSceneInput {
|
||||
title: String
|
||||
details: String
|
||||
url: String
|
||||
date: String
|
||||
|
||||
# no image, file, duration or relationships
|
||||
|
||||
remote_site_id: String
|
||||
}
|
||||
|
||||
type ScrapedGallery {
|
||||
title: String
|
||||
details: String
|
||||
url: String
|
||||
date: String
|
||||
|
||||
studio: ScrapedSceneStudio
|
||||
tags: [ScrapedSceneTag!]
|
||||
performers: [ScrapedScenePerformer!]
|
||||
studio: ScrapedStudio
|
||||
tags: [ScrapedTag!]
|
||||
performers: [ScrapedPerformer!]
|
||||
}
|
||||
|
||||
input ScrapedGalleryInput {
|
||||
title: String
|
||||
details: String
|
||||
url: String
|
||||
date: String
|
||||
|
||||
# no studio, tags or performers
|
||||
}
|
||||
|
||||
input ScraperSourceInput {
|
||||
"""Index of the configured stash-box instance to use. Should be unset if scraper_id is set"""
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
"""Stash-box endpoint"""
|
||||
stash_box_endpoint: String
|
||||
"""Scraper ID to scrape with. Should be unset if stash_box_index is set"""
|
||||
scraper_id: ID
|
||||
}
|
||||
|
||||
type ScraperSource {
|
||||
"""Index of the configured stash-box instance to use. Should be unset if scraper_id is set"""
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
"""Stash-box endpoint"""
|
||||
stash_box_endpoint: String
|
||||
"""Scraper ID to scrape with. Should be unset if stash_box_index is set"""
|
||||
scraper_id: ID
|
||||
}
|
||||
|
||||
input ScrapeSingleSceneInput {
|
||||
"""Instructs to query by string"""
|
||||
query: String
|
||||
"""Instructs to query by scene fingerprints"""
|
||||
scene_id: ID
|
||||
"""Instructs to query by scene fragment"""
|
||||
scene_input: ScrapedSceneInput
|
||||
}
|
||||
|
||||
input ScrapeMultiScenesInput {
|
||||
"""Instructs to query by scene fingerprints"""
|
||||
scene_ids: [ID!]
|
||||
}
|
||||
|
||||
input ScrapeSinglePerformerInput {
|
||||
"""Instructs to query by string"""
|
||||
query: String
|
||||
"""Instructs to query by performer id"""
|
||||
performer_id: ID
|
||||
"""Instructs to query by performer fragment"""
|
||||
performer_input: ScrapedPerformerInput
|
||||
}
|
||||
|
||||
input ScrapeMultiPerformersInput {
|
||||
"""Instructs to query by scene fingerprints"""
|
||||
performer_ids: [ID!]
|
||||
}
|
||||
|
||||
input ScrapeSingleGalleryInput {
|
||||
"""Instructs to query by string"""
|
||||
query: String
|
||||
"""Instructs to query by gallery id"""
|
||||
gallery_id: ID
|
||||
"""Instructs to query by gallery fragment"""
|
||||
gallery_input: ScrapedGalleryInput
|
||||
}
|
||||
|
||||
input ScrapeSingleMovieInput {
|
||||
"""Instructs to query by string"""
|
||||
query: String
|
||||
"""Instructs to query by movie id"""
|
||||
movie_id: ID
|
||||
"""Instructs to query by gallery fragment"""
|
||||
movie_input: ScrapedMovieInput
|
||||
}
|
||||
|
||||
input StashBoxSceneQueryInput {
|
||||
@@ -135,7 +178,7 @@ input StashBoxPerformerQueryInput {
|
||||
|
||||
type StashBoxPerformerQueryResult {
|
||||
query: String!
|
||||
results: [ScrapedScenePerformer!]!
|
||||
results: [ScrapedPerformer!]!
|
||||
}
|
||||
|
||||
type StashBoxFingerprint {
|
||||
@@ -144,10 +187,16 @@ type StashBoxFingerprint {
|
||||
duration: Int!
|
||||
}
|
||||
|
||||
"""If neither performer_ids nor performer_names are set, tag all performers"""
|
||||
input StashBoxBatchPerformerTagInput {
|
||||
"Stash endpoint to use for the performer tagging"
|
||||
endpoint: Int!
|
||||
"Fields to exclude when executing the performer tagging"
|
||||
exclude_fields: [String!]
|
||||
"Refresh performers already tagged by StashBox if true. Only tag performers with no StashBox tagging if false"
|
||||
refresh: Boolean!
|
||||
"If set, only tag these performer ids"
|
||||
performer_ids: [ID!]
|
||||
"If set, only tag these performer names"
|
||||
performer_names: [String!]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ type Studio {
|
||||
url: String
|
||||
parent_studio: Studio
|
||||
child_studios: [Studio!]!
|
||||
aliases: [String!]!
|
||||
|
||||
image_path: String # Resolver
|
||||
scene_count: Int # Resolver
|
||||
@@ -15,6 +16,8 @@ type Studio {
|
||||
details: String
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
movie_count: Int
|
||||
movies: [Movie!]!
|
||||
}
|
||||
|
||||
input StudioCreateInput {
|
||||
@@ -26,6 +29,7 @@ input StudioCreateInput {
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
details: String
|
||||
aliases: [String!]
|
||||
}
|
||||
|
||||
input StudioUpdateInput {
|
||||
@@ -38,6 +42,7 @@ input StudioUpdateInput {
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
details: String
|
||||
aliases: [String!]
|
||||
}
|
||||
|
||||
input StudioDestroyInput {
|
||||
|
||||
@@ -11,6 +11,9 @@ type Tag {
|
||||
image_count: Int # Resolver
|
||||
gallery_count: Int # Resolver
|
||||
performer_count: Int
|
||||
|
||||
parents: [Tag!]!
|
||||
children: [Tag!]!
|
||||
}
|
||||
|
||||
input TagCreateInput {
|
||||
@@ -19,6 +22,9 @@ input TagCreateInput {
|
||||
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
|
||||
parent_ids: [ID!]
|
||||
child_ids: [ID!]
|
||||
}
|
||||
|
||||
input TagUpdateInput {
|
||||
@@ -28,6 +34,9 @@ input TagUpdateInput {
|
||||
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
|
||||
parent_ids: [ID!]
|
||||
child_ids: [ID!]
|
||||
}
|
||||
|
||||
input TagDestroyInput {
|
||||
|
||||
@@ -49,6 +49,7 @@ fragment PerformerFragment on Performer {
|
||||
disambiguation
|
||||
aliases
|
||||
gender
|
||||
merged_ids
|
||||
urls {
|
||||
...URLFragment
|
||||
}
|
||||
@@ -75,11 +76,6 @@ fragment PerformerFragment on Performer {
|
||||
piercings {
|
||||
...BodyModificationFragment
|
||||
}
|
||||
details
|
||||
death_date {
|
||||
...FuzzyDateFragment
|
||||
}
|
||||
weight
|
||||
}
|
||||
|
||||
fragment PerformerAppearanceFragment on PerformerAppearance {
|
||||
@@ -127,8 +123,8 @@ query FindSceneByFingerprint($fingerprint: FingerprintQueryInput!) {
|
||||
}
|
||||
}
|
||||
|
||||
query FindScenesByFingerprints($fingerprints: [String!]!) {
|
||||
findScenesByFingerprints(fingerprints: $fingerprints) {
|
||||
query FindScenesByFullFingerprints($fingerprints: [FingerprintQueryInput!]!) {
|
||||
findScenesByFullFingerprints(fingerprints: $fingerprints) {
|
||||
...SceneFragment
|
||||
}
|
||||
}
|
||||
@@ -151,6 +147,12 @@ query FindPerformerByID($id: ID!) {
|
||||
}
|
||||
}
|
||||
|
||||
query FindSceneByID($id: ID!) {
|
||||
findScene(id: $id) {
|
||||
...SceneFragment
|
||||
}
|
||||
}
|
||||
|
||||
mutation SubmitFingerprint($input: FingerprintSubmission!) {
|
||||
submitFingerprint(input: $input)
|
||||
}
|
||||
|
||||
15
main.go
15
main.go
@@ -2,25 +2,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
|
||||
"github.com/stashapp/stash/pkg/api"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
//go:embed ui/v2.5/build
|
||||
var uiBox embed.FS
|
||||
|
||||
//go:embed ui/login
|
||||
var loginUIBox embed.FS
|
||||
|
||||
func main() {
|
||||
manager.Initialize()
|
||||
api.Start()
|
||||
api.Start(uiBox, loginUIBox)
|
||||
|
||||
// stop any profiling at exit
|
||||
defer pprof.StopCPUProfile()
|
||||
blockForever()
|
||||
|
||||
err := manager.GetInstance().Shutdown()
|
||||
if err != nil {
|
||||
logger.Errorf("Error when closing: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func blockForever() {
|
||||
|
||||
143
pkg/api/authentication.go
Normal file
143
pkg/api/authentication.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/session"
|
||||
)
|
||||
|
||||
const loginEndPoint = "/login"
|
||||
|
||||
const (
|
||||
tripwireActivatedErrMsg = "Stash is exposed to the public internet without authentication, and is not serving any more content to protect your privacy. " +
|
||||
"More information and fixes are available at https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet"
|
||||
|
||||
externalAccessErrMsg = "You have attempted to access Stash over the internet, and authentication is not enabled. " +
|
||||
"This is extremely dangerous! The whole world can see your your stash page and browse your files! " +
|
||||
"Stash is not answering any other requests to protect your privacy. " +
|
||||
"Please read the log entry or visit https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet"
|
||||
)
|
||||
|
||||
func allowUnauthenticated(r *http.Request) bool {
|
||||
return strings.HasPrefix(r.URL.Path, loginEndPoint) || r.URL.Path == "/css"
|
||||
}
|
||||
|
||||
func authenticateHandler() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
c := config.GetInstance()
|
||||
|
||||
if !checkSecurityTripwireActivated(c, w) {
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := manager.GetInstance().SessionStore.Authenticate(w, r)
|
||||
if err != nil {
|
||||
if errors.Is(err, session.ErrUnauthorized) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, err = w.Write([]byte(err.Error()))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// unauthorized error
|
||||
w.Header().Add("WWW-Authenticate", `FormBased`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := session.CheckAllowPublicWithoutAuth(c, r); err != nil {
|
||||
var externalAccess session.ExternalAccessError
|
||||
var untrustedProxy session.UntrustedProxyError
|
||||
switch {
|
||||
case errors.As(err, &externalAccess):
|
||||
securityActivateTripwireAccessedFromInternetWithoutAuth(c, externalAccess, w)
|
||||
return
|
||||
case errors.As(err, &untrustedProxy):
|
||||
logger.Warnf("Rejected request from untrusted proxy: %v", net.IP(untrustedProxy))
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
default:
|
||||
logger.Errorf("Error checking external access security: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
if c.HasCredentials() {
|
||||
// authentication is required
|
||||
if userID == "" && !allowUnauthenticated(r) {
|
||||
// authentication was not received, redirect
|
||||
// if graphql was requested, we just return a forbidden error
|
||||
if r.URL.Path == "/graphql" {
|
||||
w.Header().Add("WWW-Authenticate", `FormBased`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
prefix := getProxyPrefix(r.Header)
|
||||
|
||||
// otherwise redirect to the login page
|
||||
u := url.URL{
|
||||
Path: prefix + "/login",
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set(returnURLParam, prefix+r.URL.Path)
|
||||
u.RawQuery = q.Encode()
|
||||
http.Redirect(w, r, u.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx = session.SetCurrentUserID(ctx, userID)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkSecurityTripwireActivated(c *config.Instance, w http.ResponseWriter) bool {
|
||||
if accessErr := session.CheckExternalAccessTripwire(c); accessErr != nil {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, err := w.Write([]byte(tripwireActivatedErrMsg))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func securityActivateTripwireAccessedFromInternetWithoutAuth(c *config.Instance, accessErr session.ExternalAccessError, w http.ResponseWriter) {
|
||||
session.LogExternalAccessError(accessErr)
|
||||
|
||||
err := c.ActivatePublicAccessTripwire(net.IP(accessErr).String())
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, err = w.Write([]byte(externalAccessErrMsg))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
err = manager.GetInstance().Shutdown()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func getArgumentMap(ctx context.Context) map[string]interface{} {
|
||||
func getUpdateInputMap(ctx context.Context) map[string]interface{} {
|
||||
args := getArgumentMap(ctx)
|
||||
|
||||
input, _ := args[updateInputField]
|
||||
input := args[updateInputField]
|
||||
var ret map[string]interface{}
|
||||
if input != nil {
|
||||
ret, _ = input.(map[string]interface{})
|
||||
@@ -36,7 +36,7 @@ func getUpdateInputMap(ctx context.Context) map[string]interface{} {
|
||||
func getUpdateInputMaps(ctx context.Context) []map[string]interface{} {
|
||||
args := getArgumentMap(ctx)
|
||||
|
||||
input, _ := args[updateInputField]
|
||||
input := args[updateInputField]
|
||||
var ret []map[string]interface{}
|
||||
if input != nil {
|
||||
// convert []interface{} into []map[string]interface{}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
//we use the github REST V3 API as no login is required
|
||||
// we use the github REST V3 API as no login is required
|
||||
const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases"
|
||||
const apiTags string = "https://api.github.com/repos/stashapp/stash/tags"
|
||||
const apiAcceptHeader string = "application/vnd.github.v3+json"
|
||||
@@ -29,6 +30,7 @@ var ErrNoVersion = errors.New("no stash version")
|
||||
var stashReleases = func() map[string]string {
|
||||
return map[string]string{
|
||||
"darwin/amd64": "stash-osx",
|
||||
"darwin/arm64": "stash-osx-applesilicon",
|
||||
"linux/amd64": "stash-linux",
|
||||
"windows/amd64": "stash-win.exe",
|
||||
"linux/arm": "stash-pi",
|
||||
@@ -106,34 +108,37 @@ type githubTagResponse struct {
|
||||
Node_id string
|
||||
}
|
||||
|
||||
func makeGithubRequest(url string, output interface{}) error {
|
||||
func makeGithubRequest(ctx context.Context, url string, output interface{}) error {
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
|
||||
req.Header.Add("Accept", apiAcceptHeader) // gh api recommendation , send header with api version
|
||||
response, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Github API request failed: %s", err)
|
||||
//lint:ignore ST1005 Github is a proper capitalized noun
|
||||
return fmt.Errorf("Github API request failed: %w", err)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
//lint:ignore ST1005 Github is a proper capitalized noun
|
||||
return fmt.Errorf("Github API request failed: %s", response.Status)
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(response.Body)
|
||||
data, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Github API read response failed: %s", err)
|
||||
//lint:ignore ST1005 Github is a proper capitalized noun
|
||||
return fmt.Errorf("Github API read response failed: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, output)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unmarshalling Github API response failed: %s", err)
|
||||
return fmt.Errorf("unmarshalling Github API response failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -143,7 +148,7 @@ func makeGithubRequest(url string, output interface{}) error {
|
||||
// If running a build from the "master" branch, then the latest full release
|
||||
// is used, otherwise it uses the release that is tagged with "latest_develop"
|
||||
// which is the latest pre-release build.
|
||||
func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease string, err error) {
|
||||
func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string, latestRelease string, err error) {
|
||||
|
||||
arch := runtime.GOARCH // https://en.wikipedia.org/wiki/Comparison_of_ARM_cores
|
||||
isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4 // armv6 doesn't support any of these features
|
||||
@@ -176,14 +181,14 @@ func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease strin
|
||||
}
|
||||
|
||||
release := githubReleasesResponse{}
|
||||
err = makeGithubRequest(url, &release)
|
||||
err = makeGithubRequest(ctx, url, &release)
|
||||
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if release.Prerelease == usePreRelease {
|
||||
latestVersion = getReleaseHash(release, shortHash, usePreRelease)
|
||||
latestVersion = getReleaseHash(ctx, release, shortHash, usePreRelease)
|
||||
|
||||
if wantedRelease != "" {
|
||||
for _, asset := range release.Assets {
|
||||
@@ -196,17 +201,17 @@ func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease strin
|
||||
}
|
||||
|
||||
if latestVersion == "" {
|
||||
return "", "", fmt.Errorf("No version found for \"%s\"", version)
|
||||
return "", "", fmt.Errorf("no version found for \"%s\"", version)
|
||||
}
|
||||
return latestVersion, latestRelease, nil
|
||||
}
|
||||
|
||||
func getReleaseHash(release githubReleasesResponse, shortHash bool, usePreRelease bool) string {
|
||||
func getReleaseHash(ctx context.Context, release githubReleasesResponse, shortHash bool, usePreRelease bool) string {
|
||||
shaLength := len(release.Target_commitish)
|
||||
// the /latest API call doesn't return the hash in target_commitish
|
||||
// also add sanity check in case Target_commitish is not 40 characters
|
||||
if !usePreRelease || shaLength != 40 {
|
||||
return getShaFromTags(shortHash, release.Tag_name)
|
||||
return getShaFromTags(ctx, shortHash, release.Tag_name)
|
||||
}
|
||||
|
||||
if shortHash {
|
||||
@@ -221,9 +226,9 @@ func getReleaseHash(release githubReleasesResponse, shortHash bool, usePreReleas
|
||||
return release.Target_commitish
|
||||
}
|
||||
|
||||
func printLatestVersion() {
|
||||
func printLatestVersion(ctx context.Context) {
|
||||
_, githash, _ = GetVersion()
|
||||
latest, _, err := GetLatestVersion(true)
|
||||
latest, _, err := GetLatestVersion(ctx, true)
|
||||
if err != nil {
|
||||
logger.Errorf("Couldn't find latest version: %s", err)
|
||||
} else {
|
||||
@@ -237,13 +242,21 @@ func printLatestVersion() {
|
||||
|
||||
// get sha from the github api tags endpoint
|
||||
// returns the sha1 hash/shorthash or "" if something's wrong
|
||||
func getShaFromTags(shortHash bool, name string) string {
|
||||
func getShaFromTags(ctx context.Context, shortHash bool, name string) string {
|
||||
url := apiTags
|
||||
tags := []githubTagResponse{}
|
||||
err := makeGithubRequest(url, &tags)
|
||||
err := makeGithubRequest(ctx, url, &tags)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Github Tags Api %v", err)
|
||||
// If the context is canceled, we don't want to log this as an error
|
||||
// in the path. The function here just gives up and returns "" if
|
||||
// something goes wrong. Hence, log the error at the info-level so
|
||||
// it's still present, but don't treat this as an error.
|
||||
if errors.Is(err, context.Canceled) {
|
||||
logger.Infof("aborting sha request due to context cancellation")
|
||||
} else {
|
||||
logger.Errorf("Github Tags Api: %v", err)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
_, gitShort, _ := GetVersion() // retrieve short hash to check actual length
|
||||
|
||||
@@ -5,8 +5,8 @@ package api
|
||||
type key int
|
||||
|
||||
const (
|
||||
galleryKey key = iota
|
||||
performerKey
|
||||
// galleryKey key = 0
|
||||
performerKey key = iota + 1
|
||||
sceneKey
|
||||
studioKey
|
||||
movieKey
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gobuffalo/packr/v2"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/static"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type imageBox struct {
|
||||
box *packr.Box
|
||||
box fs.FS
|
||||
files []string
|
||||
}
|
||||
|
||||
func newImageBox(box *packr.Box) *imageBox {
|
||||
return &imageBox{
|
||||
box: box,
|
||||
files: box.List(),
|
||||
func newImageBox(box fs.FS) (*imageBox, error) {
|
||||
ret := &imageBox{
|
||||
box: box,
|
||||
}
|
||||
|
||||
err := fs.WalkDir(box, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if !d.IsDir() {
|
||||
ret.files = append(ret.files, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return ret, err
|
||||
}
|
||||
|
||||
var performerBox *imageBox
|
||||
@@ -26,8 +38,15 @@ var performerBoxMale *imageBox
|
||||
var performerBoxCustom *imageBox
|
||||
|
||||
func initialiseImages() {
|
||||
performerBox = newImageBox(packr.New("Performer Box", "../../static/performer"))
|
||||
performerBoxMale = newImageBox(packr.New("Male Performer Box", "../../static/performer_male"))
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -36,7 +55,11 @@ func initialiseCustomImages() {
|
||||
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.
|
||||
performerBoxCustom = newImageBox(packr.Folder(customPath))
|
||||
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
|
||||
}
|
||||
@@ -63,5 +86,11 @@ func getRandomPerformerImageUsingName(name, gender, customPath string) ([]byte,
|
||||
|
||||
imageFiles := box.files
|
||||
index := utils.IntFromString(name) % uint64(len(imageFiles))
|
||||
return box.box.Find(imageFiles[index])
|
||||
img, err := box.box.Open(imageFiles[index])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer img.Close()
|
||||
|
||||
return io.ReadAll(img)
|
||||
}
|
||||
|
||||
34
pkg/api/locale.go
Normal file
34
pkg/api/locale.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// matcher defines a matcher for the languages we support
|
||||
var matcher = language.NewMatcher([]language.Tag{
|
||||
language.MustParse("en-US"), // The first language is used as fallback.
|
||||
language.MustParse("en-GB"),
|
||||
language.MustParse("en-AU"),
|
||||
language.MustParse("es-ES"),
|
||||
language.MustParse("de-DE"),
|
||||
language.MustParse("it-IT"),
|
||||
language.MustParse("fr-FR"),
|
||||
language.MustParse("pt-BR"),
|
||||
language.MustParse("sv-SE"),
|
||||
language.MustParse("zh-CN"),
|
||||
language.MustParse("zh-TW"),
|
||||
})
|
||||
|
||||
// newCollator parses a locale into a collator
|
||||
// Go through the available matches and return a valid match, in practice the first is a fallback
|
||||
// Optionally pass collation options through for creation.
|
||||
// If passed a nil-locale string, return nil
|
||||
func newCollator(locale *string, opts ...collate.Option) *collate.Collator {
|
||||
if locale == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tag, _ := language.MatchStrings(matcher, *locale)
|
||||
return collate.New(tag, opts...)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
@@ -10,6 +11,11 @@ import (
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotImplemented = errors.New("not implemented")
|
||||
ErrNotSupported = errors.New("not supported")
|
||||
)
|
||||
|
||||
type hookExecutor interface {
|
||||
ExecutePostHooks(ctx context.Context, id int, hookType plugin.HookTriggerEnum, input interface{}, inputFields []string)
|
||||
}
|
||||
@@ -53,22 +59,6 @@ func (r *Resolver) Tag() models.TagResolver {
|
||||
return &tagResolver{r}
|
||||
}
|
||||
|
||||
func (r *Resolver) ScrapedSceneTag() models.ScrapedSceneTagResolver {
|
||||
return &scrapedSceneTagResolver{r}
|
||||
}
|
||||
|
||||
func (r *Resolver) ScrapedSceneMovie() models.ScrapedSceneMovieResolver {
|
||||
return &scrapedSceneMovieResolver{r}
|
||||
}
|
||||
|
||||
func (r *Resolver) ScrapedScenePerformer() models.ScrapedScenePerformerResolver {
|
||||
return &scrapedScenePerformerResolver{r}
|
||||
}
|
||||
|
||||
func (r *Resolver) ScrapedSceneStudio() models.ScrapedSceneStudioResolver {
|
||||
return &scrapedSceneStudioResolver{r}
|
||||
}
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
||||
type subscriptionResolver struct{ *Resolver }
|
||||
@@ -81,10 +71,6 @@ type imageResolver struct{ *Resolver }
|
||||
type studioResolver struct{ *Resolver }
|
||||
type movieResolver struct{ *Resolver }
|
||||
type tagResolver struct{ *Resolver }
|
||||
type scrapedSceneTagResolver struct{ *Resolver }
|
||||
type scrapedSceneMovieResolver struct{ *Resolver }
|
||||
type scrapedScenePerformerResolver struct{ *Resolver }
|
||||
type scrapedSceneStudioResolver struct{ *Resolver }
|
||||
|
||||
func (r *Resolver) withTxn(ctx context.Context, fn func(r models.Repository) error) error {
|
||||
return r.txnManager.WithTxn(ctx, fn)
|
||||
@@ -178,9 +164,9 @@ func (r *queryResolver) Version(ctx context.Context) (*models.Version, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
//Gets latest version (git shorthash commit for now)
|
||||
// Latestversion returns the latest git shorthash commit.
|
||||
func (r *queryResolver) Latestversion(ctx context.Context) (*models.ShortVersion, error) {
|
||||
ver, url, err := GetLatestVersion(true)
|
||||
ver, url, err := GetLatestVersion(ctx, true)
|
||||
if err == nil {
|
||||
logger.Infof("Retrieved latest hash: %s", ver)
|
||||
} else {
|
||||
|
||||
@@ -125,6 +125,18 @@ func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (r *movieResolver) Scenes(ctx context.Context, obj *models.Movie) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Scene().FindByMovieID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) CreatedAt(ctx context.Context, obj *models.Movie) (*time.Time, error) {
|
||||
return &obj.CreatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
@@ -254,3 +254,26 @@ func (r *performerResolver) CreatedAt(ctx context.Context, obj *models.Performer
|
||||
func (r *performerResolver) UpdatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
|
||||
return &obj.UpdatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().FindByPerformerID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Movie().CountByPerformerID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*models.SceneMovie, err error) {
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Scene()
|
||||
mqb := repo.Movie()
|
||||
|
||||
|
||||
@@ -58,6 +58,12 @@ func (r *sceneMarkerResolver) Preview(ctx context.Context, obj *models.SceneMark
|
||||
return urlbuilders.NewSceneURLBuilder(baseURL, sceneID).GetSceneMarkerStreamPreviewURL(obj.ID), nil
|
||||
}
|
||||
|
||||
func (r *sceneMarkerResolver) Screenshot(ctx context.Context, obj *models.SceneMarker) (string, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
sceneID := int(obj.SceneID.Int64)
|
||||
return urlbuilders.NewSceneURLBuilder(baseURL, sceneID).GetSceneMarkerStreamScreenshotURL(obj.ID), nil
|
||||
}
|
||||
|
||||
func (r *sceneMarkerResolver) CreatedAt(ctx context.Context, obj *models.SceneMarker) (*time.Time, error) {
|
||||
return &obj.CreatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *scrapedSceneTagResolver) StoredID(ctx context.Context, obj *models.ScrapedSceneTag) (*string, error) {
|
||||
return obj.ID, nil
|
||||
}
|
||||
|
||||
func (r *scrapedSceneMovieResolver) StoredID(ctx context.Context, obj *models.ScrapedSceneMovie) (*string, error) {
|
||||
return obj.ID, nil
|
||||
}
|
||||
|
||||
func (r *scrapedScenePerformerResolver) StoredID(ctx context.Context, obj *models.ScrapedScenePerformer) (*string, error) {
|
||||
return obj.ID, nil
|
||||
}
|
||||
|
||||
func (r *scrapedSceneStudioResolver) StoredID(ctx context.Context, obj *models.ScrapedSceneStudio) (*string, error) {
|
||||
return obj.ID, nil
|
||||
}
|
||||
@@ -39,12 +39,23 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
|
||||
|
||||
// indicate that image is missing by setting default query param to true
|
||||
if !hasImage {
|
||||
imagePath = imagePath + "?default=true"
|
||||
imagePath += "?default=true"
|
||||
}
|
||||
|
||||
return &imagePath, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) (ret []string, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().GetAliases(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
@@ -140,3 +151,26 @@ func (r *studioResolver) CreatedAt(ctx context.Context, obj *models.Studio) (*ti
|
||||
func (r *studioResolver) UpdatedAt(ctx context.Context, obj *models.Studio) (*time.Time, error) {
|
||||
return &obj.UpdatedAt.Timestamp, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().FindByStudioID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Movie().CountByStudioID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,28 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindByChildTagID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindByParentTagID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []string, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().GetAliases(obj.ID)
|
||||
|
||||
@@ -13,18 +13,21 @@ import (
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
var ErrOverriddenConfig = errors.New("cannot set overridden value")
|
||||
|
||||
func (r *mutationResolver) Setup(ctx context.Context, input models.SetupInput) (bool, error) {
|
||||
err := manager.GetInstance().Setup(input)
|
||||
err := manager.GetInstance().Setup(ctx, input)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) Migrate(ctx context.Context, input models.MigrateInput) (bool, error) {
|
||||
err := manager.GetInstance().Migrate(input)
|
||||
err := manager.GetInstance().Migrate(ctx, input)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.ConfigGeneralInput) (*models.ConfigGeneralResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
existingPaths := c.GetStashPaths()
|
||||
if len(input.Stashes) > 0 {
|
||||
for _, s := range input.Stashes {
|
||||
@@ -46,7 +49,20 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
c.Set(config.Stash, input.Stashes)
|
||||
}
|
||||
|
||||
if input.DatabasePath != nil {
|
||||
checkConfigOverride := func(key string) error {
|
||||
if c.HasOverride(key) {
|
||||
return fmt.Errorf("%w: %s", ErrOverriddenConfig, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
existingDBPath := c.GetDatabasePath()
|
||||
if input.DatabasePath != nil && existingDBPath != *input.DatabasePath {
|
||||
if err := checkConfigOverride(config.Database); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
ext := filepath.Ext(*input.DatabasePath)
|
||||
if ext != ".db" && ext != ".sqlite" && ext != ".sqlite3" {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("invalid database path, use extension db, sqlite, or sqlite3")
|
||||
@@ -54,14 +70,38 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
c.Set(config.Database, input.DatabasePath)
|
||||
}
|
||||
|
||||
if input.GeneratedPath != nil {
|
||||
existingGeneratedPath := c.GetGeneratedPath()
|
||||
if input.GeneratedPath != nil && existingGeneratedPath != *input.GeneratedPath {
|
||||
if err := checkConfigOverride(config.Generated); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
if err := utils.EnsureDir(*input.GeneratedPath); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
c.Set(config.Generated, input.GeneratedPath)
|
||||
}
|
||||
|
||||
if input.CachePath != nil {
|
||||
existingMetadataPath := c.GetMetadataPath()
|
||||
if input.MetadataPath != nil && existingMetadataPath != *input.MetadataPath {
|
||||
if err := checkConfigOverride(config.Metadata); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
if *input.MetadataPath != "" {
|
||||
if err := utils.EnsureDir(*input.MetadataPath); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
}
|
||||
c.Set(config.Metadata, input.MetadataPath)
|
||||
}
|
||||
|
||||
existingCachePath := c.GetCachePath()
|
||||
if input.CachePath != nil && existingCachePath != *input.CachePath {
|
||||
if err := checkConfigOverride(config.Metadata); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
if *input.CachePath != "" {
|
||||
if err := utils.EnsureDir(*input.CachePath); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
@@ -115,6 +155,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
c.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
|
||||
}
|
||||
|
||||
if input.WriteImageThumbnails != nil {
|
||||
c.Set(config.WriteImageThumbnails, *input.WriteImageThumbnails)
|
||||
}
|
||||
|
||||
if input.Username != nil {
|
||||
c.Set(config.Username, input.Username)
|
||||
}
|
||||
@@ -133,6 +177,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
c.Set(config.MaxSessionAge, *input.MaxSessionAge)
|
||||
}
|
||||
|
||||
if input.TrustedProxies != nil {
|
||||
c.Set(config.TrustedProxies, input.TrustedProxies)
|
||||
}
|
||||
|
||||
if input.LogFile != nil {
|
||||
c.Set(config.LogFile, input.LogFile)
|
||||
}
|
||||
@@ -208,17 +256,21 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
|
||||
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
setBool := func(key string, v *bool) {
|
||||
if v != nil {
|
||||
c.Set(key, *v)
|
||||
}
|
||||
}
|
||||
|
||||
if input.MenuItems != nil {
|
||||
c.Set(config.MenuItems, input.MenuItems)
|
||||
}
|
||||
|
||||
if input.SoundOnPreview != nil {
|
||||
c.Set(config.SoundOnPreview, *input.SoundOnPreview)
|
||||
}
|
||||
setBool(config.SoundOnPreview, input.SoundOnPreview)
|
||||
setBool(config.WallShowTitle, input.WallShowTitle)
|
||||
|
||||
if input.WallShowTitle != nil {
|
||||
c.Set(config.WallShowTitle, *input.WallShowTitle)
|
||||
}
|
||||
setBool(config.NoBrowser, input.NoBrowser)
|
||||
|
||||
if input.WallPlayback != nil {
|
||||
c.Set(config.WallPlayback, *input.WallPlayback)
|
||||
@@ -228,13 +280,10 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
||||
c.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
|
||||
}
|
||||
|
||||
if input.AutostartVideo != nil {
|
||||
c.Set(config.AutostartVideo, *input.AutostartVideo)
|
||||
}
|
||||
|
||||
if input.ShowStudioAsText != nil {
|
||||
c.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
|
||||
}
|
||||
setBool(config.AutostartVideo, input.AutostartVideo)
|
||||
setBool(config.ShowStudioAsText, input.ShowStudioAsText)
|
||||
setBool(config.AutostartVideoOnPlaySelected, input.AutostartVideoOnPlaySelected)
|
||||
setBool(config.ContinuePlaylistDefault, input.ContinuePlaylistDefault)
|
||||
|
||||
if input.Language != nil {
|
||||
c.Set(config.Language, *input.Language)
|
||||
@@ -252,8 +301,13 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
||||
|
||||
c.SetCSS(css)
|
||||
|
||||
if input.CSSEnabled != nil {
|
||||
c.Set(config.CSSEnabled, *input.CSSEnabled)
|
||||
setBool(config.CSSEnabled, input.CSSEnabled)
|
||||
|
||||
if input.DisableDropdownCreate != nil {
|
||||
ddc := input.DisableDropdownCreate
|
||||
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
|
||||
setBool(config.DisableDropdownCreateStudio, ddc.Studio)
|
||||
setBool(config.DisableDropdownCreateTag, ddc.Tag)
|
||||
}
|
||||
|
||||
if input.HandyKey != nil {
|
||||
@@ -289,7 +343,9 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input models.Confi
|
||||
if !*input.Enabled && dlnaService.IsRunning() {
|
||||
dlnaService.Stop(nil)
|
||||
} else if *input.Enabled && !dlnaService.IsRunning() {
|
||||
dlnaService.Start(nil)
|
||||
if err := dlnaService.Start(nil); err != nil {
|
||||
logger.Warnf("error starting DLNA service: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +387,28 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input models.C
|
||||
return makeConfigScrapingResult(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.ConfigDefaultSettingsInput) (*models.ConfigDefaultSettingsResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
if input.Identify != nil {
|
||||
c.Set(config.DefaultIdentifySettings, input.Identify)
|
||||
}
|
||||
|
||||
if input.DeleteFile != nil {
|
||||
c.Set(config.DeleteFileDefault, *input.DeleteFile)
|
||||
}
|
||||
|
||||
if input.DeleteGenerated != nil {
|
||||
c.Set(config.DeleteGeneratedDefault, *input.DeleteGenerated)
|
||||
}
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigDefaultsResult(), err
|
||||
}
|
||||
|
||||
return makeConfigDefaultsResult(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input models.GenerateAPIKeyInput) (string, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
|
||||
@@ -441,7 +441,7 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
return err
|
||||
}
|
||||
|
||||
if len(imgGalleries) == 0 {
|
||||
if len(imgGalleries) == 1 {
|
||||
if err := iqb.Destroy(img.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -465,13 +465,15 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
// if delete file is true, then delete the file as well
|
||||
// if it fails, just log a message
|
||||
if input.DeleteFile != nil && *input.DeleteFile {
|
||||
for _, gallery := range galleries {
|
||||
manager.DeleteGalleryFile(gallery)
|
||||
}
|
||||
|
||||
// #1804 - delete the image files first, since they must be removed
|
||||
// before deleting a folder
|
||||
for _, img := range imgsToDelete {
|
||||
manager.DeleteImageFile(img)
|
||||
}
|
||||
|
||||
for _, gallery := range galleries {
|
||||
manager.DeleteGalleryFile(gallery)
|
||||
}
|
||||
}
|
||||
|
||||
// if delete generated is true, then delete the generated files
|
||||
|
||||
@@ -2,7 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
@@ -89,6 +90,13 @@ func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.Aut
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataIdentify(ctx context.Context, input models.IdentifyMetadataInput) (string, error) {
|
||||
t := manager.CreateIdentifyJob(input)
|
||||
jobID := manager.GetInstance().JobManager.Add(ctx, "Identifying...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataClean(ctx context.Context, input models.CleanMetadataInput) (string, error) {
|
||||
jobID := manager.GetInstance().Clean(ctx, input)
|
||||
return strconv.Itoa(jobID), nil
|
||||
@@ -105,8 +113,10 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.Back
|
||||
mgr := manager.GetInstance()
|
||||
var backupPath string
|
||||
if download {
|
||||
utils.EnsureDir(mgr.Paths.Generated.Downloads)
|
||||
f, err := ioutil.TempFile(mgr.Paths.Generated.Downloads, "backup*.sqlite")
|
||||
if err := utils.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
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
if input.FrontImage != nil {
|
||||
frontimageData, err = utils.ProcessImageInput(*input.FrontImage)
|
||||
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
if input.BackImage != nil {
|
||||
backimageData, err = utils.ProcessImageInput(*input.BackImage)
|
||||
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
var frontimageData []byte
|
||||
frontImageIncluded := translator.hasField("front_image")
|
||||
if input.FrontImage != nil {
|
||||
frontimageData, err = utils.ProcessImageInput(*input.FrontImage)
|
||||
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
backImageIncluded := translator.hasField("back_image")
|
||||
var backimageData []byte
|
||||
if input.BackImage != nil {
|
||||
backimageData, err = utils.ProcessImageInput(*input.BackImage)
|
||||
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -202,7 +202,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
// HACK - if front image is null and back image is not null, then set the front image
|
||||
// to the default image since we can't have a null front image and a non-null back image
|
||||
if frontimageData == nil && backimageData != nil {
|
||||
frontimageData, _ = utils.ProcessImageInput(models.DefaultMovieImage)
|
||||
frontimageData, _ = utils.ProcessImageInput(ctx, models.DefaultMovieImage)
|
||||
}
|
||||
|
||||
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {
|
||||
|
||||
@@ -32,7 +32,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
var err error
|
||||
|
||||
if input.Image != nil {
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -178,7 +178,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
var err error
|
||||
imageIncluded := translator.hasField("image")
|
||||
if input.Image != nil {
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ 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: %s", err.Error())
|
||||
logger.Errorf("Error reading plugin configs: %v", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -32,7 +33,7 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
|
||||
|
||||
// Start the transaction and save the scene
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
ret, err = r.sceneUpdate(input, translator, repo)
|
||||
ret, err = r.sceneUpdate(ctx, input, translator, repo)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -52,7 +53,7 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
thisScene, err := r.sceneUpdate(*scene, translator, repo)
|
||||
thisScene, err := r.sceneUpdate(ctx, *scene, translator, repo)
|
||||
ret = append(ret, thisScene)
|
||||
|
||||
if err != nil {
|
||||
@@ -85,7 +86,7 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Scene, error) {
|
||||
func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Scene, error) {
|
||||
// Populate scene from the input
|
||||
sceneID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -110,7 +111,7 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator
|
||||
|
||||
if input.CoverImage != nil && *input.CoverImage != "" {
|
||||
var err error
|
||||
coverImageData, err = utils.ProcessImageInput(*input.CoverImage)
|
||||
coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,7 +120,7 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator
|
||||
}
|
||||
|
||||
qb := repo.Scene()
|
||||
scene, err := qb.Update(updatedScene)
|
||||
s, err := qb.Update(updatedScene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -169,13 +170,13 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator
|
||||
|
||||
// only update the cover image if provided and everything else was successful
|
||||
if coverImageData != nil {
|
||||
err = manager.SetSceneScreenshot(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
|
||||
err = scene.SetScreenshot(manager.GetInstance().Paths, s.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return scene, nil
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateScenePerformers(qb models.SceneReaderWriter, sceneID int, performerIDs []string) error {
|
||||
@@ -304,6 +305,18 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the movies
|
||||
if translator.hasField("movie_ids") {
|
||||
movies, err := adjustSceneMovieIDs(qb, sceneID, *input.MovieIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateMovies(sceneID, movies); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -395,6 +408,48 @@ func adjustSceneGalleryIDs(qb models.SceneReader, sceneID int, ids models.BulkUp
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustSceneMovieIDs(qb models.SceneReader, sceneID int, updateIDs models.BulkUpdateIds) ([]models.MoviesScenes, error) {
|
||||
existingMovies, err := qb.GetMovies(sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if we are setting the ids, just return the ids
|
||||
if updateIDs.Mode == models.BulkUpdateIDModeSet {
|
||||
existingMovies = []models.MoviesScenes{}
|
||||
for _, idStr := range updateIDs.Ids {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
existingMovies = append(existingMovies, models.MoviesScenes{MovieID: id})
|
||||
}
|
||||
|
||||
return existingMovies, nil
|
||||
}
|
||||
|
||||
for _, idStr := range updateIDs.Ids {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
|
||||
// look for the id in the list
|
||||
foundExisting := false
|
||||
for idx, existingMovie := range existingMovies {
|
||||
if existingMovie.MovieID == id {
|
||||
if updateIDs.Mode == models.BulkUpdateIDModeRemove {
|
||||
// remove from the list
|
||||
existingMovies = append(existingMovies[:idx], existingMovies[idx+1:]...)
|
||||
}
|
||||
|
||||
foundExisting = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundExisting && updateIDs.Mode != models.BulkUpdateIDModeRemove {
|
||||
existingMovies = append(existingMovies, models.MoviesScenes{MovieID: id})
|
||||
}
|
||||
}
|
||||
|
||||
return existingMovies, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
|
||||
sceneID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/studio"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
@@ -32,7 +34,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
if input.Image != nil {
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,19 +66,19 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
}
|
||||
|
||||
// Start the transaction and save the studio
|
||||
var studio *models.Studio
|
||||
var s *models.Studio
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Studio()
|
||||
|
||||
var err error
|
||||
studio, err = qb.Create(newStudio)
|
||||
s, err = qb.Create(newStudio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(studio.ID, imageData); err != nil {
|
||||
if err := qb.UpdateImage(s.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -84,7 +86,17 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(studio.ID, stashIDJoins); err != nil {
|
||||
if err := qb.UpdateStashIDs(s.ID, stashIDJoins); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(input.Aliases) > 0 {
|
||||
if err := studio.EnsureAliasesUnique(s.ID, input.Aliases, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateAliases(s.ID, input.Aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -94,8 +106,8 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, studio.ID, plugin.StudioCreatePost, input, nil)
|
||||
return r.getStudio(ctx, studio.ID)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, s.ID, plugin.StudioCreatePost, input, nil)
|
||||
return r.getStudio(ctx, s.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.StudioUpdateInput) (*models.Studio, error) {
|
||||
@@ -118,7 +130,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
imageIncluded := translator.hasField("image")
|
||||
if input.Image != nil {
|
||||
var err error
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -136,7 +148,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
|
||||
// Start the transaction and save the studio
|
||||
var studio *models.Studio
|
||||
var s *models.Studio
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Studio()
|
||||
|
||||
@@ -145,19 +157,19 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
}
|
||||
|
||||
var err error
|
||||
studio, err = qb.Update(updatedStudio)
|
||||
s, err = qb.Update(updatedStudio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(studio.ID, imageData); err != nil {
|
||||
if err := qb.UpdateImage(s.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if imageIncluded {
|
||||
// must be unsetting
|
||||
if err := qb.DestroyImage(studio.ID); err != nil {
|
||||
if err := qb.DestroyImage(s.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -170,13 +182,23 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("aliases") {
|
||||
if err := studio.EnsureAliasesUnique(studioID, input.Aliases, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateAliases(studioID, input.Aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, studio.ID, plugin.StudioUpdatePost, input, translator.getFields())
|
||||
return r.getStudio(ctx, studio.ID)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, s.ID, plugin.StudioUpdatePost, input, translator.getFields())
|
||||
return r.getStudio(ctx, s.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StudioDestroy(ctx context.Context, input models.StudioDestroyInput) (bool, error) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/tag"
|
||||
@@ -36,14 +37,31 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
|
||||
var err error
|
||||
|
||||
if input.Image != nil {
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Start the transaction and save the t
|
||||
var parentIDs []int
|
||||
var childIDs []int
|
||||
|
||||
if len(input.ParentIds) > 0 {
|
||||
parentIDs, err = utils.StringSliceToIntSlice(input.ParentIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(input.ChildIds) > 0 {
|
||||
childIDs, err = utils.StringSliceToIntSlice(input.ChildIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Start the transaction and save the tag
|
||||
var t *models.Tag
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Tag()
|
||||
@@ -75,6 +93,26 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
|
||||
}
|
||||
}
|
||||
|
||||
if len(parentIDs) > 0 {
|
||||
if err := qb.UpdateParentTags(t.ID, parentIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(childIDs) > 0 {
|
||||
if err := qb.UpdateChildTags(t.ID, childIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This should be called before any changes are made, but
|
||||
// requires a rewrite of ValidateHierarchy.
|
||||
if len(parentIDs) > 0 || len(childIDs) > 0 {
|
||||
if err := tag.ValidateHierarchy(t, parentIDs, childIDs, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -99,13 +137,30 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
||||
|
||||
imageIncluded := translator.hasField("image")
|
||||
if input.Image != nil {
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var parentIDs []int
|
||||
var childIDs []int
|
||||
|
||||
if translator.hasField("parent_ids") {
|
||||
parentIDs, err = utils.StringSliceToIntSlice(input.ParentIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("child_ids") {
|
||||
childIDs, err = utils.StringSliceToIntSlice(input.ChildIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Start the transaction and save the tag
|
||||
var t *models.Tag
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
@@ -161,6 +216,27 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
||||
}
|
||||
}
|
||||
|
||||
if parentIDs != nil {
|
||||
if err := qb.UpdateParentTags(tagID, parentIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if childIDs != nil {
|
||||
if err := qb.UpdateChildTags(tagID, childIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This should be called before any changes are made, but
|
||||
// requires a rewrite of ValidateHierarchy.
|
||||
if parentIDs != nil || childIDs != nil {
|
||||
if err := tag.ValidateHierarchy(t, parentIDs, childIDs, qb); err != nil {
|
||||
logger.Errorf("Error saving tag: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -242,10 +318,30 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input models.TagsMerge
|
||||
return fmt.Errorf("Tag with ID %d not found", destination)
|
||||
}
|
||||
|
||||
parents, children, err := tag.MergeHierarchy(destination, source, qb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = qb.Merge(source, destination); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = qb.UpdateParentTags(destination, parents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = qb.UpdateChildTags(destination, children)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tag.ValidateHierarchy(t, parents, children, qb)
|
||||
if err != nil {
|
||||
logger.Errorf("Error merging tag: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -6,23 +6,26 @@ import (
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"golang.org/x/text/collate"
|
||||
)
|
||||
|
||||
func (r *queryResolver) Configuration(ctx context.Context) (*models.ConfigResult, error) {
|
||||
return makeConfigResult(), nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Directory(ctx context.Context, path *string) (*models.Directory, error) {
|
||||
func (r *queryResolver) Directory(ctx context.Context, path, locale *string) (*models.Directory, error) {
|
||||
|
||||
directory := &models.Directory{}
|
||||
var err error
|
||||
|
||||
col := newCollator(locale, collate.IgnoreCase, collate.Numeric)
|
||||
|
||||
var dirPath = ""
|
||||
if path != nil {
|
||||
dirPath = *path
|
||||
}
|
||||
currentDir := utils.GetDir(dirPath)
|
||||
directories, err := utils.ListDir(currentDir)
|
||||
directories, err := utils.ListDir(col, currentDir)
|
||||
if err != nil {
|
||||
return directory, err
|
||||
}
|
||||
@@ -40,6 +43,7 @@ func makeConfigResult() *models.ConfigResult {
|
||||
Interface: makeConfigInterfaceResult(),
|
||||
Dlna: makeConfigDLNAResult(),
|
||||
Scraping: makeConfigScrapingResult(),
|
||||
Defaults: makeConfigDefaultsResult(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +63,8 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
ConfigFilePath: config.GetConfigFilePath(),
|
||||
MetadataPath: config.GetMetadataPath(),
|
||||
ConfigFilePath: config.GetConfigFile(),
|
||||
ScrapersPath: config.GetScrapersPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
CalculateMd5: config.IsCalculateMD5(),
|
||||
@@ -73,10 +78,12 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
PreviewPreset: config.GetPreviewPreset(),
|
||||
MaxTranscodeSize: &maxTranscodeSize,
|
||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||
WriteImageThumbnails: config.IsWriteImageThumbnails(),
|
||||
APIKey: config.GetAPIKey(),
|
||||
Username: config.GetUsername(),
|
||||
Password: config.GetPasswordHash(),
|
||||
MaxSessionAge: config.GetMaxSessionAge(),
|
||||
TrustedProxies: config.GetTrustedProxies(),
|
||||
LogFile: &logFile,
|
||||
LogOut: config.GetLogOut(),
|
||||
LogLevel: config.GetLogLevel(),
|
||||
@@ -101,8 +108,11 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
soundOnPreview := config.GetSoundOnPreview()
|
||||
wallShowTitle := config.GetWallShowTitle()
|
||||
wallPlayback := config.GetWallPlayback()
|
||||
noBrowser := config.GetNoBrowser()
|
||||
maximumLoopDuration := config.GetMaximumLoopDuration()
|
||||
autostartVideo := config.GetAutostartVideo()
|
||||
autostartVideoOnPlaySelected := config.GetAutostartVideoOnPlaySelected()
|
||||
continuePlaylistDefault := config.GetContinuePlaylistDefault()
|
||||
showStudioAsText := config.GetShowStudioAsText()
|
||||
css := config.GetCSS()
|
||||
cssEnabled := config.GetCSSEnabled()
|
||||
@@ -112,19 +122,23 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
scriptOffset := config.GetFunscriptOffset()
|
||||
|
||||
return &models.ConfigInterfaceResult{
|
||||
MenuItems: menuItems,
|
||||
SoundOnPreview: &soundOnPreview,
|
||||
WallShowTitle: &wallShowTitle,
|
||||
WallPlayback: &wallPlayback,
|
||||
MaximumLoopDuration: &maximumLoopDuration,
|
||||
AutostartVideo: &autostartVideo,
|
||||
ShowStudioAsText: &showStudioAsText,
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
Language: &language,
|
||||
SlideshowDelay: &slideshowDelay,
|
||||
HandyKey: &handyKey,
|
||||
FunscriptOffset: &scriptOffset,
|
||||
MenuItems: menuItems,
|
||||
SoundOnPreview: &soundOnPreview,
|
||||
WallShowTitle: &wallShowTitle,
|
||||
WallPlayback: &wallPlayback,
|
||||
MaximumLoopDuration: &maximumLoopDuration,
|
||||
NoBrowser: &noBrowser,
|
||||
AutostartVideo: &autostartVideo,
|
||||
ShowStudioAsText: &showStudioAsText,
|
||||
AutostartVideoOnPlaySelected: &autostartVideoOnPlaySelected,
|
||||
ContinuePlaylistDefault: &continuePlaylistDefault,
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
Language: &language,
|
||||
SlideshowDelay: &slideshowDelay,
|
||||
DisabledDropdownCreate: config.GetDisableDropdownCreate(),
|
||||
HandyKey: &handyKey,
|
||||
FunscriptOffset: &scriptOffset,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,3 +166,15 @@ func makeConfigScrapingResult() *models.ConfigScrapingResult {
|
||||
ExcludeTagPatterns: config.GetScraperExcludeTagPatterns(),
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult {
|
||||
config := config.GetInstance()
|
||||
deleteFileDefault := config.GetDeleteFileDefault()
|
||||
deleteGeneratedDefault := config.GetDeleteGeneratedDefault()
|
||||
|
||||
return &models.ConfigDefaultSettingsResult{
|
||||
Identify: config.GetDefaultIdentifySettings(),
|
||||
DeleteFile: &deleteFileDefault,
|
||||
DeleteGenerated: &deleteGeneratedDefault,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
|
||||
@@ -39,14 +41,32 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
|
||||
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *models.FindImagesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Image()
|
||||
images, total, err := qb.Query(imageFilter, filter)
|
||||
|
||||
fields := graphql.CollectAllFields(ctx)
|
||||
|
||||
result, err := qb.Query(models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: utils.StrInclude(fields, "count"),
|
||||
},
|
||||
ImageFilter: imageFilter,
|
||||
Megapixels: utils.StrInclude(fields, "megapixels"),
|
||||
TotalSize: utils.StrInclude(fields, "filesize"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
images, err := result.Resolve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindImagesResultType{
|
||||
Count: total,
|
||||
Images: images,
|
||||
Count: result.Count,
|
||||
Images: images,
|
||||
Megapixels: result.Megapixels,
|
||||
Filesize: result.TotalSize,
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
|
||||
@@ -65,16 +67,34 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input models.SceneH
|
||||
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var scenes []*models.Scene
|
||||
var total int
|
||||
var err error
|
||||
|
||||
fields := graphql.CollectAllFields(ctx)
|
||||
result := &models.SceneQueryResult{}
|
||||
|
||||
if len(sceneIDs) > 0 {
|
||||
scenes, err = repo.Scene().FindMany(sceneIDs)
|
||||
if err == nil {
|
||||
total = len(scenes)
|
||||
result.Count = len(scenes)
|
||||
for _, s := range scenes {
|
||||
result.TotalDuration += s.Duration.Float64
|
||||
size, _ := strconv.ParseFloat(s.Size.String, 64)
|
||||
result.TotalSize += size
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scenes, total, err = repo.Scene().Query(sceneFilter, filter)
|
||||
result, err = repo.Scene().Query(models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: utils.StrInclude(fields, "count"),
|
||||
},
|
||||
SceneFilter: sceneFilter,
|
||||
TotalDuration: utils.StrInclude(fields, "duration"),
|
||||
TotalSize: utils.StrInclude(fields, "filesize"),
|
||||
})
|
||||
if err == nil {
|
||||
scenes, err = result.Resolve()
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -82,8 +102,10 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
|
||||
}
|
||||
|
||||
ret = &models.FindScenesResultType{
|
||||
Count: total,
|
||||
Scenes: scenes,
|
||||
Count: result.Count,
|
||||
Scenes: scenes,
|
||||
Duration: result.TotalDuration,
|
||||
Filesize: result.TotalSize,
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -114,14 +136,31 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
|
||||
queryFilter.Q = nil
|
||||
}
|
||||
|
||||
scenes, total, err := repo.Scene().Query(sceneFilter, queryFilter)
|
||||
fields := graphql.CollectAllFields(ctx)
|
||||
|
||||
result, err := repo.Scene().Query(models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: queryFilter,
|
||||
Count: utils.StrInclude(fields, "count"),
|
||||
},
|
||||
SceneFilter: sceneFilter,
|
||||
TotalDuration: utils.StrInclude(fields, "duration"),
|
||||
TotalSize: utils.StrInclude(fields, "filesize"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenes, err := result.Resolve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindScenesResultType{
|
||||
Count: total,
|
||||
Scenes: scenes,
|
||||
Count: result.Count,
|
||||
Scenes: scenes,
|
||||
Duration: result.TotalDuration,
|
||||
Filesize: result.TotalSize,
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,7 +2,9 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
@@ -29,8 +31,9 @@ func (r *queryResolver) ScrapeFreeonesPerformerList(ctx context.Context, query s
|
||||
|
||||
var ret []string
|
||||
for _, v := range scrapedPerformers {
|
||||
name := v.Name
|
||||
ret = append(ret, *name)
|
||||
if v.Name != nil {
|
||||
ret = append(ret, *v.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
@@ -68,8 +71,21 @@ func (r *queryResolver) ScrapePerformerURL(ctx context.Context, url string) (*mo
|
||||
return manager.GetInstance().ScraperCache.ScrapePerformerURL(url)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string, query string) ([]*models.ScrapedScene, error) {
|
||||
if query == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return manager.GetInstance().ScraperCache.ScrapeSceneQuery(scraperID, query)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeScene(ctx context.Context, scraperID string, scene models.SceneUpdateInput) (*models.ScrapedScene, error) {
|
||||
return manager.GetInstance().ScraperCache.ScrapeScene(scraperID, scene)
|
||||
id, err := strconv.Atoi(scene.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return manager.GetInstance().ScraperCache.ScrapeScene(scraperID, id)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*models.ScrapedScene, error) {
|
||||
@@ -77,7 +93,12 @@ func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*models
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeGallery(ctx context.Context, scraperID string, gallery models.GalleryUpdateInput) (*models.ScrapedGallery, error) {
|
||||
return manager.GetInstance().ScraperCache.ScrapeGallery(scraperID, gallery)
|
||||
id, err := strconv.Atoi(gallery.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return manager.GetInstance().ScraperCache.ScrapeGallery(scraperID, id)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*models.ScrapedGallery, error) {
|
||||
@@ -98,11 +119,11 @@ func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.Sta
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
|
||||
|
||||
if len(input.SceneIds) > 0 {
|
||||
return client.FindStashBoxScenesByFingerprints(input.SceneIds)
|
||||
return client.FindStashBoxScenesByFingerprintsFlat(input.SceneIds)
|
||||
}
|
||||
|
||||
if input.Q != nil {
|
||||
return client.QueryStashBoxScene(*input.Q)
|
||||
return client.QueryStashBoxScene(ctx, *input.Q)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
@@ -127,3 +148,180 @@ func (r *queryResolver) QueryStashBoxPerformer(ctx context.Context, input models
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if index < 0 || index >= len(boxes) {
|
||||
return nil, fmt.Errorf("invalid stash_box_index %d", index)
|
||||
}
|
||||
|
||||
return stashbox.NewClient(*boxes[index], r.txnManager), nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeSingleSceneInput) ([]*models.ScrapedScene, error) {
|
||||
if source.ScraperID != nil {
|
||||
var singleScene *models.ScrapedScene
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case input.SceneID != nil:
|
||||
var sceneID int
|
||||
sceneID, err = strconv.Atoi(*input.SceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
singleScene, err = manager.GetInstance().ScraperCache.ScrapeScene(*source.ScraperID, sceneID)
|
||||
case input.SceneInput != nil:
|
||||
singleScene, err = manager.GetInstance().ScraperCache.ScrapeSceneFragment(*source.ScraperID, *input.SceneInput)
|
||||
case input.Query != nil:
|
||||
return manager.GetInstance().ScraperCache.ScrapeSceneQuery(*source.ScraperID, *input.Query)
|
||||
default:
|
||||
err = errors.New("scene_id, scene_input or query must be set")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if singleScene != nil {
|
||||
return []*models.ScrapedScene{singleScene}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
} else if source.StashBoxIndex != nil {
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if input.SceneID != nil {
|
||||
return client.FindStashBoxScenesByFingerprintsFlat([]string{*input.SceneID})
|
||||
} else if input.Query != nil {
|
||||
return client.QueryStashBoxScene(ctx, *input.Query)
|
||||
}
|
||||
|
||||
return nil, errors.New("scene_id or query must be set")
|
||||
}
|
||||
|
||||
return nil, errors.New("scraper_id or stash_box_index must be set")
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeMultiScenesInput) ([][]*models.ScrapedScene, error) {
|
||||
if source.ScraperID != nil {
|
||||
return nil, ErrNotImplemented
|
||||
} else if source.StashBoxIndex != nil {
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.FindStashBoxScenesByFingerprints(input.SceneIds)
|
||||
}
|
||||
|
||||
return nil, errors.New("scraper_id or stash_box_index must be set")
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeSinglePerformerInput) ([]*models.ScrapedPerformer, error) {
|
||||
if source.ScraperID != nil {
|
||||
if input.PerformerInput != nil {
|
||||
singlePerformer, err := manager.GetInstance().ScraperCache.ScrapePerformer(*source.ScraperID, *input.PerformerInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if singlePerformer != nil {
|
||||
return []*models.ScrapedPerformer{singlePerformer}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if input.Query != nil {
|
||||
return manager.GetInstance().ScraperCache.ScrapePerformerList(*source.ScraperID, *input.Query)
|
||||
}
|
||||
|
||||
return nil, ErrNotImplemented
|
||||
} else if source.StashBoxIndex != nil {
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []*models.StashBoxPerformerQueryResult
|
||||
switch {
|
||||
case input.PerformerID != nil:
|
||||
ret, err = client.FindStashBoxPerformersByNames([]string{*input.PerformerID})
|
||||
case input.Query != nil:
|
||||
ret, err = client.QueryStashBoxPerformer(*input.Query)
|
||||
default:
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ret) > 0 {
|
||||
return ret[0].Results, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("scraper_id or stash_box_index must be set")
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeMultiPerformersInput) ([][]*models.ScrapedPerformer, error) {
|
||||
if source.ScraperID != nil {
|
||||
return nil, ErrNotImplemented
|
||||
} else if source.StashBoxIndex != nil {
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.FindStashBoxPerformersByPerformerNames(input.PerformerIds)
|
||||
}
|
||||
|
||||
return nil, errors.New("scraper_id or stash_box_index must be set")
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeSingleGalleryInput) ([]*models.ScrapedGallery, error) {
|
||||
if source.ScraperID != nil {
|
||||
var singleGallery *models.ScrapedGallery
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case input.GalleryID != nil:
|
||||
var galleryID int
|
||||
galleryID, err = strconv.Atoi(*input.GalleryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
singleGallery, err = manager.GetInstance().ScraperCache.ScrapeGallery(*source.ScraperID, galleryID)
|
||||
case input.GalleryInput != nil:
|
||||
singleGallery, err = manager.GetInstance().ScraperCache.ScrapeGalleryFragment(*source.ScraperID, *input.GalleryInput)
|
||||
default:
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if singleGallery != nil {
|
||||
return []*models.ScrapedGallery{singleGallery}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
} else if source.StashBoxIndex != nil {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
return nil, errors.New("scraper_id must be set")
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleMovie(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeSingleMovieInput) ([]*models.ScrapedMovie, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
@@ -2,32 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type throttledUpdate struct {
|
||||
id int
|
||||
pendingUpdate *job.Job
|
||||
lastUpdate time.Time
|
||||
broadcastTimer *time.Timer
|
||||
killTimer *time.Timer
|
||||
}
|
||||
|
||||
func (tu *throttledUpdate) broadcast(output chan *models.JobStatusUpdate) {
|
||||
tu.lastUpdate = time.Now()
|
||||
output <- &models.JobStatusUpdate{
|
||||
Type: models.JobStatusUpdateTypeUpdate,
|
||||
Job: jobToJobModel(*tu.pendingUpdate),
|
||||
}
|
||||
|
||||
tu.broadcastTimer = nil
|
||||
tu.pendingUpdate = nil
|
||||
}
|
||||
|
||||
func makeJobStatusUpdate(t models.JobStatusUpdateType, j job.Job) *models.JobStatusUpdate {
|
||||
return &models.JobStatusUpdate{
|
||||
Type: t,
|
||||
|
||||
@@ -8,20 +8,22 @@ import (
|
||||
)
|
||||
|
||||
func getLogLevel(logType string) models.LogLevel {
|
||||
if logType == "progress" {
|
||||
switch logType {
|
||||
case "progress":
|
||||
return models.LogLevelProgress
|
||||
} else if logType == "debug" {
|
||||
case "trace":
|
||||
return models.LogLevelTrace
|
||||
case "debug":
|
||||
return models.LogLevelDebug
|
||||
} else if logType == "info" {
|
||||
case "info":
|
||||
return models.LogLevelInfo
|
||||
} else if logType == "warn" {
|
||||
case "warn":
|
||||
return models.LogLevelWarning
|
||||
} else if logType == "error" {
|
||||
case "error":
|
||||
return models.LogLevelError
|
||||
default:
|
||||
return models.LogLevelDebug
|
||||
}
|
||||
|
||||
// default to debug
|
||||
return models.LogLevelDebug
|
||||
}
|
||||
|
||||
func logEntriesFromLogItems(logItems []logger.LogItem) []*models.LogEntry {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -32,15 +33,35 @@ func (rs imageRoutes) Routes() chi.Router {
|
||||
// region Handlers
|
||||
|
||||
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
image := r.Context().Value(imageKey).(*models.Image)
|
||||
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(image.Checksum, models.DefaultGthumbWidth)
|
||||
img := r.Context().Value(imageKey).(*models.Image)
|
||||
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
|
||||
|
||||
// if the thumbnail doesn't exist, fall back to the original file
|
||||
w.Header().Add("Cache-Control", "max-age=604800000")
|
||||
|
||||
// if the thumbnail doesn't exist, encode on the fly
|
||||
exists, _ := utils.FileExists(filepath)
|
||||
if exists {
|
||||
http.ServeFile(w, r, filepath)
|
||||
} else {
|
||||
rs.Image(w, r)
|
||||
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMPEG)
|
||||
data, err := encoder.GetThumbnail(img, models.DefaultGthumbWidth)
|
||||
if err != nil {
|
||||
logger.Errorf("error generating thumbnail for image: %s", err.Error())
|
||||
|
||||
// backwards compatibility - fallback to original image instead
|
||||
rs.Image(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// write the generated thumbnail to disk if enabled
|
||||
if manager.GetInstance().Config.IsWriteImageThumbnails() {
|
||||
if err := utils.WriteFile(filepath, data); err != nil {
|
||||
logger.Errorf("error writing thumbnail for image %s: %s", img.Path, err)
|
||||
}
|
||||
}
|
||||
if n, err := w.Write(data); err != nil {
|
||||
logger.Errorf("error writing thumbnail response. Wrote %v bytes: %v", n, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +80,7 @@ func ImageCtx(next http.Handler) http.Handler {
|
||||
imageID, _ := strconv.Atoi(imageIdentifierQueryParam)
|
||||
|
||||
var image *models.Image
|
||||
manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
readTxnErr := manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
qb := repo.Image()
|
||||
if imageID == 0 {
|
||||
image, _ = qb.FindByChecksum(imageIdentifierQueryParam)
|
||||
@@ -69,6 +90,9 @@ func ImageCtx(next http.Handler) http.Handler {
|
||||
|
||||
return nil
|
||||
})
|
||||
if readTxnErr != nil {
|
||||
logger.Warnf("read transaction failure while trying to read image by id: %v", readTxnErr)
|
||||
}
|
||||
|
||||
if image == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -32,17 +33,22 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
|
||||
defaultParam := r.URL.Query().Get("default")
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
image, _ = repo.Movie().GetFrontImage(movie.ID)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warnf("read transaction error while getting front image: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(image) == 0 {
|
||||
_, image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
|
||||
}
|
||||
|
||||
utils.ServeImage(image, w, r)
|
||||
if err := utils.ServeImage(image, w, r); err != nil {
|
||||
logger.Warnf("error serving front image: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -50,17 +56,22 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
|
||||
defaultParam := r.URL.Query().Get("default")
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
image, _ = repo.Movie().GetBackImage(movie.ID)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warnf("read transaction error on fetch back image: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(image) == 0 {
|
||||
_, image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
|
||||
}
|
||||
|
||||
utils.ServeImage(image, w, r)
|
||||
if err := utils.ServeImage(image, w, r); err != nil {
|
||||
logger.Warnf("error while serving image: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func MovieCtx(next http.Handler) http.Handler {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -33,17 +34,22 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
readTxnErr := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
image, _ = repo.Performer().GetImage(performer.ID)
|
||||
return nil
|
||||
})
|
||||
if readTxnErr != nil {
|
||||
logger.Warnf("couldn't execute getting a performer image from read transaction: %v", readTxnErr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(image) == 0 || defaultParam == "true" {
|
||||
image, _ = getRandomPerformerImageUsingName(performer.Name.String, performer.Gender.String, config.GetInstance().GetCustomPerformerImageLocation())
|
||||
}
|
||||
|
||||
utils.ServeImage(image, w, r)
|
||||
if err := utils.ServeImage(image, w, r); err != nil {
|
||||
logger.Warnf("error serving image: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func PerformerCtx(next http.Handler) http.Handler {
|
||||
|
||||
@@ -16,8 +16,7 @@ import (
|
||||
)
|
||||
|
||||
type sceneRoutes struct {
|
||||
txnManager models.TransactionManager
|
||||
sceneServer manager.SceneServer
|
||||
txnManager models.TransactionManager
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) Routes() chi.Router {
|
||||
@@ -42,6 +41,7 @@ func (rs sceneRoutes) Routes() chi.Router {
|
||||
|
||||
r.Get("/scene_marker/{sceneMarkerId}/stream", rs.SceneMarkerStream)
|
||||
r.Get("/scene_marker/{sceneMarkerId}/preview", rs.SceneMarkerPreview)
|
||||
r.Get("/scene_marker/{sceneMarkerId}/screenshot", rs.SceneMarkerScreenshot)
|
||||
})
|
||||
r.With(SceneCtx).Get("/{sceneId}_thumbs.vtt", rs.VttThumbs)
|
||||
r.With(SceneCtx).Get("/{sceneId}_sprite.jpg", rs.VttSprite)
|
||||
@@ -57,9 +57,10 @@ func getSceneFileContainer(scene *models.Scene) ffmpeg.Container {
|
||||
container = ffmpeg.Container(scene.Format.String)
|
||||
} else { // container isn't in the DB
|
||||
// shouldn't happen, fallback to ffprobe
|
||||
tmpVideoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
|
||||
ffprobe := manager.GetInstance().FFProbe
|
||||
tmpVideoFile, err := ffprobe.NewVideoFile(scene.Path, false)
|
||||
if err != nil {
|
||||
logger.Errorf("[transcode] error reading video file: %s", err.Error())
|
||||
logger.Errorf("[transcode] error reading video file: %v", err)
|
||||
return ffmpeg.Container("")
|
||||
}
|
||||
|
||||
@@ -85,7 +86,9 @@ func (rs sceneRoutes) StreamMKV(w http.ResponseWriter, r *http.Request) {
|
||||
container := getSceneFileContainer(scene)
|
||||
if container != ffmpeg.Matroska {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("not an mkv file"))
|
||||
if _, err := w.Write([]byte("not an mkv file")); err != nil {
|
||||
logger.Warnf("[stream] error writing to stream: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,9 +106,10 @@ func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) {
|
||||
func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
|
||||
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
|
||||
ffprobe := manager.GetInstance().FFProbe
|
||||
videoFile, err := ffprobe.NewVideoFile(scene.Path, false)
|
||||
if err != nil {
|
||||
logger.Errorf("[stream] error reading video file: %s", err.Error())
|
||||
logger.Errorf("[stream] error reading video file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -126,7 +130,9 @@ func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
|
||||
rangeStr := requestByteRange.ToHeaderValue(int64(str.Len()))
|
||||
w.Header().Set("Content-Range", rangeStr)
|
||||
|
||||
w.Write(ret)
|
||||
if n, err := w.Write(ret); err != nil {
|
||||
logger.Warnf("[stream] error writing stream (wrote %v bytes): %v", n, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) StreamTS(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -138,15 +144,18 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
|
||||
// needs to be transcoded
|
||||
|
||||
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
|
||||
ffprobe := manager.GetInstance().FFProbe
|
||||
videoFile, err := ffprobe.NewVideoFile(scene.Path, false)
|
||||
if err != nil {
|
||||
logger.Errorf("[stream] error reading video file: %s", err.Error())
|
||||
logger.Errorf("[stream] error reading video file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// start stream based on query param, if provided
|
||||
r.ParseForm()
|
||||
if err = r.ParseForm(); err != nil {
|
||||
logger.Warnf("[stream] error parsing query form: %v", err)
|
||||
}
|
||||
|
||||
startTime := r.Form.Get("start")
|
||||
requestedSize := r.Form.Get("resolution")
|
||||
|
||||
@@ -164,13 +173,15 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
|
||||
options.MaxTranscodeSize = models.StreamingResolutionEnum(requestedSize)
|
||||
}
|
||||
|
||||
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
|
||||
encoder := manager.GetInstance().FFMPEG
|
||||
stream, err = encoder.GetTranscodeStream(options)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("[stream] error transcoding video file: %s", err.Error())
|
||||
logger.Errorf("[stream] error transcoding video file: %v", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(err.Error()))
|
||||
if _, err := w.Write([]byte(err.Error())); err != nil {
|
||||
logger.Warnf("[stream] error writing response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -289,6 +300,12 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if sceneMarker == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
|
||||
http.ServeFile(w, r, filepath)
|
||||
}
|
||||
@@ -306,6 +323,12 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if sceneMarker == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPreviewImagePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
|
||||
|
||||
// If the image doesn't exist, send the placeholder
|
||||
@@ -320,6 +343,39 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
|
||||
http.ServeFile(w, r, filepath)
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
|
||||
var sceneMarker *models.SceneMarker
|
||||
if err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
sceneMarker, err = repo.SceneMarker().Find(sceneMarkerID)
|
||||
return err
|
||||
}); err != nil {
|
||||
logger.Warnf("Error when getting scene marker for stream: %s", err.Error())
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if sceneMarker == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
|
||||
|
||||
// If the image doesn't exist, send the placeholder
|
||||
exists, _ := utils.FileExists(filepath)
|
||||
if !exists {
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_, _ = w.Write(utils.PendingGenerateResource)
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, filepath)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
func SceneCtx(next http.Handler) http.Handler {
|
||||
@@ -328,7 +384,7 @@ func SceneCtx(next http.Handler) http.Handler {
|
||||
sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam)
|
||||
|
||||
var scene *models.Scene
|
||||
manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
readTxnErr := manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
qb := repo.Scene()
|
||||
if sceneID == 0 {
|
||||
// determine checksum/os by the length of the query param
|
||||
@@ -343,6 +399,9 @@ func SceneCtx(next http.Handler) http.Handler {
|
||||
|
||||
return nil
|
||||
})
|
||||
if readTxnErr != nil {
|
||||
logger.Warnf("error executing SceneCtx transaction: %v", readTxnErr)
|
||||
}
|
||||
|
||||
if scene == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
|
||||
@@ -2,10 +2,13 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -32,17 +35,27 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
image, _ = repo.Studio().GetImage(studio.ID)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warnf("read transaction error while fetching studio image: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(image) == 0 {
|
||||
_, image, _ = utils.ProcessBase64Image(models.DefaultStudioImage)
|
||||
}
|
||||
|
||||
utils.ServeImage(image, w, r)
|
||||
if err := utils.ServeImage(image, w, r); err != nil {
|
||||
// Broken pipe errors are common when serving images and the remote
|
||||
// connection closes the connection. Filter them out of the error
|
||||
// messages, as they are benign.
|
||||
if !errors.Is(err, syscall.EPIPE) {
|
||||
logger.Warnf("cannot serve studio image: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StudioCtx(next http.Handler) http.Handler {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -32,17 +33,22 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
image, _ = repo.Tag().GetImage(tag.ID)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warnf("read transaction error while getting tag image: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(image) == 0 {
|
||||
image = models.DefaultTagImage
|
||||
}
|
||||
|
||||
utils.ServeImage(image, w, r)
|
||||
if err := utils.ServeImage(image, w, r); err != nil {
|
||||
logger.Warnf("error serving tag image: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TagCtx(next http.Handler) http.Handler {
|
||||
|
||||
@@ -3,11 +3,12 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
@@ -21,100 +22,22 @@ import (
|
||||
gqlPlayground "github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/gobuffalo/packr/v2"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/browser"
|
||||
"github.com/rs/cors"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/session"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
var version string
|
||||
var buildstamp string
|
||||
var githash string
|
||||
var officialBuild string
|
||||
|
||||
var uiBox *packr.Box
|
||||
|
||||
//var legacyUiBox *packr.Box
|
||||
var loginUIBox *packr.Box
|
||||
|
||||
func allowUnauthenticated(r *http.Request) bool {
|
||||
return strings.HasPrefix(r.URL.Path, "/login") || r.URL.Path == "/css"
|
||||
}
|
||||
|
||||
func authenticateHandler() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := manager.GetInstance().SessionStore.Authenticate(w, r)
|
||||
if err != nil {
|
||||
if err != session.ErrUnauthorized {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, err = w.Write([]byte(err.Error()))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// unauthorized error
|
||||
w.Header().Add("WWW-Authenticate", `FormBased`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
c := config.GetInstance()
|
||||
ctx := r.Context()
|
||||
|
||||
// handle redirect if no user and user is required
|
||||
if userID == "" && c.HasCredentials() && !allowUnauthenticated(r) {
|
||||
// if we don't have a userID, then redirect
|
||||
// if graphql was requested, we just return a forbidden error
|
||||
if r.URL.Path == "/graphql" {
|
||||
w.Header().Add("WWW-Authenticate", `FormBased`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise redirect to the login page
|
||||
u := url.URL{
|
||||
Path: "/login",
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set(returnURLParam, r.URL.Path)
|
||||
u.RawQuery = q.Encode()
|
||||
http.Redirect(w, r, u.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = session.SetCurrentUserID(ctx, userID)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func visitedPluginHandler() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// get the visited plugins and set them in the context
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const loginEndPoint = "/login"
|
||||
|
||||
func Start() {
|
||||
uiBox = packr.New("UI Box", "../../ui/v2.5/build")
|
||||
//legacyUiBox = packr.New("UI Box", "../../ui/v1/dist/stash-frontend")
|
||||
loginUIBox = packr.New("Login UI Box", "../../ui/login")
|
||||
|
||||
func Start(uiBox embed.FS, loginUIBox embed.FS) {
|
||||
initialiseImages()
|
||||
|
||||
r := chi.NewRouter()
|
||||
@@ -182,10 +105,10 @@ func Start() {
|
||||
r.HandleFunc("/playground", gqlPlayground.Handler("GraphQL playground", "/graphql"))
|
||||
|
||||
// session handlers
|
||||
r.Post(loginEndPoint, handleLogin)
|
||||
r.Get("/logout", handleLogout)
|
||||
r.Post(loginEndPoint, handleLogin(loginUIBox))
|
||||
r.Get("/logout", handleLogout(loginUIBox))
|
||||
|
||||
r.Get(loginEndPoint, getLoginHandler)
|
||||
r.Get(loginEndPoint, getLoginHandler(loginUIBox))
|
||||
|
||||
r.Mount("/performer", performerRoutes{
|
||||
txnManager: txnManager,
|
||||
@@ -226,11 +149,18 @@ func Start() {
|
||||
r.HandleFunc("/login*", func(w http.ResponseWriter, r *http.Request) {
|
||||
ext := path.Ext(r.URL.Path)
|
||||
if ext == ".html" || ext == "" {
|
||||
data, _ := loginUIBox.Find("login.html")
|
||||
_, _ = w.Write(data)
|
||||
prefix := getProxyPrefix(r.Header)
|
||||
|
||||
data := getLoginPage(loginUIBox)
|
||||
baseURLIndex := strings.Replace(string(data), "%BASE_URL%", prefix+"/", 2)
|
||||
_, _ = w.Write([]byte(baseURLIndex))
|
||||
} else {
|
||||
r.URL.Path = strings.Replace(r.URL.Path, loginEndPoint, "", 1)
|
||||
http.FileServer(loginUIBox).ServeHTTP(w, r)
|
||||
loginRoot, err := fs.Sub(loginUIBox, loginRootDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
http.FileServer(http.FS(loginRoot)).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -255,6 +185,8 @@ func Start() {
|
||||
|
||||
// Serve the web app
|
||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
const uiRootDir = "ui/v2.5/build"
|
||||
|
||||
ext := path.Ext(r.URL.Path)
|
||||
|
||||
if customUILocation != "" {
|
||||
@@ -267,14 +199,25 @@ func Start() {
|
||||
}
|
||||
|
||||
if ext == ".html" || ext == "" {
|
||||
data, _ := uiBox.Find("index.html")
|
||||
_, _ = w.Write(data)
|
||||
data, err := uiBox.ReadFile(uiRootDir + "/index.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
prefix := getProxyPrefix(r.Header)
|
||||
baseURLIndex := strings.Replace(string(data), "%BASE_URL%", prefix+"/", 2)
|
||||
baseURLIndex = strings.Replace(baseURLIndex, "base href=\"/\"", fmt.Sprintf("base href=\"%s\"", prefix+"/"), 2)
|
||||
_, _ = w.Write([]byte(baseURLIndex))
|
||||
} else {
|
||||
isStatic, _ := path.Match("/static/*/*", r.URL.Path)
|
||||
if isStatic {
|
||||
w.Header().Add("Cache-Control", "max-age=604800000")
|
||||
}
|
||||
http.FileServer(uiBox).ServeHTTP(w, r)
|
||||
uiRoot, err := fs.Sub(uiBox, uiRootDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
http.FileServer(http.FS(uiRoot)).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -288,7 +231,7 @@ func Start() {
|
||||
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: %s", err.Error()))
|
||||
panic(fmt.Errorf("error loading TLS config: %v", err))
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
@@ -299,14 +242,28 @@ func Start() {
|
||||
|
||||
go func() {
|
||||
printVersion()
|
||||
printLatestVersion()
|
||||
printLatestVersion(context.TODO())
|
||||
logger.Infof("stash is listening on " + address)
|
||||
if tlsConfig != nil {
|
||||
displayAddress = "https://" + displayAddress + "/"
|
||||
} else {
|
||||
displayAddress = "http://" + displayAddress + "/"
|
||||
}
|
||||
|
||||
// This can be done before actually starting the server, as modern browsers will
|
||||
// automatically reload the page if a local port is closed at page load and then opened.
|
||||
if !c.GetNoBrowser() && manager.GetInstance().IsDesktop() {
|
||||
err = browser.OpenURL(displayAddress)
|
||||
if err != nil {
|
||||
logger.Error("Could not open browser: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
logger.Infof("stash is running at https://" + displayAddress + "/")
|
||||
logger.Infof("stash is running at " + displayAddress)
|
||||
logger.Error(server.ListenAndServeTLS("", ""))
|
||||
} else {
|
||||
logger.Infof("stash is running at http://" + displayAddress + "/")
|
||||
logger.Infof("stash is running at " + displayAddress)
|
||||
logger.Error(server.ListenAndServe())
|
||||
}
|
||||
}()
|
||||
@@ -314,12 +271,21 @@ func Start() {
|
||||
|
||||
func printVersion() {
|
||||
versionString := githash
|
||||
if IsOfficialBuild() {
|
||||
versionString += " - Official Build"
|
||||
} else {
|
||||
versionString += " - Unofficial Build"
|
||||
}
|
||||
if version != "" {
|
||||
versionString = version + " (" + versionString + ")"
|
||||
}
|
||||
fmt.Printf("stash version: %s - %s\n", versionString, buildstamp)
|
||||
}
|
||||
|
||||
func IsOfficialBuild() bool {
|
||||
return officialBuild == "true"
|
||||
}
|
||||
|
||||
func GetVersion() (string, string, string) {
|
||||
return version, githash, buildstamp
|
||||
}
|
||||
@@ -342,12 +308,12 @@ func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
|
||||
return nil, errors.New("SSL key file must be present if certificate file is present")
|
||||
}
|
||||
|
||||
cert, err := ioutil.ReadFile(certFile)
|
||||
cert, err := os.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading SSL certificate file %s: %s", certFile, err.Error())
|
||||
}
|
||||
|
||||
key, err := ioutil.ReadFile(keyFile)
|
||||
key, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading SSL key file %s: %s", keyFile, err.Error())
|
||||
}
|
||||
@@ -355,7 +321,7 @@ func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
|
||||
certs := make([]tls.Certificate, 1)
|
||||
certs[0], err = tls.X509KeyPair(cert, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing key pair: %s", err.Error())
|
||||
return nil, fmt.Errorf("error parsing key pair: %v", err)
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: certs,
|
||||
@@ -382,11 +348,19 @@ func BaseURLMiddleware(next http.Handler) http.Handler {
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
baseURL := scheme + "://" + r.Host
|
||||
prefix := getProxyPrefix(r.Header)
|
||||
|
||||
port := ""
|
||||
forwardedPort := r.Header.Get("X-Forwarded-Port")
|
||||
if forwardedPort != "" && forwardedPort != "80" && forwardedPort != "8080" && forwardedPort != "443" && !strings.Contains(r.Host, ":") {
|
||||
port = ":" + forwardedPort
|
||||
}
|
||||
|
||||
baseURL := scheme + "://" + r.Host + port + prefix
|
||||
|
||||
externalHost := config.GetInstance().GetExternalHost()
|
||||
if externalHost != "" {
|
||||
baseURL = externalHost
|
||||
baseURL = externalHost + prefix
|
||||
}
|
||||
|
||||
r = r.WithContext(context.WithValue(ctx, BaseURLCtxKey, baseURL))
|
||||
@@ -395,3 +369,12 @@ func BaseURLMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func getProxyPrefix(headers http.Header) string {
|
||||
prefix := ""
|
||||
if headers.Get("X-Forwarded-Prefix") != "" {
|
||||
prefix = strings.TrimRight(headers.Get("X-Forwarded-Prefix"), "/")
|
||||
}
|
||||
|
||||
return prefix
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
@@ -10,20 +12,24 @@ import (
|
||||
"github.com/stashapp/stash/pkg/session"
|
||||
)
|
||||
|
||||
const cookieName = "session"
|
||||
const usernameFormKey = "username"
|
||||
const passwordFormKey = "password"
|
||||
const userIDKey = "userID"
|
||||
|
||||
const loginRootDir = "ui/login"
|
||||
const returnURLParam = "returnURL"
|
||||
|
||||
func getLoginPage(loginUIBox embed.FS) []byte {
|
||||
data, err := loginUIBox.ReadFile(loginRootDir + "/login.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
type loginTemplateData struct {
|
||||
URL string
|
||||
Error string
|
||||
}
|
||||
|
||||
func redirectToLogin(w http.ResponseWriter, returnURL string, loginError string) {
|
||||
data, _ := loginUIBox.Find("login.html")
|
||||
func redirectToLogin(loginUIBox embed.FS, w http.ResponseWriter, returnURL string, loginError string) {
|
||||
data := getLoginPage(loginUIBox)
|
||||
templ, err := template.New("Login").Parse(string(data))
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error: %s", err), http.StatusInternalServerError)
|
||||
@@ -36,42 +42,48 @@ func redirectToLogin(w http.ResponseWriter, returnURL string, loginError string)
|
||||
}
|
||||
}
|
||||
|
||||
func getLoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !config.GetInstance().HasCredentials() {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
func getLoginHandler(loginUIBox embed.FS) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !config.GetInstance().HasCredentials() {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
redirectToLogin(w, r.URL.Query().Get(returnURLParam), "")
|
||||
redirectToLogin(loginUIBox, w, r.URL.Query().Get(returnURLParam), "")
|
||||
}
|
||||
}
|
||||
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.FormValue(returnURLParam)
|
||||
if url == "" {
|
||||
url = "/"
|
||||
}
|
||||
func handleLogin(loginUIBox embed.FS) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.FormValue(returnURLParam)
|
||||
if url == "" {
|
||||
url = "/"
|
||||
}
|
||||
|
||||
err := manager.GetInstance().SessionStore.Login(w, r)
|
||||
if err == session.ErrInvalidCredentials {
|
||||
// redirect back to the login page with an error
|
||||
redirectToLogin(w, url, "Username or password is invalid")
|
||||
return
|
||||
}
|
||||
err := manager.GetInstance().SessionStore.Login(w, r)
|
||||
if errors.Is(err, session.ErrInvalidCredentials) {
|
||||
// redirect back to the login page with an error
|
||||
redirectToLogin(loginUIBox, w, url, "Username or password is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if err := manager.GetInstance().SessionStore.Logout(w, r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
func handleLogout(loginUIBox embed.FS) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := manager.GetInstance().SessionStore.Logout(w, r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect to the login page if credentials are required
|
||||
getLoginHandler(w, r)
|
||||
// redirect to the login page if credentials are required
|
||||
getLoginHandler(loginUIBox)(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@ func (b SceneURLBuilder) GetSceneMarkerStreamPreviewURL(sceneMarkerID int) strin
|
||||
return b.BaseURL + "/scene/" + b.SceneID + "/scene_marker/" + strconv.Itoa(sceneMarkerID) + "/preview"
|
||||
}
|
||||
|
||||
func (b SceneURLBuilder) GetSceneMarkerStreamScreenshotURL(sceneMarkerID int) string {
|
||||
return b.BaseURL + "/scene/" + b.SceneID + "/scene_marker/" + strconv.Itoa(sceneMarkerID) + "/screenshot"
|
||||
}
|
||||
|
||||
func (b SceneURLBuilder) GetFunscriptURL() string {
|
||||
return b.BaseURL + "/scene/" + b.SceneID + "/funscript"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user