mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c45ae068fc | ||
|
|
a20fbe33c0 | ||
|
|
82f4a8f671 | ||
|
|
33050f700e | ||
|
|
4ad0241c53 | ||
|
|
7e8c764dc7 | ||
|
|
fd9e4b3ec2 | ||
|
|
3abdcbee6f | ||
|
|
476688c84d | ||
|
|
7152be6086 | ||
|
|
e4ef14e830 | ||
|
|
f543046349 | ||
|
|
c9f76a01c5 | ||
|
|
5c4bf4ecdf | ||
|
|
17be7e97d3 | ||
|
|
71e39e5cb8 | ||
|
|
a17199ba21 | ||
|
|
d1c207e40b | ||
|
|
129dd0ffcc | ||
|
|
a3838734c5 | ||
|
|
b897de3e5e | ||
|
|
5407596e0d | ||
|
|
f7a164ffe5 | ||
|
|
653cd16eb2 | ||
|
|
a2153ced52 | ||
|
|
a44993bbf4 | ||
|
|
ba83da1983 | ||
|
|
0a98296642 | ||
|
|
ca970b9706 | ||
|
|
2b288fd67c | ||
|
|
7f1ad30db1 | ||
|
|
5721ea2b70 | ||
|
|
8c2a25b833 | ||
|
|
601a16b5cb | ||
|
|
879c20efc7 | ||
|
|
283f76240f | ||
|
|
ad17e7defe | ||
|
|
7a2e59fcef | ||
|
|
7c09f24f34 | ||
|
|
fb82866512 | ||
|
|
15da2c1f4c | ||
|
|
1dac598755 | ||
|
|
ad442fbee5 | ||
|
|
4e9925fd3f | ||
|
|
7b064ac99e | ||
|
|
a8a3b4cfd9 | ||
|
|
306ba63ab6 | ||
|
|
c21ded028a | ||
|
|
899ee713ab | ||
|
|
a3c34a51aa | ||
|
|
010a355e0b | ||
|
|
bcf0fda7ac | ||
|
|
96fdd94a01 | ||
|
|
68738bd227 | ||
|
|
8133aa8c91 | ||
|
|
ae1841efb0 | ||
|
|
27aef4ac2e | ||
|
|
b3d6a8eedd | ||
|
|
a023a86ca6 | ||
|
|
294e2090d0 | ||
|
|
c69d72b243 | ||
|
|
cdea9374d8 | ||
|
|
b1b223c90a | ||
|
|
c74456c07e | ||
|
|
ca55f96fd8 | ||
|
|
b7799df2a6 | ||
|
|
10341fba58 | ||
|
|
996dfb1c2f | ||
|
|
ce47efc415 | ||
|
|
3089e1ad69 | ||
|
|
62ff6f3c7f | ||
|
|
e49beb139c | ||
|
|
d8ee57cd50 | ||
|
|
427c18be7d | ||
|
|
7788a6fd07 | ||
|
|
49060e6686 | ||
|
|
a94bf29b34 | ||
|
|
ecb53cee55 | ||
|
|
fb77e18182 | ||
|
|
c47aafff66 | ||
|
|
aa1894964f | ||
|
|
c8d4dacffd | ||
|
|
c79f299d1a | ||
|
|
6a5dc4e774 | ||
|
|
540d72bc44 | ||
|
|
d96850c008 | ||
|
|
48c6373afa | ||
|
|
5512d37da3 | ||
|
|
f79677ba96 | ||
|
|
bfd8e81ffd | ||
|
|
720b233be6 | ||
|
|
3ddfafa831 | ||
|
|
f598fa71da | ||
|
|
6cebf146cb | ||
|
|
d0caf87eeb | ||
|
|
a0b082a36d | ||
|
|
ec23b26c60 | ||
|
|
15a7b8a859 | ||
|
|
b69d9cc840 | ||
|
|
12917f51d0 | ||
|
|
a3e72b61ee | ||
|
|
2739696813 | ||
|
|
f477b996b5 | ||
|
|
70250c93f1 | ||
|
|
4cca3b298d | ||
|
|
436ae0a027 | ||
|
|
dc3ce2b414 | ||
|
|
4244bd0b18 | ||
|
|
cbd273a19c | ||
|
|
b7f938531b | ||
|
|
af6841be49 | ||
|
|
d986a9eb4f | ||
|
|
3156191b83 | ||
|
|
593207866f | ||
|
|
1f5377da1c | ||
|
|
a4e25f32ea | ||
|
|
6775a28ec7 | ||
|
|
a8fca47a8c | ||
|
|
2b1a57c6d0 | ||
|
|
a7e5ccd080 | ||
|
|
a1fc14f8c4 | ||
|
|
9c13b39f99 | ||
|
|
b3d35dfae4 | ||
|
|
f26766033e | ||
|
|
fda4776d30 | ||
|
|
f9a624b803 | ||
|
|
4be60310c3 | ||
|
|
2d483f2d11 | ||
|
|
e18c050fb1 | ||
|
|
da4d49d940 | ||
|
|
845d718c67 | ||
|
|
ed057c971f | ||
|
|
94a978d063 | ||
|
|
dcb86d9186 | ||
|
|
62bdff351d | ||
|
|
bf25759a57 | ||
|
|
621e890a48 | ||
|
|
e843c890fb | ||
|
|
ff23d4e20b |
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
@@ -12,7 +12,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:8
|
||||
COMPILER_IMAGE: stashapp/compiler:9
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -23,6 +23,11 @@ jobs:
|
||||
- name: Checkout
|
||||
run: git fetch --prune --unshallow --tags
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Pull compiler image
|
||||
run: docker pull $COMPILER_IMAGE
|
||||
|
||||
@@ -108,7 +113,7 @@ jobs:
|
||||
- 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'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stash-win.exe
|
||||
path: dist/stash-win.exe
|
||||
@@ -116,7 +121,7 @@ jobs:
|
||||
- name: Upload macOS binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stash-macos
|
||||
path: dist/stash-macos
|
||||
@@ -124,7 +129,7 @@ jobs:
|
||||
- name: Upload Linux binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stash-linux
|
||||
path: dist/stash-linux
|
||||
@@ -132,7 +137,7 @@ jobs:
|
||||
- name: Upload UI
|
||||
# only upload for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stash-ui.zip
|
||||
path: dist/stash-ui.zip
|
||||
|
||||
7
.github/workflows/golangci-lint.yml
vendored
7
.github/workflows/golangci-lint.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:8
|
||||
COMPILER_IMAGE: stashapp/compiler:9
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
@@ -21,6 +21,11 @@ jobs:
|
||||
- name: Checkout
|
||||
run: git fetch --prune --unshallow --tags
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Pull compiler image
|
||||
run: docker pull $COMPILER_IMAGE
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ linters:
|
||||
- unused
|
||||
# Linters added by the stash project.
|
||||
# - contextcheck
|
||||
- copyloopvar
|
||||
- dogsled
|
||||
- errchkjson
|
||||
- errorlint
|
||||
# - exhaustive
|
||||
- exportloopref
|
||||
- gocritic
|
||||
# - goerr113
|
||||
- gofmt
|
||||
|
||||
17
Makefile
17
Makefile
@@ -307,7 +307,8 @@ test:
|
||||
# runs all tests - including integration tests
|
||||
.PHONY: it
|
||||
it:
|
||||
go test -tags=integration ./...
|
||||
$(eval GO_BUILD_TAGS += integration)
|
||||
go test -tags "$(GO_BUILD_TAGS)" ./...
|
||||
|
||||
# generates test mocks
|
||||
.PHONY: generate-test-mocks
|
||||
@@ -371,6 +372,20 @@ fmt-ui:
|
||||
validate-ui:
|
||||
cd ui/v2.5 && yarn run validate
|
||||
|
||||
# these targets run the same steps as fmt-ui and validate-ui, but only on files that have changed
|
||||
fmt-ui-quick:
|
||||
cd ui/v2.5 && yarn run prettier --write $$(git diff --name-only --relative --diff-filter d . ../../graphql)
|
||||
|
||||
# does not run tsc checks, as they are slow
|
||||
validate-ui-quick:
|
||||
cd ui/v2.5 && \
|
||||
tsfiles=$$(git diff --name-only --relative --diff-filter d src | grep -e "\.tsx\?\$$"); \
|
||||
scssfiles=$$(git diff --name-only --relative --diff-filter d src | grep "\.scss"); \
|
||||
prettyfiles=$$(git diff --name-only --relative --diff-filter d . ../../graphql); \
|
||||
if [ -n "$$tsfiles" ]; then yarn run eslint $$tsfiles; fi && \
|
||||
if [ -n "$$scssfiles" ]; then yarn run stylelint $$scssfiles; fi && \
|
||||
if [ -n "$$prettyfiles" ]; then yarn run prettier --check $$prettyfiles; fi
|
||||
|
||||
# runs all of the backend PR-acceptance steps
|
||||
.PHONY: validate-backend
|
||||
validate-backend: lint it
|
||||
|
||||
@@ -57,10 +57,11 @@ Stash can pull metadata (performers, tags, descriptions, studios, and more) dire
|
||||
<sub>[StashDB](http://stashdb.org) is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box).</sub>
|
||||
|
||||
# Translation
|
||||
[](https://hosted.weblate.org/engage/stashapp/)
|
||||
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇪🇪 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇷🇺 🇪🇸 🇸🇪 🇹🇼 🇹🇷
|
||||
[](https://translate.codeberg.org/engage/stash/)
|
||||
|
||||
Stash is available in 25 languages (so far!) and it could be in your language too. We use Weblate to coordinate community translations. If you want to help us translate Stash into your language, you can make an account at [Stash's Weblate](https://hosted.weblate.org/projects/stashapp/stash/) to get started contributing new languages or improving existing ones. Thanks!
|
||||
Stash is available in 32 languages (so far!) and it could be in your language too. We use Weblate to coordinate community translations. If you want to help us translate Stash into your language, you can make an account at [Codeberg's Weblate](https://translate.codeberg.org/projects/stash/stash/) to get started contributing new languages or improving existing ones. Thanks!
|
||||
|
||||
[](https://translate.codeberg.org/engage/stash/)
|
||||
|
||||
# Support (FAQ)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ func customUsage() {
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func printPhash(ff *ffmpeg.FFMpeg, ffp ffmpeg.FFProbe, inputfile string, quiet *bool) error {
|
||||
func printPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, quiet *bool) error {
|
||||
ffvideoFile, err := ffp.NewVideoFile(inputfile)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -80,7 +80,7 @@ func main() {
|
||||
ffmpegPath, ffprobePath := getPaths()
|
||||
encoder := ffmpeg.NewEncoder(ffmpegPath)
|
||||
// don't need to InitHWSupport, phashing doesn't use hw acceleration
|
||||
ffprobe := ffmpeg.FFProbe(ffprobePath)
|
||||
ffprobe := ffmpeg.NewFFProbe(ffprobePath)
|
||||
|
||||
for _, item := range args {
|
||||
if err := printPhash(encoder, ffprobe, item, quiet); err != nil {
|
||||
|
||||
@@ -16,7 +16,7 @@ ARG STASH_VERSION
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
|
||||
|
||||
# Build Backend
|
||||
FROM golang:1.19-alpine as backend
|
||||
FROM golang:1.22-alpine as backend
|
||||
RUN apk add --no-cache make alpine-sdk
|
||||
WORKDIR /stash
|
||||
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
||||
|
||||
@@ -16,7 +16,7 @@ ARG STASH_VERSION
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
|
||||
|
||||
# Build Backend
|
||||
FROM golang:1.19-bullseye as backend
|
||||
FROM golang:1.22-bullseye as backend
|
||||
RUN apt update && apt install -y build-essential golang
|
||||
WORKDIR /stash
|
||||
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.19
|
||||
FROM golang:1.22
|
||||
|
||||
LABEL maintainer="https://discord.gg/2TsNFKt"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
user=stashapp
|
||||
repo=compiler
|
||||
version=8
|
||||
version=9
|
||||
|
||||
latest:
|
||||
docker build -t ${user}/${repo}:latest .
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
## Goals and design vision
|
||||
|
||||
The goal of stash is to be:
|
||||
- an application for organising and viewing adult content - currently this is videos and images, in future this will be extended to include audio and text content
|
||||
- organising includes scraping of metadata from websites and metadata repositories
|
||||
- free and open-source
|
||||
- portable and offline - can be run on a USB stick without needing to install dependencies (with the exception of ffmpeg)
|
||||
- minimal, but highly extensible. The core feature set should be the minimum required to achieve the primary goal, while being extensible enough to extend via plugins
|
||||
- easy to learn and use, with minimal technical knowledge required
|
||||
|
||||
The core stash system is not intended for:
|
||||
- managing downloading of content
|
||||
- managing content on external websites
|
||||
- publically sharing content
|
||||
|
||||
Other requirements:
|
||||
- support as many video and image formats as possible
|
||||
- interfaces with external systems (for example stash-box) should be made as generic as possible.
|
||||
|
||||
Design considerations:
|
||||
- features are easy to add and difficult to remove. Large superfluous features should be scrutinised and avoided where possible (eg DLNA, filename parser). Such features should be considered for third-party plugins instead.
|
||||
|
||||
## Technical Debt
|
||||
Please be sure to consider how heavily your contribution impacts the maintainability of the project long term, sometimes less is more. We don't want to merge collossal pull requests with hundreds of dependencies by a driveby contributor.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
* [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)
|
||||
* To install, follow the [local installation instructions](https://golangci-lint.run/welcome/install/#local-installation)
|
||||
* [Yarn](https://yarnpkg.com/en/docs/install) - Yarn package manager
|
||||
|
||||
## Environment
|
||||
@@ -69,6 +69,9 @@ NOTE: The `make` command in OpenBSD will be `gmake`. For example, `make pre-ui`
|
||||
* `make it` - Runs all unit and integration tests
|
||||
* `make fmt` - Formats the Go source code
|
||||
* `make fmt-ui` - Formats the UI source code
|
||||
* `make validate-ui` - Runs tests and checks for the UI only
|
||||
* `make fmt-ui-quick` - (experimental) Formats only changed UI source code
|
||||
* `make validate-ui-quick` - (experimental) Runs tests and checks of changed UI code
|
||||
* `make server-start` - Runs a development stash server in the `.local` directory
|
||||
* `make server-clean` - Removes the `.local` directory and all of its contents
|
||||
* `make ui-start` - Runs the UI in development mode. Requires a running Stash server to connect to - the server URL can be changed from the default of `http://localhost:9999` using the environment variable `VITE_APP_PLATFORM_URL`, but keep in mind that authentication cannot be used since the session authorization cookie cannot be sent cross-origin. The UI runs on port `3000` or the next available port.
|
||||
|
||||
54
go.mod
54
go.mod
@@ -1,9 +1,9 @@
|
||||
module github.com/stashapp/stash
|
||||
|
||||
go 1.19
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.2
|
||||
github.com/99designs/gqlgen v0.17.49
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552
|
||||
github.com/Yamashou/gqlgenc v0.0.6
|
||||
github.com/anacrolix/dms v1.2.2
|
||||
@@ -15,46 +15,48 @@ require (
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
||||
github.com/doug-martin/goqu/v9 v9.18.0
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/httplog v0.3.1
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/gofrs/uuid/v5 v5.0.0
|
||||
github.com/gofrs/uuid/v5 v5.1.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.6
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/kermieisinthehouse/gosx-notifier v0.1.2
|
||||
github.com/kermieisinthehouse/systray v1.2.4
|
||||
github.com/knadh/koanf v1.5.0
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
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/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cast v1.5.1
|
||||
github.com/spf13/cast v1.6.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.16.0
|
||||
github.com/vearutop/statigz v1.4.0
|
||||
github.com/vektah/dataloaden v0.3.0
|
||||
github.com/vektah/gqlparser/v2 v2.4.2
|
||||
github.com/vektah/gqlparser/v2 v2.5.16
|
||||
github.com/vektra/mockery/v2 v2.10.0
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
|
||||
github.com/zencoder/go-dash/v3 v3.0.2
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/image v0.12.0
|
||||
golang.org/x/net v0.23.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/term v0.18.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/net v0.26.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/term v0.21.0
|
||||
golang.org/x/text v0.16.0
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
@@ -65,7 +67,7 @@ require (
|
||||
github.com/asticode/go-astikit v0.20.0 // indirect
|
||||
github.com/asticode/go-astits v1.8.0 // indirect
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
@@ -75,20 +77,18 @@ require (
|
||||
github.com/gobwas/ws v1.3.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matryer/moq v0.2.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -99,19 +99,21 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/zerolog v1.30.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cobra v1.7.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.16.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/urfave/cli/v2 v2.8.1 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.2 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
129
go.sum
129
go.sum
@@ -49,14 +49,19 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/99designs/gqlgen v0.17.2 h1:yczvlwMsfcVu/JtejqfrLwXuSP0yZFhmcss3caEvHw8=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/99designs/gqlgen v0.17.2/go.mod h1:K5fzLKwtph+FFgh9j7nFbRUdBKvTcGnsta51fsMTn3o=
|
||||
github.com/99designs/gqlgen v0.17.49 h1:b3hNGexHd33fBSAd4NDT/c3NCcQzcAVkknhN9ym36YQ=
|
||||
github.com/99designs/gqlgen v0.17.49/go.mod h1:tC8YFVZMed81x7UJ7ORUwXF4Kn6SXuucFqQBhN8+BU0=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552 h1:eukVk+mGmbSZppLw8WJGpEUgMC570eb32y7FOsPW4Kc=
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552/go.mod h1:LKbO1i6L1lSlwWx4NHWVECxubHNKFz2YQoEMGXAFVy8=
|
||||
@@ -81,6 +86,9 @@ github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
|
||||
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
|
||||
github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes=
|
||||
@@ -114,6 +122,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bool64/dev v0.2.28 h1:6ayDfrB/jnNr2iQAZHI+uT3Qi6rErSbJYQs1y8rSrwM=
|
||||
github.com/bool64/dev v0.2.28/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
|
||||
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -154,8 +163,9 @@ github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9M
|
||||
github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -193,7 +203,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
@@ -202,8 +213,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/httplog v0.3.1 h1:uC3IUWCZagtbCinb3ypFh36SEcgd6StWw2Bu0XSXRtg=
|
||||
@@ -220,8 +231,9 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
|
||||
@@ -234,8 +246,8 @@ github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K
|
||||
github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0=
|
||||
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
||||
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gofrs/uuid/v5 v5.1.0 h1:S5rqVKIigghZTCBKPCw0Y+bXkn26K3TB5mvQq2Ix8dk=
|
||||
github.com/gofrs/uuid/v5 v5.1.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
@@ -294,7 +306,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -319,6 +332,8 @@ github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
@@ -368,10 +383,9 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
|
||||
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.6 h1:3xi/Cafd1NaoEnS/yDssIiuVeDVywU0QdFGl3aQaQHM=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.6/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
@@ -399,8 +413,8 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
@@ -435,15 +449,16 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
@@ -453,7 +468,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matryer/moq v0.2.3 h1:Q06vEqnBYjjfx5KKgHfYRKE/lvlRu+Nj+xodG4YdHnU=
|
||||
github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
@@ -469,12 +483,12 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
@@ -565,6 +579,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
@@ -575,6 +590,8 @@ github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiS
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
@@ -582,8 +599,9 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5P
|
||||
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
|
||||
github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
@@ -594,6 +612,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
@@ -601,8 +621,8 @@ github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfA
|
||||
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
|
||||
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
|
||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
|
||||
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
@@ -617,8 +637,9 @@ github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1Fof
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
@@ -628,8 +649,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
@@ -643,23 +665,23 @@ github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso
|
||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
|
||||
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
|
||||
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
|
||||
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
|
||||
github.com/vearutop/statigz v1.4.0 h1:RQL0KG3j/uyA/PFpHeZ/L6l2ta920/MxlOAIGEOuwmU=
|
||||
github.com/vearutop/statigz v1.4.0/go.mod h1:LYTolBLiz9oJISwiVKnOQoIwhO1LWX1A7OECawGS8XE=
|
||||
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
|
||||
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
|
||||
github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/vektah/gqlparser/v2 v2.4.2 h1:29TGc6QmhEUq5fll+2FPoTmhUhR65WEKN4VK/jo0OlM=
|
||||
github.com/vektah/gqlparser/v2 v2.4.2/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8=
|
||||
github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww=
|
||||
github.com/vektra/mockery/v2 v2.10.0 h1:MiiQWxwdq7/ET6dCXLaJzSGEN17k758H7JHS9kOdiks=
|
||||
github.com/vektra/mockery/v2 v2.10.0/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
|
||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e h1:GruPsb+44XvYAzuAgJW1d1WHqmcI73L2XSjsbx/eJZw=
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e/go.mod h1:wA8kQ8WFipMciY9WcWzqQgZordm/P7l8IZdvx1crwmc=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -706,8 +728,8 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -721,8 +743,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
|
||||
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -750,9 +772,8 @@ golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -802,9 +823,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -834,8 +854,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -925,17 +945,15 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -948,10 +966,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1017,9 +1033,8 @@ golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -51,6 +51,11 @@ models:
|
||||
fieldName: DurationFinite
|
||||
frame_rate:
|
||||
fieldName: FrameRateFinite
|
||||
# movie is group under the hood
|
||||
Movie:
|
||||
model: github.com/stashapp/stash/pkg/models.Group
|
||||
MovieFilterType:
|
||||
model: github.com/stashapp/stash/pkg/models.GroupFilterType
|
||||
# autobind on config causes generation issues
|
||||
BlobsStorageType:
|
||||
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
|
||||
|
||||
@@ -4,6 +4,7 @@ type Query {
|
||||
findSavedFilter(id: ID!): SavedFilter
|
||||
findSavedFilters(mode: FilterMode): [SavedFilter!]!
|
||||
findDefaultFilter(mode: FilterMode!): SavedFilter
|
||||
@deprecated(reason: "default filter now stored in UI config")
|
||||
|
||||
"Find a scene by ID or Checksum"
|
||||
findScene(id: ID, checksum: String): Scene
|
||||
@@ -76,13 +77,22 @@ type Query {
|
||||
): FindStudiosResultType!
|
||||
|
||||
"Find a movie by ID"
|
||||
findMovie(id: ID!): Movie
|
||||
findMovie(id: ID!): Movie @deprecated(reason: "Use findGroup instead")
|
||||
"A function which queries Movie objects"
|
||||
findMovies(
|
||||
movie_filter: MovieFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindMoviesResultType!
|
||||
): FindMoviesResultType! @deprecated(reason: "Use findGroups instead")
|
||||
|
||||
"Find a group by ID"
|
||||
findGroup(id: ID!): Group
|
||||
"A function which queries Group objects"
|
||||
findGroups(
|
||||
group_filter: GroupFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindGroupsResultType!
|
||||
|
||||
findGallery(id: ID!): Gallery
|
||||
findGalleries(
|
||||
@@ -155,7 +165,13 @@ type Query {
|
||||
scrapeSingleMovie(
|
||||
source: ScraperSourceInput!
|
||||
input: ScrapeSingleMovieInput!
|
||||
): [ScrapedMovie!]!
|
||||
): [ScrapedMovie!]! @deprecated(reason: "Use scrapeSingleGroup instead")
|
||||
|
||||
"Scrape for a single group"
|
||||
scrapeSingleGroup(
|
||||
source: ScraperSourceInput!
|
||||
input: ScrapeSingleGroupInput!
|
||||
): [ScrapedGroup!]!
|
||||
|
||||
"Scrapes content based on a URL"
|
||||
scrapeURL(url: String!, ty: ScrapeContentType!): ScrapedContent
|
||||
@@ -168,6 +184,9 @@ type Query {
|
||||
scrapeGalleryURL(url: String!): ScrapedGallery
|
||||
"Scrapes a complete movie record based on a URL"
|
||||
scrapeMovieURL(url: String!): ScrapedMovie
|
||||
@deprecated(reason: "Use scrapeGroupURL instead")
|
||||
"Scrapes a complete group record based on a URL"
|
||||
scrapeGroupURL(url: String!): ScrapedGroup
|
||||
|
||||
# Plugins
|
||||
"List loaded plugins"
|
||||
@@ -213,7 +232,7 @@ type Query {
|
||||
allPerformers: [Performer!]!
|
||||
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
|
||||
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
|
||||
allMovies: [Movie!]! @deprecated(reason: "Use findMovies instead")
|
||||
allMovies: [Movie!]! @deprecated(reason: "Use findGroups instead")
|
||||
|
||||
# Get everything with minimal metadata
|
||||
|
||||
@@ -257,6 +276,13 @@ type Mutation {
|
||||
"Sets the resume time point (if provided) and adds the provided duration to the scene's play duration"
|
||||
sceneSaveActivity(id: ID!, resume_time: Float, playDuration: Float): Boolean!
|
||||
|
||||
"Resets the resume time point and play duration"
|
||||
sceneResetActivity(
|
||||
id: ID!
|
||||
reset_resume: Boolean
|
||||
reset_duration: Boolean
|
||||
): Boolean!
|
||||
|
||||
"Increments the play count for the scene. Returns the new play count value."
|
||||
sceneIncrementPlayCount(id: ID!): Int!
|
||||
@deprecated(reason: "Use sceneAddPlay instead")
|
||||
@@ -298,6 +324,8 @@ type Mutation {
|
||||
|
||||
addGalleryImages(input: GalleryAddInput!): Boolean!
|
||||
removeGalleryImages(input: GalleryRemoveInput!): Boolean!
|
||||
setGalleryCover(input: GallerySetCoverInput!): Boolean!
|
||||
resetGalleryCover(input: GalleryResetCoverInput!): Boolean!
|
||||
|
||||
galleryChapterCreate(input: GalleryChapterCreateInput!): GalleryChapter
|
||||
galleryChapterUpdate(input: GalleryChapterUpdateInput!): GalleryChapter
|
||||
@@ -315,16 +343,34 @@ type Mutation {
|
||||
studiosDestroy(ids: [ID!]!): Boolean!
|
||||
|
||||
movieCreate(input: MovieCreateInput!): Movie
|
||||
@deprecated(reason: "Use groupCreate instead")
|
||||
movieUpdate(input: MovieUpdateInput!): Movie
|
||||
@deprecated(reason: "Use groupUpdate instead")
|
||||
movieDestroy(input: MovieDestroyInput!): Boolean!
|
||||
@deprecated(reason: "Use groupDestroy instead")
|
||||
moviesDestroy(ids: [ID!]!): Boolean!
|
||||
@deprecated(reason: "Use groupsDestroy instead")
|
||||
bulkMovieUpdate(input: BulkMovieUpdateInput!): [Movie!]
|
||||
@deprecated(reason: "Use bulkGroupUpdate instead")
|
||||
|
||||
groupCreate(input: GroupCreateInput!): Group
|
||||
groupUpdate(input: GroupUpdateInput!): Group
|
||||
groupDestroy(input: GroupDestroyInput!): Boolean!
|
||||
groupsDestroy(ids: [ID!]!): Boolean!
|
||||
bulkGroupUpdate(input: BulkGroupUpdateInput!): [Group!]
|
||||
|
||||
addGroupSubGroups(input: GroupSubGroupAddInput!): Boolean!
|
||||
removeGroupSubGroups(input: GroupSubGroupRemoveInput!): Boolean!
|
||||
|
||||
"Reorder sub groups within a group. Returns true if successful."
|
||||
reorderSubGroups(input: ReorderSubGroupsInput!): Boolean!
|
||||
|
||||
tagCreate(input: TagCreateInput!): Tag
|
||||
tagUpdate(input: TagUpdateInput!): Tag
|
||||
tagDestroy(input: TagDestroyInput!): Boolean!
|
||||
tagsDestroy(ids: [ID!]!): Boolean!
|
||||
tagsMerge(input: TagsMergeInput!): Tag
|
||||
bulkTagUpdate(input: BulkTagUpdateInput!): [Tag!]
|
||||
|
||||
"""
|
||||
Moves the given files to the given destination. Returns true if successful.
|
||||
@@ -344,6 +390,7 @@ type Mutation {
|
||||
saveFilter(input: SaveFilterInput!): SavedFilter!
|
||||
destroySavedFilter(input: DestroyFilterInput!): Boolean!
|
||||
setDefaultFilter(input: SetDefaultFilterInput!): Boolean!
|
||||
@deprecated(reason: "now uses UI config")
|
||||
|
||||
"Change general configuration options"
|
||||
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
|
||||
|
||||
@@ -170,6 +170,14 @@ input PerformerFilterType {
|
||||
birthdate: DateCriterionInput
|
||||
"Filter by death date"
|
||||
death_date: DateCriterionInput
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related images that meet this criteria"
|
||||
images_filter: ImageFilterType
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by related tags that meet this criteria"
|
||||
tags_filter: TagFilterType
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
@@ -183,6 +191,8 @@ input SceneMarkerFilterType {
|
||||
scene_tags: HierarchicalMultiCriterionInput
|
||||
"Filter to only include scene markers with these performers"
|
||||
performers: MultiCriterionInput
|
||||
"Filter to only include scene markers from these scenes"
|
||||
scenes: MultiCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
@@ -193,6 +203,8 @@ input SceneMarkerFilterType {
|
||||
scene_created_at: TimestampCriterionInput
|
||||
"Filter by lscene ast update time"
|
||||
scene_updated_at: TimestampCriterionInput
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scene_filter: SceneFilterType
|
||||
}
|
||||
|
||||
input SceneFilterType {
|
||||
@@ -247,7 +259,9 @@ input SceneFilterType {
|
||||
"Filter to only include scenes with this studio"
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"Filter to only include scenes with this movie"
|
||||
movies: MultiCriterionInput
|
||||
movies: MultiCriterionInput @deprecated(reason: "use groups instead")
|
||||
"Filter to only include scenes with this group"
|
||||
groups: HierarchicalMultiCriterionInput
|
||||
"Filter to only include scenes with this gallery"
|
||||
galleries: MultiCriterionInput
|
||||
"Filter to only include scenes with these tags"
|
||||
@@ -288,9 +302,29 @@ input SceneFilterType {
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by related performers that meet this criteria"
|
||||
performers_filter: PerformerFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
"Filter by related tags that meet this criteria"
|
||||
tags_filter: TagFilterType
|
||||
"Filter by related movies that meet this criteria"
|
||||
movies_filter: MovieFilterType
|
||||
@deprecated(reason: "use groups_filter instead")
|
||||
"Filter by related groups that meet this criteria"
|
||||
groups_filter: GroupFilterType
|
||||
"Filter by related markers that meet this criteria"
|
||||
markers_filter: SceneMarkerFilterType
|
||||
}
|
||||
|
||||
input MovieFilterType {
|
||||
AND: MovieFilterType
|
||||
OR: MovieFilterType
|
||||
NOT: MovieFilterType
|
||||
|
||||
name: StringCriterionInput
|
||||
director: StringCriterionInput
|
||||
synopsis: StringCriterionInput
|
||||
@@ -307,12 +341,68 @@ input MovieFilterType {
|
||||
url: StringCriterionInput
|
||||
"Filter to only include movies where performer appears in a scene"
|
||||
performers: MultiCriterionInput
|
||||
"Filter to only include movies with these tags"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter by tag count"
|
||||
tag_count: IntCriterionInput
|
||||
"Filter by date"
|
||||
date: DateCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
}
|
||||
|
||||
input GroupFilterType {
|
||||
AND: GroupFilterType
|
||||
OR: GroupFilterType
|
||||
NOT: GroupFilterType
|
||||
|
||||
name: StringCriterionInput
|
||||
director: StringCriterionInput
|
||||
synopsis: StringCriterionInput
|
||||
|
||||
"Filter by duration (in seconds)"
|
||||
duration: IntCriterionInput
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter to only include groups with this studio"
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"Filter to only include groups missing this property"
|
||||
is_missing: String
|
||||
"Filter by url"
|
||||
url: StringCriterionInput
|
||||
"Filter to only include groups where performer appears in a scene"
|
||||
performers: MultiCriterionInput
|
||||
"Filter to only include groups with these tags"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter by tag count"
|
||||
tag_count: IntCriterionInput
|
||||
"Filter by date"
|
||||
date: DateCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
|
||||
"Filter by containing groups"
|
||||
containing_groups: HierarchicalMultiCriterionInput
|
||||
"Filter by sub groups"
|
||||
sub_groups: HierarchicalMultiCriterionInput
|
||||
"Filter by number of containing groups the group has"
|
||||
containing_group_count: IntCriterionInput
|
||||
"Filter by number of sub-groups the group has"
|
||||
sub_group_count: IntCriterionInput
|
||||
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
}
|
||||
|
||||
input StudioFilterType {
|
||||
@@ -326,6 +416,8 @@ input StudioFilterType {
|
||||
parents: MultiCriterionInput
|
||||
"Filter by StashID"
|
||||
stash_id_endpoint: StashIDCriterionInput
|
||||
"Filter to only include studios with these tags"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter to only include studios missing this property"
|
||||
is_missing: String
|
||||
# rating expressed as 1-100
|
||||
@@ -338,6 +430,8 @@ input StudioFilterType {
|
||||
image_count: IntCriterionInput
|
||||
"Filter by gallery count"
|
||||
gallery_count: IntCriterionInput
|
||||
"Filter by tag count"
|
||||
tag_count: IntCriterionInput
|
||||
"Filter by url"
|
||||
url: StringCriterionInput
|
||||
"Filter by studio aliases"
|
||||
@@ -346,6 +440,12 @@ input StudioFilterType {
|
||||
child_count: IntCriterionInput
|
||||
"Filter by autotag ignore value"
|
||||
ignore_auto_tag: Boolean
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related images that meet this criteria"
|
||||
images_filter: ImageFilterType
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
@@ -411,6 +511,17 @@ input GalleryFilterType {
|
||||
code: StringCriterionInput
|
||||
"Filter by photographer"
|
||||
photographer: StringCriterionInput
|
||||
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related images that meet this criteria"
|
||||
images_filter: ImageFilterType
|
||||
"Filter by related performers that meet this criteria"
|
||||
performers_filter: PerformerFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
"Filter by related tags that meet this criteria"
|
||||
tags_filter: TagFilterType
|
||||
}
|
||||
|
||||
input TagFilterType {
|
||||
@@ -445,6 +556,15 @@ input TagFilterType {
|
||||
"Filter by number of performers with this tag"
|
||||
performer_count: IntCriterionInput
|
||||
|
||||
"Filter by number of studios with this tag"
|
||||
studio_count: IntCriterionInput
|
||||
|
||||
"Filter by number of movies with this tag"
|
||||
movie_count: IntCriterionInput
|
||||
|
||||
"Filter by number of group with this tag"
|
||||
group_count: IntCriterionInput
|
||||
|
||||
"Filter by number of markers with this tag"
|
||||
marker_count: IntCriterionInput
|
||||
|
||||
@@ -463,6 +583,13 @@ input TagFilterType {
|
||||
"Filter by autotag ignore value"
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related images that meet this criteria"
|
||||
images_filter: ImageFilterType
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
|
||||
@@ -528,6 +655,15 @@ input ImageFilterType {
|
||||
code: StringCriterionInput
|
||||
"Filter by photographer"
|
||||
photographer: StringCriterionInput
|
||||
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by related performers that meet this criteria"
|
||||
performers_filter: PerformerFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
"Filter by related tags that meet this criteria"
|
||||
tags_filter: TagFilterType
|
||||
}
|
||||
|
||||
enum CriterionModifier {
|
||||
@@ -623,6 +759,7 @@ enum FilterMode {
|
||||
GALLERIES
|
||||
SCENE_MARKERS
|
||||
MOVIES
|
||||
GROUPS
|
||||
TAGS
|
||||
IMAGES
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
type GalleryPathsType {
|
||||
cover: String!
|
||||
preview: String! # Resolver
|
||||
}
|
||||
|
||||
"Gallery type"
|
||||
type Gallery {
|
||||
id: ID!
|
||||
@@ -25,6 +30,9 @@ type Gallery {
|
||||
performers: [Performer!]!
|
||||
|
||||
cover: Image
|
||||
|
||||
paths: GalleryPathsType! # Resolver
|
||||
image(index: Int!): Image!
|
||||
}
|
||||
|
||||
input GalleryCreateInput {
|
||||
@@ -108,3 +116,12 @@ input GalleryRemoveInput {
|
||||
gallery_id: ID!
|
||||
image_ids: [ID!]!
|
||||
}
|
||||
|
||||
input GallerySetCoverInput {
|
||||
gallery_id: ID!
|
||||
cover_image_id: ID!
|
||||
}
|
||||
|
||||
input GalleryResetCoverInput {
|
||||
gallery_id: ID!
|
||||
}
|
||||
|
||||
137
graphql/schema/types/group.graphql
Normal file
137
graphql/schema/types/group.graphql
Normal file
@@ -0,0 +1,137 @@
|
||||
"GroupDescription represents a relationship to a group with a description of the relationship"
|
||||
type GroupDescription {
|
||||
group: Group!
|
||||
description: String
|
||||
}
|
||||
|
||||
type Group {
|
||||
id: ID!
|
||||
name: String!
|
||||
aliases: String
|
||||
"Duration in seconds"
|
||||
duration: Int
|
||||
date: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio: Studio
|
||||
director: String
|
||||
synopsis: String
|
||||
urls: [String!]!
|
||||
tags: [Tag!]!
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
|
||||
containing_groups: [GroupDescription!]!
|
||||
sub_groups: [GroupDescription!]!
|
||||
|
||||
front_image_path: String # Resolver
|
||||
back_image_path: String # Resolver
|
||||
scene_count(depth: Int): Int! # Resolver
|
||||
sub_group_count(depth: Int): Int! # Resolver
|
||||
scenes: [Scene!]!
|
||||
}
|
||||
|
||||
input GroupDescriptionInput {
|
||||
group_id: ID!
|
||||
description: String
|
||||
}
|
||||
|
||||
input GroupCreateInput {
|
||||
name: String!
|
||||
aliases: String
|
||||
"Duration in seconds"
|
||||
duration: Int
|
||||
date: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
urls: [String!]
|
||||
tag_ids: [ID!]
|
||||
|
||||
containing_groups: [GroupDescriptionInput!]
|
||||
sub_groups: [GroupDescriptionInput!]
|
||||
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input GroupUpdateInput {
|
||||
id: ID!
|
||||
name: String
|
||||
aliases: String
|
||||
duration: Int
|
||||
date: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
urls: [String!]
|
||||
tag_ids: [ID!]
|
||||
|
||||
containing_groups: [GroupDescriptionInput!]
|
||||
sub_groups: [GroupDescriptionInput!]
|
||||
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input BulkUpdateGroupDescriptionsInput {
|
||||
groups: [GroupDescriptionInput!]!
|
||||
mode: BulkUpdateIdMode!
|
||||
}
|
||||
|
||||
input BulkGroupUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
urls: BulkUpdateStrings
|
||||
tag_ids: BulkUpdateIds
|
||||
|
||||
containing_groups: BulkUpdateGroupDescriptionsInput
|
||||
sub_groups: BulkUpdateGroupDescriptionsInput
|
||||
}
|
||||
|
||||
input GroupDestroyInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input ReorderSubGroupsInput {
|
||||
"ID of the group to reorder sub groups for"
|
||||
group_id: ID!
|
||||
"""
|
||||
IDs of the sub groups to reorder. These must be a subset of the current sub groups.
|
||||
Sub groups will be inserted in this order at the insert_index
|
||||
"""
|
||||
sub_group_ids: [ID!]!
|
||||
"The sub-group ID at which to insert the sub groups"
|
||||
insert_at_id: ID!
|
||||
"If true, the sub groups will be inserted after the insert_index, otherwise they will be inserted before"
|
||||
insert_after: Boolean
|
||||
}
|
||||
|
||||
type FindGroupsResultType {
|
||||
count: Int!
|
||||
groups: [Group!]!
|
||||
}
|
||||
|
||||
input GroupSubGroupAddInput {
|
||||
containing_group_id: ID!
|
||||
sub_groups: [GroupDescriptionInput!]!
|
||||
"The index at which to insert the sub groups. If not provided, the sub groups will be appended to the end"
|
||||
insert_index: Int
|
||||
}
|
||||
|
||||
input GroupSubGroupRemoveInput {
|
||||
containing_group_id: ID!
|
||||
sub_group_ids: [ID!]!
|
||||
}
|
||||
@@ -284,7 +284,8 @@ input ExportObjectsInput {
|
||||
studios: ExportObjectTypeInput
|
||||
performers: ExportObjectTypeInput
|
||||
tags: ExportObjectTypeInput
|
||||
movies: ExportObjectTypeInput
|
||||
groups: ExportObjectTypeInput
|
||||
movies: ExportObjectTypeInput @deprecated(reason: "Use groups instead")
|
||||
galleries: ExportObjectTypeInput
|
||||
includeDependencies: Boolean
|
||||
}
|
||||
|
||||
@@ -10,13 +10,15 @@ type Movie {
|
||||
studio: Studio
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]!
|
||||
tags: [Tag!]!
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
|
||||
front_image_path: String # Resolver
|
||||
back_image_path: String # Resolver
|
||||
scene_count: Int! # Resolver
|
||||
scene_count(depth: Int): Int! # Resolver
|
||||
scenes: [Scene!]!
|
||||
}
|
||||
|
||||
@@ -31,7 +33,9 @@ input MovieCreateInput {
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
@@ -49,7 +53,9 @@ input MovieUpdateInput {
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
@@ -63,6 +69,8 @@ input BulkMovieUpdateInput {
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
urls: BulkUpdateStrings
|
||||
tag_ids: BulkUpdateIds
|
||||
}
|
||||
|
||||
input MovieDestroyInput {
|
||||
|
||||
@@ -16,10 +16,11 @@ type Performer {
|
||||
id: ID!
|
||||
name: String!
|
||||
disambiguation: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
gender: GenderEnum
|
||||
twitter: String
|
||||
instagram: String
|
||||
twitter: String @deprecated(reason: "Use urls")
|
||||
instagram: String @deprecated(reason: "Use urls")
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
@@ -41,7 +42,8 @@ type Performer {
|
||||
scene_count: Int! # Resolver
|
||||
image_count: Int! # Resolver
|
||||
gallery_count: Int! # Resolver
|
||||
movie_count: Int! # Resolver
|
||||
group_count: Int! # Resolver
|
||||
movie_count: Int! @deprecated(reason: "use group_count instead") # Resolver
|
||||
performer_count: Int! # Resolver
|
||||
o_counter: Int # Resolver
|
||||
scenes: [Scene!]!
|
||||
@@ -54,13 +56,15 @@ type Performer {
|
||||
weight: Int
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
movies: [Movie!]!
|
||||
groups: [Group!]!
|
||||
movies: [Movie!]! @deprecated(reason: "use groups instead")
|
||||
}
|
||||
|
||||
input PerformerCreateInput {
|
||||
name: String!
|
||||
disambiguation: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
@@ -75,8 +79,8 @@ input PerformerCreateInput {
|
||||
tattoos: String
|
||||
piercings: String
|
||||
alias_list: [String!]
|
||||
twitter: String
|
||||
instagram: String
|
||||
twitter: String @deprecated(reason: "Use urls")
|
||||
instagram: String @deprecated(reason: "Use urls")
|
||||
favorite: Boolean
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
@@ -95,7 +99,8 @@ input PerformerUpdateInput {
|
||||
id: ID!
|
||||
name: String
|
||||
disambiguation: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
@@ -110,8 +115,8 @@ input PerformerUpdateInput {
|
||||
tattoos: String
|
||||
piercings: String
|
||||
alias_list: [String!]
|
||||
twitter: String
|
||||
instagram: String
|
||||
twitter: String @deprecated(reason: "Use urls")
|
||||
instagram: String @deprecated(reason: "Use urls")
|
||||
favorite: Boolean
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
@@ -135,7 +140,8 @@ input BulkPerformerUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
disambiguation: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: BulkUpdateStrings
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
@@ -150,8 +156,8 @@ input BulkPerformerUpdateInput {
|
||||
tattoos: String
|
||||
piercings: String
|
||||
alias_list: BulkUpdateStrings
|
||||
twitter: String
|
||||
instagram: String
|
||||
twitter: String @deprecated(reason: "Use urls")
|
||||
instagram: String @deprecated(reason: "Use urls")
|
||||
favorite: Boolean
|
||||
tag_ids: BulkUpdateIds
|
||||
# rating expressed as 1-100
|
||||
|
||||
@@ -26,6 +26,11 @@ type SceneMovie {
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
type SceneGroup {
|
||||
group: Group!
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
type VideoCaption {
|
||||
language_code: String!
|
||||
caption_type: String!
|
||||
@@ -68,7 +73,8 @@ type Scene {
|
||||
scene_markers: [SceneMarker!]!
|
||||
galleries: [Gallery!]!
|
||||
studio: Studio
|
||||
movies: [SceneMovie!]!
|
||||
groups: [SceneGroup!]!
|
||||
movies: [SceneMovie!]! @deprecated(reason: "Use groups")
|
||||
tags: [Tag!]!
|
||||
performers: [Performer!]!
|
||||
stash_ids: [StashID!]!
|
||||
@@ -82,6 +88,11 @@ input SceneMovieInput {
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
input SceneGroupInput {
|
||||
group_id: ID!
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
input SceneCreateInput {
|
||||
title: String
|
||||
code: String
|
||||
@@ -96,7 +107,8 @@ input SceneCreateInput {
|
||||
studio_id: ID
|
||||
gallery_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
movies: [SceneMovieInput!]
|
||||
groups: [SceneGroupInput!]
|
||||
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
cover_image: String
|
||||
@@ -128,7 +140,8 @@ input SceneUpdateInput {
|
||||
studio_id: ID
|
||||
gallery_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
movies: [SceneMovieInput!]
|
||||
groups: [SceneGroupInput!]
|
||||
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
cover_image: String
|
||||
@@ -175,7 +188,8 @@ input BulkSceneUpdateInput {
|
||||
gallery_ids: BulkUpdateIds
|
||||
performer_ids: BulkUpdateIds
|
||||
tag_ids: BulkUpdateIds
|
||||
movie_ids: BulkUpdateIds
|
||||
group_ids: BulkUpdateIds
|
||||
movie_ids: BulkUpdateIds @deprecated(reason: "Use group_ids")
|
||||
}
|
||||
|
||||
input SceneDestroyInput {
|
||||
|
||||
65
graphql/schema/types/scraped-group.graphql
Normal file
65
graphql/schema/types/scraped-group.graphql
Normal file
@@ -0,0 +1,65 @@
|
||||
"A movie from a scraping operation..."
|
||||
type ScrapedMovie {
|
||||
stored_id: ID
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
synopsis: String
|
||||
studio: ScrapedStudio
|
||||
tags: [ScrapedTag!]
|
||||
|
||||
"This should be a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a base64 encoded data URL"
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input ScrapedMovieInput {
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
synopsis: String
|
||||
# not including tags for the input
|
||||
}
|
||||
|
||||
"A group from a scraping operation..."
|
||||
type ScrapedGroup {
|
||||
stored_id: ID
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
urls: [String!]
|
||||
synopsis: String
|
||||
studio: ScrapedStudio
|
||||
tags: [ScrapedTag!]
|
||||
|
||||
"This should be a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a base64 encoded data URL"
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input ScrapedGroupInput {
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
urls: [String!]
|
||||
synopsis: String
|
||||
# not including tags for the input
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
"A movie from a scraping operation..."
|
||||
type ScrapedMovie {
|
||||
stored_id: ID
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
url: String
|
||||
synopsis: String
|
||||
studio: ScrapedStudio
|
||||
|
||||
"This should be a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a base64 encoded data URL"
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input ScrapedMovieInput {
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
url: String
|
||||
synopsis: String
|
||||
}
|
||||
@@ -5,9 +5,10 @@ type ScrapedPerformer {
|
||||
name: String
|
||||
disambiguation: String
|
||||
gender: String
|
||||
url: String
|
||||
twitter: String
|
||||
instagram: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
twitter: String @deprecated(reason: "use urls")
|
||||
instagram: String @deprecated(reason: "use urls")
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
@@ -40,9 +41,10 @@ input ScrapedPerformerInput {
|
||||
name: String
|
||||
disambiguation: String
|
||||
gender: String
|
||||
url: String
|
||||
twitter: String
|
||||
instagram: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
twitter: String @deprecated(reason: "use urls")
|
||||
instagram: String @deprecated(reason: "use urls")
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
|
||||
@@ -11,6 +11,7 @@ enum ScrapeType {
|
||||
enum ScrapeContentType {
|
||||
GALLERY
|
||||
MOVIE
|
||||
GROUP
|
||||
PERFORMER
|
||||
SCENE
|
||||
}
|
||||
@@ -22,6 +23,7 @@ union ScrapedContent =
|
||||
| ScrapedScene
|
||||
| ScrapedGallery
|
||||
| ScrapedMovie
|
||||
| ScrapedGroup
|
||||
| ScrapedPerformer
|
||||
|
||||
type ScraperSpec {
|
||||
@@ -40,7 +42,9 @@ type Scraper {
|
||||
"Details for gallery scraper"
|
||||
gallery: ScraperSpec
|
||||
"Details for movie scraper"
|
||||
movie: ScraperSpec
|
||||
movie: ScraperSpec @deprecated(reason: "use group")
|
||||
"Details for group scraper"
|
||||
group: ScraperSpec
|
||||
}
|
||||
|
||||
type ScrapedStudio {
|
||||
@@ -76,7 +80,8 @@ type ScrapedScene {
|
||||
studio: ScrapedStudio
|
||||
tags: [ScrapedTag!]
|
||||
performers: [ScrapedPerformer!]
|
||||
movies: [ScrapedMovie!]
|
||||
movies: [ScrapedMovie!] @deprecated(reason: "use groups")
|
||||
groups: [ScrapedGroup!]
|
||||
|
||||
remote_site_id: String
|
||||
duration: Int
|
||||
@@ -128,7 +133,7 @@ input ScraperSourceInput {
|
||||
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 to scrape with. Should be unset if stash_box_endpoint/stash_box_index is set"
|
||||
scraper_id: ID
|
||||
}
|
||||
|
||||
@@ -137,7 +142,7 @@ type ScraperSource {
|
||||
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 to scrape with. Should be unset if stash_box_endpoint/stash_box_index is set"
|
||||
scraper_id: ID
|
||||
}
|
||||
|
||||
@@ -190,13 +195,24 @@ input ScrapeSingleMovieInput {
|
||||
query: String
|
||||
"Instructs to query by movie id"
|
||||
movie_id: ID
|
||||
"Instructs to query by gallery fragment"
|
||||
"Instructs to query by movie fragment"
|
||||
movie_input: ScrapedMovieInput
|
||||
}
|
||||
|
||||
input ScrapeSingleGroupInput {
|
||||
"Instructs to query by string"
|
||||
query: String
|
||||
"Instructs to query by group id"
|
||||
group_id: ID
|
||||
"Instructs to query by group fragment"
|
||||
group_input: ScrapedGroupInput
|
||||
}
|
||||
|
||||
input StashBoxSceneQueryInput {
|
||||
"Index of the configured stash-box instance to use"
|
||||
stash_box_index: Int!
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
"Endpoint of the stash-box instance to use"
|
||||
stash_box_endpoint: String
|
||||
"Instructs query by scene fingerprints"
|
||||
scene_ids: [ID!]
|
||||
"Query by query string"
|
||||
@@ -205,7 +221,9 @@ input StashBoxSceneQueryInput {
|
||||
|
||||
input StashBoxPerformerQueryInput {
|
||||
"Index of the configured stash-box instance to use"
|
||||
stash_box_index: Int!
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
"Endpoint of the stash-box instance to use"
|
||||
stash_box_endpoint: String
|
||||
"Instructs query by scene fingerprints"
|
||||
performer_ids: [ID!]
|
||||
"Query by query string"
|
||||
@@ -226,7 +244,9 @@ type StashBoxFingerprint {
|
||||
"If neither ids nor names are set, tag all items"
|
||||
input StashBoxBatchTagInput {
|
||||
"Stash endpoint to use for the tagging"
|
||||
endpoint: Int!
|
||||
endpoint: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
"Endpoint of the stash-box instance to use"
|
||||
stash_box_endpoint: String
|
||||
"Fields to exclude when executing the tagging"
|
||||
exclude_fields: [String!]
|
||||
"Refresh items already tagged by StashBox if true. Only tag items with no StashBox tagging if false"
|
||||
|
||||
@@ -22,10 +22,12 @@ input StashIDInput {
|
||||
|
||||
input StashBoxFingerprintSubmissionInput {
|
||||
scene_ids: [String!]!
|
||||
stash_box_index: Int!
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
stash_box_endpoint: String
|
||||
}
|
||||
|
||||
input StashBoxDraftSubmissionInput {
|
||||
id: String!
|
||||
stash_box_index: Int!
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
stash_box_endpoint: String
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ type StatsResultType {
|
||||
gallery_count: Int!
|
||||
performer_count: Int!
|
||||
studio_count: Int!
|
||||
movie_count: Int!
|
||||
group_count: Int!
|
||||
movie_count: Int! @deprecated(reason: "use group_count instead")
|
||||
tag_count: Int!
|
||||
total_o_count: Int!
|
||||
total_play_duration: Float!
|
||||
|
||||
@@ -5,6 +5,7 @@ type Studio {
|
||||
parent_studio: Studio
|
||||
child_studios: [Studio!]!
|
||||
aliases: [String!]!
|
||||
tags: [Tag!]!
|
||||
ignore_auto_tag: Boolean!
|
||||
|
||||
image_path: String # Resolver
|
||||
@@ -12,7 +13,8 @@ type Studio {
|
||||
image_count(depth: Int): Int! # Resolver
|
||||
gallery_count(depth: Int): Int! # Resolver
|
||||
performer_count(depth: Int): Int! # Resolver
|
||||
movie_count(depth: Int): Int! # Resolver
|
||||
group_count(depth: Int): Int! # Resolver
|
||||
movie_count(depth: Int): Int! @deprecated(reason: "use group_count instead") # Resolver
|
||||
stash_ids: [StashID!]!
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
@@ -20,7 +22,8 @@ type Studio {
|
||||
details: String
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
movies: [Movie!]!
|
||||
groups: [Group!]!
|
||||
movies: [Movie!]! @deprecated(reason: "use groups instead")
|
||||
}
|
||||
|
||||
input StudioCreateInput {
|
||||
@@ -35,6 +38,7 @@ input StudioCreateInput {
|
||||
favorite: Boolean
|
||||
details: String
|
||||
aliases: [String!]
|
||||
tag_ids: [ID!]
|
||||
ignore_auto_tag: Boolean
|
||||
}
|
||||
|
||||
@@ -51,6 +55,7 @@ input StudioUpdateInput {
|
||||
favorite: Boolean
|
||||
details: String
|
||||
aliases: [String!]
|
||||
tag_ids: [ID!]
|
||||
ignore_auto_tag: Boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ type Tag {
|
||||
image_count(depth: Int): Int! # Resolver
|
||||
gallery_count(depth: Int): Int! # Resolver
|
||||
performer_count(depth: Int): Int! # Resolver
|
||||
studio_count(depth: Int): Int! # Resolver
|
||||
group_count(depth: Int): Int! # Resolver
|
||||
movie_count(depth: Int): Int! @deprecated(reason: "use group_count instead") # Resolver
|
||||
parents: [Tag!]!
|
||||
children: [Tag!]!
|
||||
|
||||
@@ -60,3 +63,14 @@ input TagsMergeInput {
|
||||
source: [ID!]!
|
||||
destination: ID!
|
||||
}
|
||||
|
||||
input BulkTagUpdateInput {
|
||||
ids: [ID!]
|
||||
description: String
|
||||
aliases: BulkUpdateStrings
|
||||
ignore_auto_tag: Boolean
|
||||
favorite: Boolean
|
||||
|
||||
parent_ids: BulkUpdateIds
|
||||
child_ids: BulkUpdateIds
|
||||
}
|
||||
|
||||
@@ -346,32 +346,75 @@ func (t changesetTranslator) updateStashIDs(value []models.StashID, field string
|
||||
}
|
||||
}
|
||||
|
||||
func (t changesetTranslator) relatedMovies(value []models.SceneMovieInput) (models.RelatedMovies, error) {
|
||||
moviesScenes, err := models.MoviesScenesFromInput(value)
|
||||
func (t changesetTranslator) relatedGroupsFromMovies(value []models.SceneMovieInput) (models.RelatedGroups, error) {
|
||||
groupsScenes, err := models.GroupsScenesFromInput(value)
|
||||
if err != nil {
|
||||
return models.RelatedMovies{}, err
|
||||
return models.RelatedGroups{}, err
|
||||
}
|
||||
|
||||
return models.NewRelatedMovies(moviesScenes), nil
|
||||
return models.NewRelatedGroups(groupsScenes), nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateMovieIDs(value []models.SceneMovieInput, field string) (*models.UpdateMovieIDs, error) {
|
||||
func groupsScenesFromGroupInput(input []models.SceneGroupInput) ([]models.GroupsScenes, error) {
|
||||
ret := make([]models.GroupsScenes, len(input))
|
||||
|
||||
for i, v := range input {
|
||||
mID, err := strconv.Atoi(v.GroupID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid group ID: %s", v.GroupID)
|
||||
}
|
||||
|
||||
ret[i] = models.GroupsScenes{
|
||||
GroupID: mID,
|
||||
SceneIndex: v.SceneIndex,
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) relatedGroups(value []models.SceneGroupInput) (models.RelatedGroups, error) {
|
||||
groupsScenes, err := groupsScenesFromGroupInput(value)
|
||||
if err != nil {
|
||||
return models.RelatedGroups{}, err
|
||||
}
|
||||
|
||||
return models.NewRelatedGroups(groupsScenes), nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateGroupIDsFromMovies(value []models.SceneMovieInput, field string) (*models.UpdateGroupIDs, error) {
|
||||
if !t.hasField(field) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
moviesScenes, err := models.MoviesScenesFromInput(value)
|
||||
groupsScenes, err := models.GroupsScenesFromInput(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UpdateMovieIDs{
|
||||
Movies: moviesScenes,
|
||||
return &models.UpdateGroupIDs{
|
||||
Groups: groupsScenes,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateMovieIDsBulk(value *BulkUpdateIds, field string) (*models.UpdateMovieIDs, error) {
|
||||
func (t changesetTranslator) updateGroupIDs(value []models.SceneGroupInput, field string) (*models.UpdateGroupIDs, error) {
|
||||
if !t.hasField(field) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
groupsScenes, err := groupsScenesFromGroupInput(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UpdateGroupIDs{
|
||||
Groups: groupsScenes,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateGroupIDsBulk(value *BulkUpdateIds, field string) (*models.UpdateGroupIDs, error) {
|
||||
if !t.hasField(field) || value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -381,13 +424,74 @@ func (t changesetTranslator) updateMovieIDsBulk(value *BulkUpdateIds, field stri
|
||||
return nil, fmt.Errorf("converting ids [%v]: %w", value.Ids, err)
|
||||
}
|
||||
|
||||
movies := make([]models.MoviesScenes, len(ids))
|
||||
groups := make([]models.GroupsScenes, len(ids))
|
||||
for i, id := range ids {
|
||||
movies[i] = models.MoviesScenes{MovieID: id}
|
||||
groups[i] = models.GroupsScenes{GroupID: id}
|
||||
}
|
||||
|
||||
return &models.UpdateMovieIDs{
|
||||
Movies: movies,
|
||||
return &models.UpdateGroupIDs{
|
||||
Groups: groups,
|
||||
Mode: value.Mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func groupsDescriptionsFromGroupInput(input []*GroupDescriptionInput) ([]models.GroupIDDescription, error) {
|
||||
ret := make([]models.GroupIDDescription, len(input))
|
||||
|
||||
for i, v := range input {
|
||||
gID, err := strconv.Atoi(v.GroupID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid group ID: %s", v.GroupID)
|
||||
}
|
||||
|
||||
ret[i] = models.GroupIDDescription{
|
||||
GroupID: gID,
|
||||
}
|
||||
if v.Description != nil {
|
||||
ret[i].Description = *v.Description
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) groupIDDescriptions(value []*GroupDescriptionInput) (models.RelatedGroupDescriptions, error) {
|
||||
groupsScenes, err := groupsDescriptionsFromGroupInput(value)
|
||||
if err != nil {
|
||||
return models.RelatedGroupDescriptions{}, err
|
||||
}
|
||||
|
||||
return models.NewRelatedGroupDescriptions(groupsScenes), nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateGroupIDDescriptions(value []*GroupDescriptionInput, field string) (*models.UpdateGroupDescriptions, error) {
|
||||
if !t.hasField(field) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
groupsScenes, err := groupsDescriptionsFromGroupInput(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UpdateGroupDescriptions{
|
||||
Groups: groupsScenes,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateGroupIDDescriptionsBulk(value *BulkUpdateGroupDescriptionsInput, field string) (*models.UpdateGroupDescriptions, error) {
|
||||
if !t.hasField(field) || value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
groups, err := groupsDescriptionsFromGroupInput(value.Groups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UpdateGroupDescriptions{
|
||||
Groups: groups,
|
||||
Mode: value.Mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ package api
|
||||
type key int
|
||||
|
||||
const (
|
||||
// galleryKey key = 0
|
||||
performerKey key = iota + 1
|
||||
galleryKey key = 0
|
||||
performerKey
|
||||
sceneKey
|
||||
studioKey
|
||||
movieKey
|
||||
groupKey
|
||||
tagKey
|
||||
downloadKey
|
||||
imageKey
|
||||
|
||||
2
internal/api/doc.go
Normal file
2
internal/api/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package api provides the HTTP and Graphql API for the application.
|
||||
package api
|
||||
@@ -1,10 +1,14 @@
|
||||
// Package loaders contains the dataloaders used by the resolver in [api].
|
||||
// They are generated with `make generate-dataloaders`.
|
||||
// The dataloaders are used to batch requests to the database.
|
||||
|
||||
//go:generate go run github.com/vektah/dataloaden SceneLoader int *github.com/stashapp/stash/pkg/models.Scene
|
||||
//go:generate go run github.com/vektah/dataloaden GalleryLoader int *github.com/stashapp/stash/pkg/models.Gallery
|
||||
//go:generate go run github.com/vektah/dataloaden ImageLoader int *github.com/stashapp/stash/pkg/models.Image
|
||||
//go:generate go run github.com/vektah/dataloaden PerformerLoader int *github.com/stashapp/stash/pkg/models.Performer
|
||||
//go:generate go run github.com/vektah/dataloaden StudioLoader int *github.com/stashapp/stash/pkg/models.Studio
|
||||
//go:generate go run github.com/vektah/dataloaden TagLoader int *github.com/stashapp/stash/pkg/models.Tag
|
||||
//go:generate go run github.com/vektah/dataloaden MovieLoader int *github.com/stashapp/stash/pkg/models.Movie
|
||||
//go:generate go run github.com/vektah/dataloaden GroupLoader int *github.com/stashapp/stash/pkg/models.Group
|
||||
//go:generate go run github.com/vektah/dataloaden FileLoader github.com/stashapp/stash/pkg/models.FileID github.com/stashapp/stash/pkg/models.File
|
||||
//go:generate go run github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
@@ -52,7 +56,7 @@ type Loaders struct {
|
||||
PerformerByID *PerformerLoader
|
||||
StudioByID *StudioLoader
|
||||
TagByID *TagLoader
|
||||
MovieByID *MovieLoader
|
||||
GroupByID *GroupLoader
|
||||
FileByID *FileLoader
|
||||
}
|
||||
|
||||
@@ -94,10 +98,10 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchTags(ctx),
|
||||
},
|
||||
MovieByID: &MovieLoader{
|
||||
GroupByID: &GroupLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchMovies(ctx),
|
||||
fetch: m.fetchGroups(ctx),
|
||||
},
|
||||
FileByID: &FileLoader{
|
||||
wait: wait,
|
||||
@@ -232,11 +236,11 @@ func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.T
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models.Movie, []error) {
|
||||
return func(keys []int) (ret []*models.Movie, errs []error) {
|
||||
func (m Middleware) fetchGroups(ctx context.Context) func(keys []int) ([]*models.Group, []error) {
|
||||
return func(keys []int) (ret []*models.Group, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Movie.FindMany(ctx, keys)
|
||||
ret, err = m.Repository.Group.FindMany(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// MovieLoaderConfig captures the config to create a new MovieLoader
|
||||
type MovieLoaderConfig struct {
|
||||
// GroupLoaderConfig captures the config to create a new GroupLoader
|
||||
type GroupLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*models.Movie, []error)
|
||||
Fetch func(keys []int) ([]*models.Group, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
@@ -21,19 +21,19 @@ type MovieLoaderConfig struct {
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewMovieLoader creates a new MovieLoader given a fetch, wait, and maxBatch
|
||||
func NewMovieLoader(config MovieLoaderConfig) *MovieLoader {
|
||||
return &MovieLoader{
|
||||
// NewGroupLoader creates a new GroupLoader given a fetch, wait, and maxBatch
|
||||
func NewGroupLoader(config GroupLoaderConfig) *GroupLoader {
|
||||
return &GroupLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// MovieLoader batches and caches requests
|
||||
type MovieLoader struct {
|
||||
// GroupLoader batches and caches requests
|
||||
type GroupLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]*models.Movie, []error)
|
||||
fetch func(keys []int) ([]*models.Group, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
@@ -44,51 +44,51 @@ type MovieLoader struct {
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]*models.Movie
|
||||
cache map[int]*models.Group
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *movieLoaderBatch
|
||||
batch *groupLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type movieLoaderBatch struct {
|
||||
type groupLoaderBatch struct {
|
||||
keys []int
|
||||
data []*models.Movie
|
||||
data []*models.Group
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Movie by key, batching and caching will be applied automatically
|
||||
func (l *MovieLoader) Load(key int) (*models.Movie, error) {
|
||||
// Load a Group by key, batching and caching will be applied automatically
|
||||
func (l *GroupLoader) Load(key int) (*models.Group, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Movie.
|
||||
// LoadThunk returns a function that when called will block waiting for a Group.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *MovieLoader) LoadThunk(key int) func() (*models.Movie, error) {
|
||||
func (l *GroupLoader) LoadThunk(key int) func() (*models.Group, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (*models.Movie, error) {
|
||||
return func() (*models.Group, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &movieLoaderBatch{done: make(chan struct{})}
|
||||
l.batch = &groupLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (*models.Movie, error) {
|
||||
return func() (*models.Group, error) {
|
||||
<-batch.done
|
||||
|
||||
var data *models.Movie
|
||||
var data *models.Group
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
@@ -113,43 +113,43 @@ func (l *MovieLoader) LoadThunk(key int) func() (*models.Movie, error) {
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *MovieLoader) LoadAll(keys []int) ([]*models.Movie, []error) {
|
||||
results := make([]func() (*models.Movie, error), len(keys))
|
||||
func (l *GroupLoader) LoadAll(keys []int) ([]*models.Group, []error) {
|
||||
results := make([]func() (*models.Group, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
movies := make([]*models.Movie, len(keys))
|
||||
groups := make([]*models.Group, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
movies[i], errors[i] = thunk()
|
||||
groups[i], errors[i] = thunk()
|
||||
}
|
||||
return movies, errors
|
||||
return groups, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Movies.
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Groups.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *MovieLoader) LoadAllThunk(keys []int) func() ([]*models.Movie, []error) {
|
||||
results := make([]func() (*models.Movie, error), len(keys))
|
||||
func (l *GroupLoader) LoadAllThunk(keys []int) func() ([]*models.Group, []error) {
|
||||
results := make([]func() (*models.Group, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]*models.Movie, []error) {
|
||||
movies := make([]*models.Movie, len(keys))
|
||||
return func() ([]*models.Group, []error) {
|
||||
groups := make([]*models.Group, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
movies[i], errors[i] = thunk()
|
||||
groups[i], errors[i] = thunk()
|
||||
}
|
||||
return movies, errors
|
||||
return groups, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *MovieLoader) Prime(key int, value *models.Movie) bool {
|
||||
func (l *GroupLoader) Prime(key int, value *models.Group) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
@@ -163,22 +163,22 @@ func (l *MovieLoader) Prime(key int, value *models.Movie) bool {
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *MovieLoader) Clear(key int) {
|
||||
func (l *GroupLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *MovieLoader) unsafeSet(key int, value *models.Movie) {
|
||||
func (l *GroupLoader) unsafeSet(key int, value *models.Group) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]*models.Movie{}
|
||||
l.cache = map[int]*models.Group{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *movieLoaderBatch) keyIndex(l *MovieLoader, key int) int {
|
||||
func (b *groupLoaderBatch) keyIndex(l *GroupLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
@@ -202,7 +202,7 @@ func (b *movieLoaderBatch) keyIndex(l *MovieLoader, key int) int {
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *movieLoaderBatch) startTimer(l *MovieLoader) {
|
||||
func (b *groupLoaderBatch) startTimer(l *GroupLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
@@ -218,7 +218,7 @@ func (b *movieLoaderBatch) startTimer(l *MovieLoader) {
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *movieLoaderBatch) end(l *MovieLoader) {
|
||||
func (b *groupLoaderBatch) end(l *GroupLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
@@ -37,6 +37,7 @@ type Resolver struct {
|
||||
sceneService manager.SceneService
|
||||
imageService manager.ImageService
|
||||
galleryService manager.GalleryService
|
||||
groupService manager.GroupService
|
||||
|
||||
hookExecutor hookExecutor
|
||||
}
|
||||
@@ -72,9 +73,14 @@ func (r *Resolver) SceneMarker() SceneMarkerResolver {
|
||||
func (r *Resolver) Studio() StudioResolver {
|
||||
return &studioResolver{r}
|
||||
}
|
||||
func (r *Resolver) Movie() MovieResolver {
|
||||
return &movieResolver{r}
|
||||
|
||||
func (r *Resolver) Group() GroupResolver {
|
||||
return &groupResolver{r}
|
||||
}
|
||||
func (r *Resolver) Movie() MovieResolver {
|
||||
return &movieResolver{&groupResolver{r}}
|
||||
}
|
||||
|
||||
func (r *Resolver) Subscription() SubscriptionResolver {
|
||||
return &subscriptionResolver{r}
|
||||
}
|
||||
@@ -111,7 +117,11 @@ type sceneResolver struct{ *Resolver }
|
||||
type sceneMarkerResolver struct{ *Resolver }
|
||||
type imageResolver struct{ *Resolver }
|
||||
type studioResolver struct{ *Resolver }
|
||||
type movieResolver struct{ *Resolver }
|
||||
|
||||
// movie is group under the hood
|
||||
type groupResolver struct{ *Resolver }
|
||||
type movieResolver struct{ *groupResolver }
|
||||
|
||||
type tagResolver struct{ *Resolver }
|
||||
type galleryFileResolver struct{ *Resolver }
|
||||
type videoFileResolver struct{ *Resolver }
|
||||
@@ -173,7 +183,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
||||
galleryQB := repo.Gallery
|
||||
studioQB := repo.Studio
|
||||
performerQB := repo.Performer
|
||||
movieQB := repo.Movie
|
||||
movieQB := repo.Group
|
||||
tagQB := repo.Tag
|
||||
|
||||
// embrace the error
|
||||
@@ -218,7 +228,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
moviesCount, err := movieQB.Count(ctx)
|
||||
groupsCount, err := movieQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -262,7 +272,8 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
||||
GalleryCount: galleryCount,
|
||||
PerformerCount: performersCount,
|
||||
StudioCount: studiosCount,
|
||||
MovieCount: moviesCount,
|
||||
GroupCount: groupsCount,
|
||||
MovieCount: groupsCount,
|
||||
TagCount: tagsCount,
|
||||
TotalOCount: totalOCount,
|
||||
TotalPlayDuration: totalPlayDuration,
|
||||
|
||||
@@ -2,8 +2,10 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
@@ -189,3 +191,28 @@ func (r *galleryResolver) Urls(ctx context.Context, obj *models.Gallery) ([]stri
|
||||
|
||||
return obj.URLs.List(), nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Paths(ctx context.Context, obj *models.Gallery) (*GalleryPathsType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
builder := urlbuilders.NewGalleryURLBuilder(baseURL, obj)
|
||||
|
||||
return &GalleryPathsType{
|
||||
Cover: builder.GetCoverURL(),
|
||||
Preview: builder.GetPreviewURL(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Image(ctx context.Context, obj *models.Gallery, index int) (ret *models.Image, err error) {
|
||||
if index < 0 {
|
||||
return nil, fmt.Errorf("index must >= 0")
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Image.FindByGalleryIDIndex(ctx, obj.ID, uint(index))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
)
|
||||
|
||||
func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
func (r *groupResolver) Date(ctx context.Context, obj *models.Group) (*string, error) {
|
||||
if obj.Date != nil {
|
||||
result := obj.Date.String()
|
||||
return &result, nil
|
||||
@@ -16,11 +18,40 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
func (r *groupResolver) Rating100(ctx context.Context, obj *models.Group) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *models.Studio, err error) {
|
||||
func (r *groupResolver) URL(ctx context.Context, obj *models.Group) (*string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Group)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
urls := obj.URLs.List()
|
||||
if len(urls) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &urls[0], nil
|
||||
}
|
||||
|
||||
func (r *groupResolver) Urls(ctx context.Context, obj *models.Group) ([]string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Group)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return obj.URLs.List(), nil
|
||||
}
|
||||
|
||||
func (r *groupResolver) Studio(ctx context.Context, obj *models.Group) (ret *models.Studio, err error) {
|
||||
if obj.StudioID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -28,26 +59,102 @@ func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *mod
|
||||
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
|
||||
}
|
||||
|
||||
func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
func (r groupResolver) Tags(ctx context.Context, obj *models.Group) (ret []*models.Tag, err error) {
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadTagIDs(ctx, r.repository.Group)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r groupResolver) relatedGroups(ctx context.Context, rgd models.RelatedGroupDescriptions) (ret []*GroupDescription, err error) {
|
||||
// rgd must be loaded
|
||||
gds := rgd.List()
|
||||
ids := make([]int, len(gds))
|
||||
for i, gd := range gds {
|
||||
ids[i] = gd.GroupID
|
||||
}
|
||||
|
||||
groups, errs := loaders.From(ctx).GroupByID.LoadAll(ids)
|
||||
|
||||
err = firstError(errs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ret = make([]*GroupDescription, len(groups))
|
||||
for i, group := range groups {
|
||||
ret[i] = &GroupDescription{Group: group}
|
||||
d := gds[i].Description
|
||||
if d != "" {
|
||||
ret[i].Description = &d
|
||||
}
|
||||
}
|
||||
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r groupResolver) ContainingGroups(ctx context.Context, obj *models.Group) (ret []*GroupDescription, err error) {
|
||||
if !obj.ContainingGroups.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadContainingGroupIDs(ctx, r.repository.Group)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return r.relatedGroups(ctx, obj.ContainingGroups)
|
||||
}
|
||||
|
||||
func (r groupResolver) SubGroups(ctx context.Context, obj *models.Group) (ret []*GroupDescription, err error) {
|
||||
if !obj.SubGroups.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadSubGroupIDs(ctx, r.repository.Group)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return r.relatedGroups(ctx, obj.SubGroups)
|
||||
}
|
||||
|
||||
func (r *groupResolver) SubGroupCount(ctx context.Context, obj *models.Group, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = group.CountByContainingGroupID(ctx, r.repository.Group, obj.ID, depth)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *groupResolver) FrontImagePath(ctx context.Context, obj *models.Group) (*string, error) {
|
||||
var hasImage bool
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
hasImage, err = r.repository.Movie.HasFrontImage(ctx, obj.ID)
|
||||
hasImage, err = r.repository.Group.HasFrontImage(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
imagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieFrontImageURL(hasImage)
|
||||
imagePath := urlbuilders.NewGroupURLBuilder(baseURL, obj).GetGroupFrontImageURL(hasImage)
|
||||
return &imagePath, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
func (r *groupResolver) BackImagePath(ctx context.Context, obj *models.Group) (*string, error) {
|
||||
var hasImage bool
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
hasImage, err = r.repository.Movie.HasBackImage(ctx, obj.ID)
|
||||
hasImage, err = r.repository.Group.HasBackImage(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -59,13 +166,13 @@ func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*
|
||||
}
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
imagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieBackImageURL()
|
||||
imagePath := urlbuilders.NewGroupURLBuilder(baseURL, obj).GetGroupBackImageURL()
|
||||
return &imagePath, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret int, err error) {
|
||||
func (r *groupResolver) SceneCount(ctx context.Context, obj *models.Group, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Scene.CountByMovieID(ctx, obj.ID)
|
||||
ret, err = scene.CountByGroupID(ctx, r.repository.Scene, obj.ID, depth)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -74,10 +181,10 @@ func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Scenes(ctx context.Context, obj *models.Movie) (ret []*models.Scene, err error) {
|
||||
func (r *groupResolver) Scenes(ctx context.Context, obj *models.Group) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = r.repository.Scene.FindByMovieID(ctx, obj.ID)
|
||||
ret, err = r.repository.Scene.FindByGroupID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -24,6 +24,79 @@ func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer
|
||||
return obj.Aliases.List(), nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
urls := obj.URLs.List()
|
||||
if len(urls) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &urls[0], nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
urls := obj.URLs.List()
|
||||
|
||||
// find the first twitter url
|
||||
for _, url := range urls {
|
||||
if performer.IsTwitterURL(url) {
|
||||
u := url
|
||||
return &u, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Instagram(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
urls := obj.URLs.List()
|
||||
|
||||
// find the first instagram url
|
||||
for _, url := range urls {
|
||||
if performer.IsInstagramURL(url) {
|
||||
u := url
|
||||
return &u, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Urls(ctx context.Context, obj *models.Performer) ([]string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return obj.URLs.List(), nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Height != nil {
|
||||
ret := strconv.Itoa(*obj.Height)
|
||||
@@ -106,9 +179,9 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||
func (r *performerResolver) GroupCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
|
||||
ret, err = r.repository.Group.CountByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -117,6 +190,11 @@ func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performe
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||
return r.GroupCount(ctx, obj)
|
||||
}
|
||||
|
||||
func (r *performerResolver) PerformerCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = performer.CountByAppearsWith(ctx, r.repository.Performer, obj.ID)
|
||||
@@ -179,9 +257,9 @@ func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
|
||||
func (r *performerResolver) Groups(ctx context.Context, obj *models.Performer) (ret []*models.Group, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
|
||||
ret, err = r.repository.Group.FindByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -189,3 +267,8 @@ func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Group, err error) {
|
||||
return r.Groups(ctx, obj)
|
||||
}
|
||||
|
||||
@@ -184,20 +184,20 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) {
|
||||
if !obj.Movies.Loaded() {
|
||||
if !obj.Groups.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
return obj.LoadMovies(ctx, qb)
|
||||
return obj.LoadGroups(ctx, qb)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
loader := loaders.From(ctx).MovieByID
|
||||
loader := loaders.From(ctx).GroupByID
|
||||
|
||||
for _, sm := range obj.Movies.List() {
|
||||
movie, err := loader.Load(sm.MovieID)
|
||||
for _, sm := range obj.Groups.List() {
|
||||
movie, err := loader.Load(sm.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -214,6 +214,37 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Groups(ctx context.Context, obj *models.Scene) (ret []*SceneGroup, err error) {
|
||||
if !obj.Groups.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
return obj.LoadGroups(ctx, qb)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
loader := loaders.From(ctx).GroupByID
|
||||
|
||||
for _, sm := range obj.Groups.List() {
|
||||
group, err := loader.Load(sm.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sceneIdx := sm.SceneIndex
|
||||
sceneGroup := &SceneGroup{
|
||||
Group: group,
|
||||
SceneIndex: sceneIdx,
|
||||
}
|
||||
|
||||
ret = append(ret, sceneGroup)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/movie"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
)
|
||||
@@ -40,6 +40,20 @@ func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) ([]str
|
||||
return obj.Aliases.List(), nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Tags(ctx context.Context, obj *models.Studio) (ret []*models.Tag, err error) {
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadTagIDs(ctx, r.repository.Studio)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = scene.CountByStudioID(ctx, r.repository.Scene, obj.ID, depth)
|
||||
@@ -84,9 +98,9 @@ func (r *studioResolver) PerformerCount(ctx context.Context, obj *models.Studio,
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
||||
func (r *studioResolver) GroupCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = movie.CountByStudioID(ctx, r.repository.Movie, obj.ID, depth)
|
||||
ret, err = group.CountByStudioID(ctx, r.repository.Group, obj.ID, depth)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -95,6 +109,11 @@ func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, dep
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
||||
return r.GroupCount(ctx, obj, depth)
|
||||
}
|
||||
|
||||
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
|
||||
if obj.ParentID == nil {
|
||||
return nil, nil
|
||||
@@ -130,9 +149,9 @@ func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*in
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
|
||||
func (r *studioResolver) Groups(ctx context.Context, obj *models.Studio) (ret []*models.Group, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
|
||||
ret, err = r.repository.Group.FindByStudioID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -140,3 +159,8 @@ func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Group, err error) {
|
||||
return r.Groups(ctx, obj)
|
||||
}
|
||||
|
||||
@@ -3,45 +3,55 @@ package api
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/studio"
|
||||
)
|
||||
|
||||
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.FindByChildTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.ParentIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadParentIDs(ctx, r.repository.Tag)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.ParentIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.FindByParentTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.ChildIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadChildIDs(ctx, r.repository.Tag)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.ChildIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []string, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.GetAliases(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.Aliases.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadAliases(ctx, r.repository.Tag)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, err
|
||||
return obj.Aliases.List(), nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||
@@ -99,6 +109,32 @@ func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag, depth
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) StudioCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = studio.CountByTagID(ctx, r.repository.Studio, obj.ID, depth)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) GroupCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = group.CountByTagID(ctx, r.repository.Group, obj.ID, depth)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) MovieCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||
return r.GroupCount(ctx, obj, depth)
|
||||
}
|
||||
|
||||
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
|
||||
var hasImage bool
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
|
||||
@@ -478,6 +478,61 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input Galler
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SetGalleryCover(ctx context.Context, input GallerySetCoverInput) (bool, error) {
|
||||
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting gallery id: %w", err)
|
||||
}
|
||||
|
||||
coverImageID, err := strconv.Atoi(input.CoverImageID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting cover image id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Gallery
|
||||
gallery, err := qb.Find(ctx, galleryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
return fmt.Errorf("gallery with id %d not found", galleryID)
|
||||
}
|
||||
|
||||
return r.galleryService.SetCover(ctx, gallery, coverImageID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ResetGalleryCover(ctx context.Context, input GalleryResetCoverInput) (bool, error) {
|
||||
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting gallery id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Gallery
|
||||
gallery, err := qb.Find(ctx, galleryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
return fmt.Errorf("gallery with id %d not found", galleryID)
|
||||
}
|
||||
|
||||
return r.galleryService.ResetCover(ctx, gallery)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret *models.GalleryChapter, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.GalleryChapter.Find(ctx, id)
|
||||
|
||||
413
internal/api/resolver_mutation_group.go
Normal file
413
internal/api/resolver_mutation_group.go
Normal file
@@ -0,0 +1,413 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate a new group from the input
|
||||
newGroup := models.NewGroup()
|
||||
|
||||
newGroup.Name = input.Name
|
||||
newGroup.Aliases = translator.string(input.Aliases)
|
||||
newGroup.Duration = input.Duration
|
||||
newGroup.Rating = input.Rating100
|
||||
newGroup.Director = translator.string(input.Director)
|
||||
newGroup.Synopsis = translator.string(input.Synopsis)
|
||||
|
||||
var err error
|
||||
|
||||
newGroup.Date, err = translator.datePtr(input.Date)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting date: %w", err)
|
||||
}
|
||||
newGroup.StudioID, err = translator.intPtrFromString(input.StudioID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
newGroup.TagIDs, err = translator.relatedIds(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
newGroup.ContainingGroups, err = translator.groupIDDescriptions(input.ContainingGroups)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting containing group ids: %w", err)
|
||||
}
|
||||
|
||||
newGroup.SubGroups, err = translator.groupIDDescriptions(input.SubGroups)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting containing group ids: %w", err)
|
||||
}
|
||||
|
||||
if input.Urls != nil {
|
||||
newGroup.URLs = models.NewRelatedStrings(input.Urls)
|
||||
}
|
||||
|
||||
return &newGroup, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
|
||||
newGroup, err := groupFromGroupCreateInput(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var frontimageData []byte
|
||||
if input.FrontImage != nil {
|
||||
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing front image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var backimageData []byte
|
||||
if input.BackImage != nil {
|
||||
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing back image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: if back image is being set, set the front image to the default.
|
||||
// This is because we can't have a null front image with a non-null back image.
|
||||
if len(frontimageData) == 0 && len(backimageData) != 0 {
|
||||
frontimageData = static.ReadAll(static.DefaultGroupImage)
|
||||
}
|
||||
|
||||
// Start the transaction and save the group
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err = r.groupService.Create(ctx, newGroup, frontimageData, backimageData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.GroupCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.MovieCreatePost, input, nil)
|
||||
return r.getGroup(ctx, newGroup.ID)
|
||||
}
|
||||
|
||||
func groupPartialFromGroupUpdateInput(translator changesetTranslator, input GroupUpdateInput) (ret models.GroupPartial, err error) {
|
||||
// Populate group from the input
|
||||
updatedGroup := models.NewGroupPartial()
|
||||
|
||||
updatedGroup.Name = translator.optionalString(input.Name, "name")
|
||||
updatedGroup.Aliases = translator.optionalString(input.Aliases, "aliases")
|
||||
updatedGroup.Duration = translator.optionalInt(input.Duration, "duration")
|
||||
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGroup.Director = translator.optionalString(input.Director, "director")
|
||||
updatedGroup.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
|
||||
|
||||
updatedGroup.Date, err = translator.optionalDate(input.Date, "date")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting date: %w", err)
|
||||
return
|
||||
}
|
||||
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting studio id: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting tag ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.ContainingGroups, err = translator.updateGroupIDDescriptions(input.ContainingGroups, "containing_groups")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting containing group ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.SubGroups, err = translator.updateGroupIDDescriptions(input.SubGroups, "sub_groups")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting containing group ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.URLs = translator.updateStrings(input.Urls, "urls")
|
||||
|
||||
return updatedGroup, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupUpdate(ctx context.Context, input GroupUpdateInput) (*models.Group, error) {
|
||||
groupID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
updatedGroup, err := groupPartialFromGroupUpdateInput(translator, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var frontimageData []byte
|
||||
frontImageIncluded := translator.hasField("front_image")
|
||||
if input.FrontImage != nil {
|
||||
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing front image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var backimageData []byte
|
||||
backImageIncluded := translator.hasField("back_image")
|
||||
if input.BackImage != nil {
|
||||
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing back image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
frontImage := group.ImageInput{
|
||||
Image: frontimageData,
|
||||
Set: frontImageIncluded,
|
||||
}
|
||||
|
||||
backImage := group.ImageInput{
|
||||
Image: backimageData,
|
||||
Set: backImageIncluded,
|
||||
}
|
||||
|
||||
_, err = r.groupService.UpdatePartial(ctx, groupID, updatedGroup, frontImage, backImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, groupID, hook.GroupUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, groupID, hook.MovieUpdatePost, input, translator.getFields())
|
||||
return r.getGroup(ctx, groupID)
|
||||
}
|
||||
|
||||
func groupPartialFromBulkGroupUpdateInput(translator changesetTranslator, input BulkGroupUpdateInput) (ret models.GroupPartial, err error) {
|
||||
updatedGroup := models.NewGroupPartial()
|
||||
|
||||
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGroup.Director = translator.optionalString(input.Director, "director")
|
||||
|
||||
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting studio id: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting tag ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.ContainingGroups, err = translator.updateGroupIDDescriptionsBulk(input.ContainingGroups, "containing_groups")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting containing group ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.SubGroups, err = translator.updateGroupIDDescriptionsBulk(input.SubGroups, "sub_groups")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting containing group ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.URLs = translator.optionalURLsBulk(input.Urls, nil)
|
||||
|
||||
return updatedGroup, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkGroupUpdate(ctx context.Context, input BulkGroupUpdateInput) ([]*models.Group, error) {
|
||||
groupIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate group from the input
|
||||
updatedGroup, err := groupPartialFromBulkGroupUpdateInput(translator, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := []*models.Group{}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
for _, groupID := range groupIDs {
|
||||
group, err := r.groupService.UpdatePartial(ctx, groupID, updatedGroup, group.ImageInput{}, group.ImageInput{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, group)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var newRet []*models.Group
|
||||
for _, group := range ret {
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.GroupUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||
|
||||
group, err = r.getGroup(ctx, group.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newRet = append(newRet, group)
|
||||
}
|
||||
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupDestroy(ctx context.Context, input GroupDestroyInput) (bool, error) {
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.repository.Group.Destroy(ctx, id)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupsDestroy(ctx context.Context, groupIDs []string) (bool, error) {
|
||||
ids, err := stringslice.StringSliceToIntSlice(groupIDs)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Group
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, groupIDs, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, groupIDs, nil)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddGroupSubGroups(ctx context.Context, input GroupSubGroupAddInput) (bool, error) {
|
||||
groupID, err := strconv.Atoi(input.ContainingGroupID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting group id: %w", err)
|
||||
}
|
||||
|
||||
subGroups, err := groupsDescriptionsFromGroupInput(input.SubGroups)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting sub group ids: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.groupService.AddSubGroups(ctx, groupID, subGroups, input.InsertIndex)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveGroupSubGroups(ctx context.Context, input GroupSubGroupRemoveInput) (bool, error) {
|
||||
groupID, err := strconv.Atoi(input.ContainingGroupID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting group id: %w", err)
|
||||
}
|
||||
|
||||
subGroupIDs, err := stringslice.StringSliceToIntSlice(input.SubGroupIds)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting sub group ids: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.groupService.RemoveSubGroups(ctx, groupID, subGroupIDs)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ReorderSubGroups(ctx context.Context, input ReorderSubGroupsInput) (bool, error) {
|
||||
groupID, err := strconv.Atoi(input.GroupID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting group id: %w", err)
|
||||
}
|
||||
|
||||
subGroupIDs, err := stringslice.StringSliceToIntSlice(input.SubGroupIds)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting sub group ids: %w", err)
|
||||
}
|
||||
|
||||
insertPointID, err := strconv.Atoi(input.InsertAtID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting insert at id: %w", err)
|
||||
}
|
||||
|
||||
insertAfter := utils.IsTrue(input.InsertAfter)
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.groupService.ReorderSubGroups(ctx, groupID, subGroupIDs, insertPointID, insertAfter)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
// used to refetch movie after hooks run
|
||||
func (r *mutationResolver) getMovie(ctx context.Context, id int) (ret *models.Movie, err error) {
|
||||
// used to refetch group after hooks run
|
||||
func (r *mutationResolver) getGroup(ctx context.Context, id int) (ret *models.Group, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.Find(ctx, id)
|
||||
ret, err = r.repository.Group.Find(ctx, id)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -24,33 +24,43 @@ func (r *mutationResolver) getMovie(ctx context.Context, id int) (ret *models.Mo
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInput) (*models.Movie, error) {
|
||||
func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInput) (*models.Group, error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate a new movie from the input
|
||||
newMovie := models.NewMovie()
|
||||
// Populate a new group from the input
|
||||
newGroup := models.NewGroup()
|
||||
|
||||
newMovie.Name = input.Name
|
||||
newMovie.Aliases = translator.string(input.Aliases)
|
||||
newMovie.Duration = input.Duration
|
||||
newMovie.Rating = input.Rating100
|
||||
newMovie.Director = translator.string(input.Director)
|
||||
newMovie.Synopsis = translator.string(input.Synopsis)
|
||||
newMovie.URL = translator.string(input.URL)
|
||||
newGroup.Name = input.Name
|
||||
newGroup.Aliases = translator.string(input.Aliases)
|
||||
newGroup.Duration = input.Duration
|
||||
newGroup.Rating = input.Rating100
|
||||
newGroup.Director = translator.string(input.Director)
|
||||
newGroup.Synopsis = translator.string(input.Synopsis)
|
||||
|
||||
var err error
|
||||
|
||||
newMovie.Date, err = translator.datePtr(input.Date)
|
||||
newGroup.Date, err = translator.datePtr(input.Date)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting date: %w", err)
|
||||
}
|
||||
newMovie.StudioID, err = translator.intPtrFromString(input.StudioID)
|
||||
newGroup.StudioID, err = translator.intPtrFromString(input.StudioID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
newGroup.TagIDs, err = translator.relatedIds(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
if input.Urls != nil {
|
||||
newGroup.URLs = models.NewRelatedStrings(input.Urls)
|
||||
} else if input.URL != nil {
|
||||
newGroup.URLs = models.NewRelatedStrings([]string{*input.URL})
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var frontimageData []byte
|
||||
if input.FrontImage != nil {
|
||||
@@ -72,27 +82,27 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
|
||||
// HACK: if back image is being set, set the front image to the default.
|
||||
// This is because we can't have a null front image with a non-null back image.
|
||||
if len(frontimageData) == 0 && len(backimageData) != 0 {
|
||||
frontimageData = static.ReadAll(static.DefaultMovieImage)
|
||||
frontimageData = static.ReadAll(static.DefaultGroupImage)
|
||||
}
|
||||
|
||||
// Start the transaction and save the movie
|
||||
// Start the transaction and save the group
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Movie
|
||||
qb := r.repository.Group
|
||||
|
||||
err = qb.Create(ctx, &newMovie)
|
||||
err = qb.Create(ctx, &newGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(frontimageData) > 0 {
|
||||
if err := qb.UpdateFrontImage(ctx, newMovie.ID, frontimageData); err != nil {
|
||||
if err := qb.UpdateFrontImage(ctx, newGroup.ID, frontimageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(backimageData) > 0 {
|
||||
if err := qb.UpdateBackImage(ctx, newMovie.ID, backimageData); err != nil {
|
||||
if err := qb.UpdateBackImage(ctx, newGroup.ID, backimageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -102,12 +112,14 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, hook.MovieCreatePost, input, nil)
|
||||
return r.getMovie(ctx, newMovie.ID)
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.GroupCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.MovieCreatePost, input, nil)
|
||||
return r.getGroup(ctx, newGroup.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInput) (*models.Movie, error) {
|
||||
movieID, err := strconv.Atoi(input.ID)
|
||||
func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInput) (*models.Group, error) {
|
||||
groupID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
@@ -116,26 +128,32 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate movie from the input
|
||||
updatedMovie := models.NewMoviePartial()
|
||||
// Populate group from the input
|
||||
updatedGroup := models.NewGroupPartial()
|
||||
|
||||
updatedMovie.Name = translator.optionalString(input.Name, "name")
|
||||
updatedMovie.Aliases = translator.optionalString(input.Aliases, "aliases")
|
||||
updatedMovie.Duration = translator.optionalInt(input.Duration, "duration")
|
||||
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedMovie.Director = translator.optionalString(input.Director, "director")
|
||||
updatedMovie.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
|
||||
updatedMovie.URL = translator.optionalString(input.URL, "url")
|
||||
updatedGroup.Name = translator.optionalString(input.Name, "name")
|
||||
updatedGroup.Aliases = translator.optionalString(input.Aliases, "aliases")
|
||||
updatedGroup.Duration = translator.optionalInt(input.Duration, "duration")
|
||||
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGroup.Director = translator.optionalString(input.Director, "director")
|
||||
updatedGroup.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
|
||||
|
||||
updatedMovie.Date, err = translator.optionalDate(input.Date, "date")
|
||||
updatedGroup.Date, err = translator.optionalDate(input.Date, "date")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting date: %w", err)
|
||||
}
|
||||
updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
updatedGroup.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
updatedGroup.URLs = translator.optionalURLs(input.Urls, input.URL)
|
||||
|
||||
var frontimageData []byte
|
||||
frontImageIncluded := translator.hasField("front_image")
|
||||
if input.FrontImage != nil {
|
||||
@@ -154,24 +172,24 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
|
||||
}
|
||||
}
|
||||
|
||||
// Start the transaction and save the movie
|
||||
var movie *models.Movie
|
||||
// Start the transaction and save the group
|
||||
var group *models.Group
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Movie
|
||||
movie, err = qb.UpdatePartial(ctx, movieID, updatedMovie)
|
||||
qb := r.repository.Group
|
||||
group, err = qb.UpdatePartial(ctx, groupID, updatedGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if frontImageIncluded {
|
||||
if err := qb.UpdateFrontImage(ctx, movie.ID, frontimageData); err != nil {
|
||||
if err := qb.UpdateFrontImage(ctx, group.ID, frontimageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if backImageIncluded {
|
||||
if err := qb.UpdateBackImage(ctx, movie.ID, backimageData); err != nil {
|
||||
if err := qb.UpdateBackImage(ctx, group.ID, backimageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -181,12 +199,14 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||
return r.getMovie(ctx, movie.ID)
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.GroupUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||
return r.getGroup(ctx, group.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieUpdateInput) ([]*models.Movie, error) {
|
||||
movieIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieUpdateInput) ([]*models.Group, error) {
|
||||
groupIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
@@ -195,29 +215,36 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate movie from the input
|
||||
updatedMovie := models.NewMoviePartial()
|
||||
// Populate group from the input
|
||||
updatedGroup := models.NewGroupPartial()
|
||||
|
||||
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedMovie.Director = translator.optionalString(input.Director, "director")
|
||||
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGroup.Director = translator.optionalString(input.Director, "director")
|
||||
|
||||
updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
ret := []*models.Movie{}
|
||||
updatedGroup.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
updatedGroup.URLs = translator.optionalURLsBulk(input.Urls, nil)
|
||||
|
||||
ret := []*models.Group{}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Movie
|
||||
qb := r.repository.Group
|
||||
|
||||
for _, movieID := range movieIDs {
|
||||
movie, err := qb.UpdatePartial(ctx, movieID, updatedMovie)
|
||||
for _, groupID := range groupIDs {
|
||||
group, err := qb.UpdatePartial(ctx, groupID, updatedGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, movie)
|
||||
ret = append(ret, group)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -225,16 +252,18 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var newRet []*models.Movie
|
||||
for _, movie := range ret {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||
var newRet []*models.Group
|
||||
for _, group := range ret {
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.GroupUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||
|
||||
movie, err = r.getMovie(ctx, movie.ID)
|
||||
group, err = r.getGroup(ctx, group.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newRet = append(newRet, movie)
|
||||
newRet = append(newRet, group)
|
||||
}
|
||||
|
||||
return newRet, nil
|
||||
@@ -247,24 +276,26 @@ func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyI
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.repository.Movie.Destroy(ctx, id)
|
||||
return r.repository.Group.Destroy(ctx, id)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string) (bool, error) {
|
||||
ids, err := stringslice.StringSliceToIntSlice(movieIDs)
|
||||
func (r *mutationResolver) MoviesDestroy(ctx context.Context, groupIDs []string) (bool, error) {
|
||||
ids, err := stringslice.StringSliceToIntSlice(groupIDs)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Movie
|
||||
qb := r.repository.Group
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(ctx, id); err != nil {
|
||||
return err
|
||||
@@ -277,7 +308,9 @@ func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string)
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, movieIDs, nil)
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, groupIDs, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, groupIDs, nil)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
@@ -12,6 +12,11 @@ import (
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
twitterURL = "https://twitter.com"
|
||||
instagramURL = "https://instagram.com"
|
||||
)
|
||||
|
||||
// used to refetch performer after hooks run
|
||||
func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *models.Performer, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -35,7 +40,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
newPerformer.Name = input.Name
|
||||
newPerformer.Disambiguation = translator.string(input.Disambiguation)
|
||||
newPerformer.Aliases = models.NewRelatedStrings(input.AliasList)
|
||||
newPerformer.URL = translator.string(input.URL)
|
||||
newPerformer.Gender = input.Gender
|
||||
newPerformer.Ethnicity = translator.string(input.Ethnicity)
|
||||
newPerformer.Country = translator.string(input.Country)
|
||||
@@ -47,8 +51,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
newPerformer.CareerLength = translator.string(input.CareerLength)
|
||||
newPerformer.Tattoos = translator.string(input.Tattoos)
|
||||
newPerformer.Piercings = translator.string(input.Piercings)
|
||||
newPerformer.Twitter = translator.string(input.Twitter)
|
||||
newPerformer.Instagram = translator.string(input.Instagram)
|
||||
newPerformer.Favorite = translator.bool(input.Favorite)
|
||||
newPerformer.Rating = input.Rating100
|
||||
newPerformer.Details = translator.string(input.Details)
|
||||
@@ -58,6 +60,21 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
newPerformer.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds)
|
||||
|
||||
newPerformer.URLs = models.NewRelatedStrings([]string{})
|
||||
if input.URL != nil {
|
||||
newPerformer.URLs.Add(*input.URL)
|
||||
}
|
||||
if input.Twitter != nil {
|
||||
newPerformer.URLs.Add(utils.URLFromHandle(*input.Twitter, twitterURL))
|
||||
}
|
||||
if input.Instagram != nil {
|
||||
newPerformer.URLs.Add(utils.URLFromHandle(*input.Instagram, instagramURL))
|
||||
}
|
||||
|
||||
if input.Urls != nil {
|
||||
newPerformer.URLs.Add(input.Urls...)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
newPerformer.Birthdate, err = translator.datePtr(input.Birthdate)
|
||||
@@ -112,6 +129,96 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
return r.getPerformer(ctx, newPerformer.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) validateNoLegacyURLs(translator changesetTranslator) error {
|
||||
// ensure url/twitter/instagram are not included in the input
|
||||
if translator.hasField("url") {
|
||||
return fmt.Errorf("url field must not be included if urls is included")
|
||||
}
|
||||
if translator.hasField("twitter") {
|
||||
return fmt.Errorf("twitter field must not be included if urls is included")
|
||||
}
|
||||
if translator.hasField("instagram") {
|
||||
return fmt.Errorf("instagram field must not be included if urls is included")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) handleLegacyURLs(ctx context.Context, performerID int, legacyURL, legacyTwitter, legacyInstagram models.OptionalString, updatedPerformer *models.PerformerPartial) error {
|
||||
qb := r.repository.Performer
|
||||
|
||||
// we need to be careful with URL/Twitter/Instagram
|
||||
// treat URL as replacing the first non-Twitter/Instagram URL in the list
|
||||
// twitter should replace any existing twitter URL
|
||||
// instagram should replace any existing instagram URL
|
||||
p, err := qb.Find(ctx, performerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.LoadURLs(ctx, qb); err != nil {
|
||||
return fmt.Errorf("loading performer URLs: %w", err)
|
||||
}
|
||||
|
||||
existingURLs := p.URLs.List()
|
||||
|
||||
// performer partial URLs should be empty
|
||||
if legacyURL.Set {
|
||||
replaced := false
|
||||
for i, url := range existingURLs {
|
||||
if !performer.IsTwitterURL(url) && !performer.IsInstagramURL(url) {
|
||||
existingURLs[i] = legacyURL.Value
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !replaced {
|
||||
existingURLs = append(existingURLs, legacyURL.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if legacyTwitter.Set {
|
||||
value := utils.URLFromHandle(legacyTwitter.Value, twitterURL)
|
||||
found := false
|
||||
// find and replace the first twitter URL
|
||||
for i, url := range existingURLs {
|
||||
if performer.IsTwitterURL(url) {
|
||||
existingURLs[i] = value
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
existingURLs = append(existingURLs, value)
|
||||
}
|
||||
}
|
||||
if legacyInstagram.Set {
|
||||
found := false
|
||||
value := utils.URLFromHandle(legacyInstagram.Value, instagramURL)
|
||||
// find and replace the first instagram URL
|
||||
for i, url := range existingURLs {
|
||||
if performer.IsInstagramURL(url) {
|
||||
existingURLs[i] = value
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
existingURLs = append(existingURLs, value)
|
||||
}
|
||||
}
|
||||
|
||||
updatedPerformer.URLs = &models.UpdateStrings{
|
||||
Values: existingURLs,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) {
|
||||
performerID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -127,7 +234,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
|
||||
updatedPerformer.Name = translator.optionalString(input.Name, "name")
|
||||
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
|
||||
updatedPerformer.URL = translator.optionalString(input.URL, "url")
|
||||
updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender")
|
||||
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
|
||||
updatedPerformer.Country = translator.optionalString(input.Country, "country")
|
||||
@@ -139,8 +245,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
|
||||
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
|
||||
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedPerformer.Details = translator.optionalString(input.Details, "details")
|
||||
@@ -149,6 +253,19 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
updatedPerformer.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
|
||||
|
||||
if translator.hasField("urls") {
|
||||
// ensure url/twitter/instagram are not included in the input
|
||||
if err := r.validateNoLegacyURLs(translator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedPerformer.URLs = translator.updateStrings(input.Urls, "urls")
|
||||
}
|
||||
|
||||
legacyURL := translator.optionalString(input.URL, "url")
|
||||
legacyTwitter := translator.optionalString(input.Twitter, "twitter")
|
||||
legacyInstagram := translator.optionalString(input.Instagram, "instagram")
|
||||
|
||||
updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting birthdate: %w", err)
|
||||
@@ -186,6 +303,12 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Performer
|
||||
|
||||
if legacyURL.Set || legacyTwitter.Set || legacyInstagram.Set {
|
||||
if err := r.handleLegacyURLs(ctx, performerID, legacyURL, legacyTwitter, legacyInstagram, &updatedPerformer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -225,7 +348,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
updatedPerformer := models.NewPerformerPartial()
|
||||
|
||||
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
|
||||
updatedPerformer.URL = translator.optionalString(input.URL, "url")
|
||||
|
||||
updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender")
|
||||
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
|
||||
updatedPerformer.Country = translator.optionalString(input.Country, "country")
|
||||
@@ -237,8 +360,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
|
||||
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
|
||||
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
|
||||
|
||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedPerformer.Details = translator.optionalString(input.Details, "details")
|
||||
@@ -246,6 +368,19 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
|
||||
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
|
||||
if translator.hasField("urls") {
|
||||
// ensure url/twitter/instagram are not included in the input
|
||||
if err := r.validateNoLegacyURLs(translator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedPerformer.URLs = translator.updateStringsBulk(input.Urls, "urls")
|
||||
}
|
||||
|
||||
legacyURL := translator.optionalString(input.URL, "url")
|
||||
legacyTwitter := translator.optionalString(input.Twitter, "twitter")
|
||||
legacyInstagram := translator.optionalString(input.Instagram, "instagram")
|
||||
|
||||
updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting birthdate: %w", err)
|
||||
@@ -277,6 +412,12 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
qb := r.repository.Performer
|
||||
|
||||
for _, performerID := range performerIDs {
|
||||
if legacyURL.Set || legacyTwitter.Set || legacyInstagram.Set {
|
||||
if err := r.handleLegacyURLs(ctx, performerID, legacyURL, legacyTwitter, legacyInstagram, &updatedPerformer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput) (ret *models.SavedFilter, err error) {
|
||||
@@ -67,30 +70,48 @@ func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input Destroy
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaultFilterInput) (bool, error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.SavedFilter
|
||||
// deprecated - write to the config in the meantime
|
||||
config := config.GetInstance()
|
||||
|
||||
if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil {
|
||||
// clearing
|
||||
def, err := qb.FindDefault(ctx, input.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uiConfig := config.GetUIConfiguration()
|
||||
if uiConfig == nil {
|
||||
uiConfig = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if def != nil {
|
||||
return qb.Destroy(ctx, def.ID)
|
||||
}
|
||||
m := utils.NestedMap(uiConfig)
|
||||
|
||||
return nil
|
||||
if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil {
|
||||
// clearing
|
||||
m.Delete("defaultFilters." + strings.ToLower(input.Mode.String()))
|
||||
config.SetUIConfiguration(m)
|
||||
|
||||
if err := config.Write(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return qb.SetDefault(ctx, &models.SavedFilter{
|
||||
Mode: input.Mode,
|
||||
FindFilter: input.FindFilter,
|
||||
ObjectFilter: input.ObjectFilter,
|
||||
UIOptions: input.UIOptions,
|
||||
})
|
||||
}); err != nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
subMap := make(map[string]interface{})
|
||||
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
TagName: "json",
|
||||
WeaklyTypedInput: true,
|
||||
Result: &subMap,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := d.Decode(input); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
m.Set("defaultFilters."+strings.ToLower(input.Mode.String()), subMap)
|
||||
|
||||
config.SetUIConfiguration(m)
|
||||
|
||||
if err := config.Write(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
||||
@@ -80,9 +80,17 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
|
||||
newScene.Movies, err = translator.relatedMovies(input.Movies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting movies: %w", err)
|
||||
// prefer groups over movies
|
||||
if len(input.Groups) > 0 {
|
||||
newScene.Groups, err = translator.relatedGroups(input.Groups)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting groups: %w", err)
|
||||
}
|
||||
} else if len(input.Movies) > 0 {
|
||||
newScene.Groups, err = translator.relatedGroupsFromMovies(input.Movies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting movies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var coverImageData []byte
|
||||
@@ -216,9 +224,16 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
|
||||
updatedScene.MovieIDs, err = translator.updateMovieIDs(input.Movies, "movies")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting movies: %w", err)
|
||||
if translator.hasField("groups") {
|
||||
updatedScene.GroupIDs, err = translator.updateGroupIDs(input.Groups, "groups")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting groups: %w", err)
|
||||
}
|
||||
} else if translator.hasField("movies") {
|
||||
updatedScene.GroupIDs, err = translator.updateGroupIDsFromMovies(input.Movies, "movies")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting movies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &updatedScene, nil
|
||||
@@ -358,9 +373,16 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
|
||||
updatedScene.MovieIDs, err = translator.updateMovieIDsBulk(input.MovieIds, "movie_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting movie ids: %w", err)
|
||||
if translator.hasField("group_ids") {
|
||||
updatedScene.GroupIDs, err = translator.updateGroupIDsBulk(input.GroupIds, "group_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting group ids: %w", err)
|
||||
}
|
||||
} else if translator.hasField("movie_ids") {
|
||||
updatedScene.GroupIDs, err = translator.updateGroupIDsBulk(input.MovieIds, "movie_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting movie ids: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
ret := []*models.Scene{}
|
||||
@@ -825,6 +847,24 @@ func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, res
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneResetActivity(ctx context.Context, id string, resetResume *bool, resetDuration *bool) (ret bool, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.ResetActivity(ctx, sceneID, utils.IsTrue(resetResume), utils.IsTrue(resetDuration))
|
||||
return err
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
|
||||
@@ -6,41 +6,46 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input StashBoxFingerprintSubmissionInput) (bool, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
||||
return false, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
|
||||
|
||||
return client.SubmitStashBoxFingerprints(ctx, input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
|
||||
client := r.newStashBoxClient(*b)
|
||||
return client.SubmitStashBoxFingerprints(ctx, input.SceneIds)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) {
|
||||
jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, input)
|
||||
b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, b, input)
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StashBoxBatchStudioTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) {
|
||||
jobID := manager.GetInstance().StashBoxBatchStudioTag(ctx, input)
|
||||
b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jobID := manager.GetInstance().StashBoxBatchStudioTag(ctx, b, input)
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
||||
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -68,7 +73,7 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
|
||||
return fmt.Errorf("loading scene URLs: %w", err)
|
||||
}
|
||||
|
||||
res, err = client.SubmitSceneDraft(ctx, scene, boxes[input.StashBoxIndex].Endpoint, cover)
|
||||
res, err = client.SubmitSceneDraft(ctx, scene, cover)
|
||||
return err
|
||||
})
|
||||
|
||||
@@ -76,13 +81,12 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
||||
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -101,7 +105,7 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
|
||||
return fmt.Errorf("performer with id %d not found", id)
|
||||
}
|
||||
|
||||
res, err = client.SubmitPerformerDraft(ctx, performer, boxes[input.StashBoxIndex].Endpoint)
|
||||
res, err = client.SubmitPerformerDraft(ctx, performer)
|
||||
return err
|
||||
})
|
||||
|
||||
|
||||
@@ -48,6 +48,11 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
return nil, fmt.Errorf("converting parent id: %w", err)
|
||||
}
|
||||
|
||||
newStudio.TagIDs, err = translator.relatedIds(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var imageData []byte
|
||||
if input.Image != nil {
|
||||
@@ -114,6 +119,11 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
return nil, fmt.Errorf("converting parent id: %w", err)
|
||||
}
|
||||
|
||||
updatedStudio.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var imageData []byte
|
||||
imageIncluded := translator.hasField("image")
|
||||
|
||||
@@ -33,26 +33,21 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||
newTag := models.NewTag()
|
||||
|
||||
newTag.Name = input.Name
|
||||
newTag.Aliases = models.NewRelatedStrings(input.Aliases)
|
||||
newTag.Favorite = translator.bool(input.Favorite)
|
||||
newTag.Description = translator.string(input.Description)
|
||||
newTag.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
|
||||
var err error
|
||||
|
||||
var parentIDs []int
|
||||
if len(input.ParentIds) > 0 {
|
||||
parentIDs, err = stringslice.StringSliceToIntSlice(input.ParentIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting parent ids: %w", err)
|
||||
}
|
||||
newTag.ParentIDs, err = translator.relatedIds(input.ParentIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting parent tag ids: %w", err)
|
||||
}
|
||||
|
||||
var childIDs []int
|
||||
if len(input.ChildIds) > 0 {
|
||||
childIDs, err = stringslice.StringSliceToIntSlice(input.ChildIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting child ids: %w", err)
|
||||
}
|
||||
newTag.ChildIDs, err = translator.relatedIds(input.ChildIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting child tag ids: %w", err)
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
@@ -68,8 +63,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Tag
|
||||
|
||||
// ensure name is unique
|
||||
if err := tag.EnsureTagNameUnique(ctx, 0, newTag.Name, qb); err != nil {
|
||||
if err := tag.ValidateCreate(ctx, newTag, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -85,36 +79,6 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||
}
|
||||
}
|
||||
|
||||
if len(input.Aliases) > 0 {
|
||||
if err := tag.EnsureAliasesUnique(ctx, newTag.ID, input.Aliases, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateAliases(ctx, newTag.ID, input.Aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(parentIDs) > 0 {
|
||||
if err := qb.UpdateParentTags(ctx, newTag.ID, parentIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(childIDs) > 0 {
|
||||
if err := qb.UpdateChildTags(ctx, newTag.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(ctx, &newTag, parentIDs, childIDs, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -137,24 +101,21 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||
// Populate tag from the input
|
||||
updatedTag := models.NewTagPartial()
|
||||
|
||||
updatedTag.Name = translator.optionalString(input.Name, "name")
|
||||
updatedTag.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedTag.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
updatedTag.Description = translator.optionalString(input.Description, "description")
|
||||
|
||||
var parentIDs []int
|
||||
if translator.hasField("parent_ids") {
|
||||
parentIDs, err = stringslice.StringSliceToIntSlice(input.ParentIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting parent ids: %w", err)
|
||||
}
|
||||
updatedTag.Aliases = translator.updateStrings(input.Aliases, "aliases")
|
||||
|
||||
updatedTag.ParentIDs, err = translator.updateIds(input.ParentIds, "parent_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting parent tag ids: %w", err)
|
||||
}
|
||||
|
||||
var childIDs []int
|
||||
if translator.hasField("child_ids") {
|
||||
childIDs, err = stringslice.StringSliceToIntSlice(input.ChildIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting child ids: %w", err)
|
||||
}
|
||||
updatedTag.ChildIDs, err = translator.updateIds(input.ChildIds, "child_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting child tag ids: %w", err)
|
||||
}
|
||||
|
||||
var imageData []byte
|
||||
@@ -171,24 +132,10 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Tag
|
||||
|
||||
// ensure name is unique
|
||||
t, err = qb.Find(ctx, tagID)
|
||||
if err != nil {
|
||||
if err := tag.ValidateUpdate(ctx, tagID, updatedTag, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t == nil {
|
||||
return fmt.Errorf("tag with id %d not found", tagID)
|
||||
}
|
||||
|
||||
if input.Name != nil && t.Name != *input.Name {
|
||||
if err := tag.EnsureTagNameUnique(ctx, tagID, *input.Name, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updatedTag.Name = models.NewOptionalString(*input.Name)
|
||||
}
|
||||
|
||||
t, err = qb.UpdatePartial(ctx, tagID, updatedTag)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -201,37 +148,6 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("aliases") {
|
||||
if err := tag.EnsureAliasesUnique(ctx, tagID, input.Aliases, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateAliases(ctx, tagID, input.Aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if parentIDs != nil {
|
||||
if err := qb.UpdateParentTags(ctx, tagID, parentIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if childIDs != nil {
|
||||
if err := qb.UpdateChildTags(ctx, 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(ctx, t, parentIDs, childIDs, qb); err != nil {
|
||||
logger.Errorf("Error saving tag: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -241,6 +157,75 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||
return r.getTag(ctx, t.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkTagUpdate(ctx context.Context, input BulkTagUpdateInput) ([]*models.Tag, error) {
|
||||
tagIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate scene from the input
|
||||
updatedTag := models.NewTagPartial()
|
||||
|
||||
updatedTag.Description = translator.optionalString(input.Description, "description")
|
||||
updatedTag.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedTag.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
|
||||
updatedTag.Aliases = translator.updateStringsBulk(input.Aliases, "aliases")
|
||||
|
||||
updatedTag.ParentIDs, err = translator.updateIdsBulk(input.ParentIds, "parent_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting parent tag ids: %w", err)
|
||||
}
|
||||
|
||||
updatedTag.ChildIDs, err = translator.updateIdsBulk(input.ChildIds, "child_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting child tag ids: %w", err)
|
||||
}
|
||||
|
||||
ret := []*models.Tag{}
|
||||
|
||||
// Start the transaction and save the scenes
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Tag
|
||||
|
||||
for _, tagID := range tagIDs {
|
||||
if err := tag.ValidateUpdate(ctx, tagID, updatedTag, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag, err := qb.UpdatePartial(ctx, tagID, updatedTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, tag)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// execute post hooks outside of txn
|
||||
var newRet []*models.Tag
|
||||
for _, tag := range ret {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, tag.ID, hook.TagUpdatePost, input, translator.getFields())
|
||||
|
||||
tag, err = r.getTag(ctx, tag.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newRet = append(newRet, tag)
|
||||
}
|
||||
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error) {
|
||||
tagID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -331,7 +316,7 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput)
|
||||
return err
|
||||
}
|
||||
|
||||
err = tag.ValidateHierarchy(ctx, t, parents, children, qb)
|
||||
err = tag.ValidateHierarchyExisting(ctx, t, parents, children, qb)
|
||||
if err != nil {
|
||||
logger.Errorf("Error merging tag: %s", err)
|
||||
return err
|
||||
|
||||
59
internal/api/resolver_query_find_group.go
Normal file
59
internal/api/resolver_query_find_group.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindGroup(ctx context.Context, id string) (ret *models.Group, err error) {
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Group.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindGroups(ctx context.Context, groupFilter *models.GroupFilterType, filter *models.FindFilterType, ids []string) (ret *FindGroupsResultType, err error) {
|
||||
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var groups []*models.Group
|
||||
var err error
|
||||
var total int
|
||||
|
||||
if len(idInts) > 0 {
|
||||
groups, err = r.repository.Group.FindMany(ctx, idInts)
|
||||
total = len(groups)
|
||||
} else {
|
||||
groups, total, err = r.repository.Group.Query(ctx, groupFilter, filter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &FindGroupsResultType{
|
||||
Count: total,
|
||||
Groups: groups,
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
@@ -8,14 +8,14 @@ import (
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Movie, err error) {
|
||||
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Group, err error) {
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.Find(ctx, idInt)
|
||||
ret, err = r.repository.Group.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -24,22 +24,22 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType, ids []string) (ret *FindMoviesResultType, err error) {
|
||||
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.GroupFilterType, filter *models.FindFilterType, ids []string) (ret *FindMoviesResultType, err error) {
|
||||
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var movies []*models.Movie
|
||||
var groups []*models.Group
|
||||
var err error
|
||||
var total int
|
||||
|
||||
if len(idInts) > 0 {
|
||||
movies, err = r.repository.Movie.FindMany(ctx, idInts)
|
||||
total = len(movies)
|
||||
groups, err = r.repository.Group.FindMany(ctx, idInts)
|
||||
total = len(groups)
|
||||
} else {
|
||||
movies, total, err = r.repository.Movie.Query(ctx, movieFilter, filter)
|
||||
groups, total, err = r.repository.Group.Query(ctx, movieFilter, filter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -48,7 +48,7 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi
|
||||
|
||||
ret = &FindMoviesResultType{
|
||||
Count: total,
|
||||
Movies: movies,
|
||||
Movies: groups,
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
@@ -58,9 +58,9 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Movie, err error) {
|
||||
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Group, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.All(ctx)
|
||||
ret, err = r.repository.Group.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -3,8 +3,12 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *models.SavedFilter, err error) {
|
||||
@@ -37,11 +41,35 @@ func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.Filte
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
|
||||
return err
|
||||
}); err != nil {
|
||||
// deprecated - read from the config in the meantime
|
||||
config := config.GetInstance()
|
||||
|
||||
uiConfig := config.GetUIConfiguration()
|
||||
if uiConfig == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m := utils.NestedMap(uiConfig)
|
||||
filterRaw, _ := m.Get("defaultFilters." + strings.ToLower(mode.String()))
|
||||
|
||||
if filterRaw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ret = &models.SavedFilter{}
|
||||
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
TagName: "json",
|
||||
WeaklyTypedInput: true,
|
||||
Result: ret,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, err
|
||||
|
||||
if err := d.Decode(filterRaw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
@@ -145,6 +144,23 @@ func filterPerformerTags(p []*models.ScrapedPerformer) {
|
||||
}
|
||||
}
|
||||
|
||||
// filterGroupTags removes tags matching excluded tag patterns from the provided scraped movies
|
||||
func filterGroupTags(p []*models.ScrapedMovie) {
|
||||
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())
|
||||
|
||||
var ignoredTags []string
|
||||
|
||||
for _, s := range p {
|
||||
var ignored []string
|
||||
s.Tags, ignored = filterTags(excludeRegexps, s.Tags)
|
||||
ignoredTags = sliceutil.AppendUniques(ignoredTags, ignored)
|
||||
}
|
||||
|
||||
if len(ignoredTags) > 0 {
|
||||
logger.Debugf("Scraping ignored tags: %s", strings.Join(ignoredTags, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*scraper.ScrapedScene, error) {
|
||||
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeScene)
|
||||
if err != nil {
|
||||
@@ -187,20 +203,48 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return marshalScrapedMovie(content)
|
||||
}
|
||||
|
||||
func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if index < 0 || index >= len(boxes) {
|
||||
return nil, fmt.Errorf("%w: invalid stash_box_index %d", ErrInput, index)
|
||||
ret, err := marshalScrapedMovie(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stashbox.NewClient(*boxes[index], r.stashboxRepository()), nil
|
||||
filterGroupTags([]*models.ScrapedMovie{ret})
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// FIXME - in the following resolvers, we're processing the deprecated field and not processing the new endpoint input
|
||||
func (r *queryResolver) ScrapeGroupURL(ctx context.Context, url string) (*models.ScrapedGroup, error) {
|
||||
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeMovie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret, err := marshalScrapedMovie(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filterGroupTags([]*models.ScrapedMovie{ret})
|
||||
|
||||
// convert to scraped group
|
||||
group := &models.ScrapedGroup{
|
||||
StoredID: ret.StoredID,
|
||||
Name: ret.Name,
|
||||
Aliases: ret.Aliases,
|
||||
Duration: ret.Duration,
|
||||
Date: ret.Date,
|
||||
Rating: ret.Rating,
|
||||
Director: ret.Director,
|
||||
URLs: ret.URLs,
|
||||
Synopsis: ret.Synopsis,
|
||||
Studio: ret.Studio,
|
||||
Tags: ret.Tags,
|
||||
FrontImage: ret.FrontImage,
|
||||
BackImage: ret.BackImage,
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*scraper.ScrapedScene, error) {
|
||||
var ret []*scraper.ScrapedScene
|
||||
@@ -245,12 +289,14 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case source.StashBoxIndex != nil:
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
case source.StashBoxIndex != nil || source.StashBoxEndpoint != nil:
|
||||
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
switch {
|
||||
case input.SceneID != nil:
|
||||
ret, err = client.FindStashBoxSceneByFingerprints(ctx, sceneID)
|
||||
@@ -275,12 +321,14 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So
|
||||
func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.Source, input ScrapeMultiScenesInput) ([][]*scraper.ScrapedScene, error) {
|
||||
if source.ScraperID != nil {
|
||||
return nil, ErrNotImplemented
|
||||
} else if source.StashBoxIndex != nil {
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
} else if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
|
||||
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
sceneIDs, err := stringslice.StringSliceToIntSlice(input.SceneIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -293,12 +341,14 @@ func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.So
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.Source, input ScrapeSingleStudioInput) ([]*models.ScrapedStudio, error) {
|
||||
if source.StashBoxIndex != nil {
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
|
||||
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
var ret []*models.ScrapedStudio
|
||||
out, err := client.FindStashBoxStudio(ctx, *input.Query)
|
||||
|
||||
@@ -346,13 +396,14 @@ func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scrape
|
||||
default:
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
// FIXME - we're relying on a deprecated field and not processing the endpoint input
|
||||
case source.StashBoxIndex != nil:
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
case source.StashBoxIndex != nil || source.StashBoxEndpoint != nil:
|
||||
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
var res []*stashbox.StashBoxPerformerQueryResult
|
||||
switch {
|
||||
case input.PerformerID != nil:
|
||||
@@ -382,12 +433,14 @@ func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scrape
|
||||
func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source scraper.Source, input ScrapeMultiPerformersInput) ([][]*models.ScrapedPerformer, error) {
|
||||
if source.ScraperID != nil {
|
||||
return nil, ErrNotImplemented
|
||||
} else if source.StashBoxIndex != nil {
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
} else if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
|
||||
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
return client.FindStashBoxPerformersByPerformerNames(ctx, input.PerformerIds)
|
||||
}
|
||||
|
||||
@@ -397,7 +450,7 @@ func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source scrape
|
||||
func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.Source, input ScrapeSingleGalleryInput) ([]*scraper.ScrapedGallery, error) {
|
||||
var ret []*scraper.ScrapedGallery
|
||||
|
||||
if source.StashBoxIndex != nil {
|
||||
if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
@@ -441,3 +494,7 @@ func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.
|
||||
func (r *queryResolver) ScrapeSingleMovie(ctx context.Context, source scraper.Source, input ScrapeSingleMovieInput) ([]*models.ScrapedMovie, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleGroup(ctx context.Context, source scraper.Source, input ScrapeSingleGroupInput) ([]*models.ScrapedGroup, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
159
internal/api/routes_gallery.go
Normal file
159
internal/api/routes_gallery.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type GalleryFinder interface {
|
||||
models.GalleryGetter
|
||||
FindByChecksum(ctx context.Context, checksum string) ([]*models.Gallery, error)
|
||||
}
|
||||
|
||||
type GalleryImageFinder interface {
|
||||
FindByGalleryIDIndex(ctx context.Context, galleryID int, index uint) (*models.Image, error)
|
||||
image.Queryer
|
||||
image.CoverQueryer
|
||||
}
|
||||
|
||||
type galleryRoutes struct {
|
||||
routes
|
||||
imageRoutes imageRoutes
|
||||
galleryFinder GalleryFinder
|
||||
imageFinder GalleryImageFinder
|
||||
fileGetter models.FileGetter
|
||||
}
|
||||
|
||||
func (rs galleryRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{galleryId}", func(r chi.Router) {
|
||||
r.Use(rs.GalleryCtx)
|
||||
|
||||
r.Get("/cover", rs.Cover)
|
||||
r.Get("/preview/{imageIndex}", rs.Preview)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (rs galleryRoutes) Cover(w http.ResponseWriter, r *http.Request) {
|
||||
g := r.Context().Value(galleryKey).(*models.Gallery)
|
||||
|
||||
var i *models.Image
|
||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
// Find cover image first
|
||||
i, _ = image.FindGalleryCover(ctx, rs.imageFinder, g.ID, config.GetInstance().GetGalleryCoverRegex())
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// serveThumbnail needs files populated
|
||||
if err := i.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
logger.Errorf("error loading primary file for image %d: %v", i.ID, err)
|
||||
}
|
||||
// set image to nil so that it doesn't try to use the primary file
|
||||
i = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if i == nil {
|
||||
// fallback to default image
|
||||
image := static.ReadAll(static.DefaultGalleryImage)
|
||||
utils.ServeImage(w, r, image)
|
||||
return
|
||||
}
|
||||
|
||||
rs.imageRoutes.serveThumbnail(w, r, i, &g.UpdatedAt)
|
||||
}
|
||||
|
||||
func (rs galleryRoutes) Preview(w http.ResponseWriter, r *http.Request) {
|
||||
g := r.Context().Value(galleryKey).(*models.Gallery)
|
||||
indexQueryParam := chi.URLParam(r, "imageIndex")
|
||||
var i *models.Image
|
||||
|
||||
index, err := strconv.Atoi(indexQueryParam)
|
||||
if err != nil || index < 0 {
|
||||
http.Error(w, "bad index", 400)
|
||||
return
|
||||
}
|
||||
|
||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
qb := rs.imageFinder
|
||||
i, _ = qb.FindByGalleryIDIndex(ctx, g.ID, uint(index))
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
// TODO - handle errors?
|
||||
|
||||
// serveThumbnail needs files populated
|
||||
if err := i.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
logger.Errorf("error loading primary file for image %d: %v", i.ID, err)
|
||||
}
|
||||
// set image to nil so that it doesn't try to use the primary file
|
||||
i = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if i == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
rs.imageRoutes.serveThumbnail(w, r, i, nil)
|
||||
}
|
||||
|
||||
func (rs galleryRoutes) GalleryCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
galleryIdentifierQueryParam := chi.URLParam(r, "galleryId")
|
||||
galleryID, _ := strconv.Atoi(galleryIdentifierQueryParam)
|
||||
|
||||
var gallery *models.Gallery
|
||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
qb := rs.galleryFinder
|
||||
if galleryID == 0 {
|
||||
galleries, _ := qb.FindByChecksum(ctx, galleryIdentifierQueryParam)
|
||||
if len(galleries) > 0 {
|
||||
gallery = galleries[0]
|
||||
}
|
||||
} else {
|
||||
gallery, _ = qb.Find(ctx, galleryID)
|
||||
}
|
||||
|
||||
if gallery != nil {
|
||||
if err := gallery.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
logger.Errorf("error loading primary file for gallery %d: %v", galleryID, err)
|
||||
}
|
||||
// set image to nil so that it doesn't try to use the primary file
|
||||
gallery = nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if gallery == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), galleryKey, gallery)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -14,22 +14,22 @@ import (
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type MovieFinder interface {
|
||||
models.MovieGetter
|
||||
GetFrontImage(ctx context.Context, movieID int) ([]byte, error)
|
||||
GetBackImage(ctx context.Context, movieID int) ([]byte, error)
|
||||
type GroupFinder interface {
|
||||
models.GroupGetter
|
||||
GetFrontImage(ctx context.Context, groupID int) ([]byte, error)
|
||||
GetBackImage(ctx context.Context, groupID int) ([]byte, error)
|
||||
}
|
||||
|
||||
type movieRoutes struct {
|
||||
type groupRoutes struct {
|
||||
routes
|
||||
movieFinder MovieFinder
|
||||
groupFinder GroupFinder
|
||||
}
|
||||
|
||||
func (rs movieRoutes) Routes() chi.Router {
|
||||
func (rs groupRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{movieId}", func(r chi.Router) {
|
||||
r.Use(rs.MovieCtx)
|
||||
r.Route("/{groupId}", func(r chi.Router) {
|
||||
r.Use(rs.GroupCtx)
|
||||
r.Get("/frontimage", rs.FrontImage)
|
||||
r.Get("/backimage", rs.BackImage)
|
||||
})
|
||||
@@ -37,77 +37,77 @@ func (rs movieRoutes) Routes() chi.Router {
|
||||
return r
|
||||
}
|
||||
|
||||
func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
|
||||
movie := r.Context().Value(movieKey).(*models.Movie)
|
||||
func (rs groupRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
|
||||
group := r.Context().Value(groupKey).(*models.Group)
|
||||
defaultParam := r.URL.Query().Get("default")
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
image, err = rs.movieFinder.GetFrontImage(ctx, movie.ID)
|
||||
image, err = rs.groupFinder.GetFrontImage(ctx, group.ID)
|
||||
return err
|
||||
})
|
||||
if errors.Is(readTxnErr, context.Canceled) {
|
||||
return
|
||||
}
|
||||
if readTxnErr != nil {
|
||||
logger.Warnf("read transaction error on fetch movie front image: %v", readTxnErr)
|
||||
logger.Warnf("read transaction error on fetch group front image: %v", readTxnErr)
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to default image
|
||||
if len(image) == 0 {
|
||||
image = static.ReadAll(static.DefaultMovieImage)
|
||||
image = static.ReadAll(static.DefaultGroupImage)
|
||||
}
|
||||
|
||||
utils.ServeImage(w, r, image)
|
||||
}
|
||||
|
||||
func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
|
||||
movie := r.Context().Value(movieKey).(*models.Movie)
|
||||
func (rs groupRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
|
||||
group := r.Context().Value(groupKey).(*models.Group)
|
||||
defaultParam := r.URL.Query().Get("default")
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
var err error
|
||||
image, err = rs.movieFinder.GetBackImage(ctx, movie.ID)
|
||||
image, err = rs.groupFinder.GetBackImage(ctx, group.ID)
|
||||
return err
|
||||
})
|
||||
if errors.Is(readTxnErr, context.Canceled) {
|
||||
return
|
||||
}
|
||||
if readTxnErr != nil {
|
||||
logger.Warnf("read transaction error on fetch movie back image: %v", readTxnErr)
|
||||
logger.Warnf("read transaction error on fetch group back image: %v", readTxnErr)
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to default image
|
||||
if len(image) == 0 {
|
||||
image = static.ReadAll(static.DefaultMovieImage)
|
||||
image = static.ReadAll(static.DefaultGroupImage)
|
||||
}
|
||||
|
||||
utils.ServeImage(w, r, image)
|
||||
}
|
||||
|
||||
func (rs movieRoutes) MovieCtx(next http.Handler) http.Handler {
|
||||
func (rs groupRoutes) GroupCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
movieID, err := strconv.Atoi(chi.URLParam(r, "movieId"))
|
||||
groupID, err := strconv.Atoi(chi.URLParam(r, "groupId"))
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
var movie *models.Movie
|
||||
var group *models.Group
|
||||
_ = rs.withReadTxn(r, func(ctx context.Context) error {
|
||||
movie, _ = rs.movieFinder.Find(ctx, movieID)
|
||||
group, _ = rs.groupFinder.Find(ctx, groupID)
|
||||
return nil
|
||||
})
|
||||
if movie == nil {
|
||||
if group == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), movieKey, movie)
|
||||
ctx := context.WithValue(r.Context(), groupKey, group)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@@ -46,14 +47,22 @@ func (rs imageRoutes) Routes() chi.Router {
|
||||
}
|
||||
|
||||
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
mgr := manager.GetInstance()
|
||||
img := r.Context().Value(imageKey).(*models.Image)
|
||||
rs.serveThumbnail(w, r, img, nil)
|
||||
}
|
||||
|
||||
func (rs imageRoutes) serveThumbnail(w http.ResponseWriter, r *http.Request, img *models.Image, modTime *time.Time) {
|
||||
mgr := manager.GetInstance()
|
||||
filepath := mgr.Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
|
||||
|
||||
// if the thumbnail doesn't exist, encode on the fly
|
||||
exists, _ := fsutil.FileExists(filepath)
|
||||
if exists {
|
||||
utils.ServeStaticFile(w, r, filepath)
|
||||
if modTime == nil {
|
||||
utils.ServeStaticFile(w, r, filepath)
|
||||
} else {
|
||||
utils.ServeStaticFileModTime(w, r, filepath, *modTime)
|
||||
}
|
||||
} else {
|
||||
const useDefault = true
|
||||
|
||||
|
||||
@@ -75,7 +75,8 @@ func (dir osFS) Open(name string) (fs.File, error) {
|
||||
return os.DirFS(string(dir)).Open(name)
|
||||
}
|
||||
|
||||
// Called at startup
|
||||
// Initialize creates a new [Server] instance.
|
||||
// It assumes that the [manager.Manager] instance has been initialised.
|
||||
func Initialize() (*Server, error) {
|
||||
mgr := manager.GetInstance()
|
||||
cfg := mgr.Config
|
||||
@@ -157,11 +158,13 @@ func Initialize() (*Server, error) {
|
||||
sceneService := mgr.SceneService
|
||||
imageService := mgr.ImageService
|
||||
galleryService := mgr.GalleryService
|
||||
groupService := mgr.GroupService
|
||||
resolver := &Resolver{
|
||||
repository: repo,
|
||||
sceneService: sceneService,
|
||||
imageService: imageService,
|
||||
galleryService: galleryService,
|
||||
groupService: groupService,
|
||||
hookExecutor: pluginCache,
|
||||
}
|
||||
|
||||
@@ -207,9 +210,10 @@ func Initialize() (*Server, error) {
|
||||
|
||||
r.Mount("/performer", server.getPerformerRoutes())
|
||||
r.Mount("/scene", server.getSceneRoutes())
|
||||
r.Mount("/gallery", server.getGalleryRoutes())
|
||||
r.Mount("/image", server.getImageRoutes())
|
||||
r.Mount("/studio", server.getStudioRoutes())
|
||||
r.Mount("/movie", server.getMovieRoutes())
|
||||
r.Mount("/group", server.getGroupRoutes())
|
||||
r.Mount("/tag", server.getTagRoutes())
|
||||
r.Mount("/downloads", server.getDownloadsRoutes())
|
||||
r.Mount("/plugin", server.getPluginRoutes())
|
||||
@@ -288,6 +292,9 @@ func Initialize() (*Server, error) {
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// Start starts the server. It listens on the configured address and port.
|
||||
// It calls ListenAndServeTLS if TLS is configured, otherwise it calls ListenAndServe.
|
||||
// Calls to Start are blocked until the server is shutdown.
|
||||
func (s *Server) Start() error {
|
||||
logger.Infof("stash is listening on " + s.Addr)
|
||||
logger.Infof("stash is running at " + s.displayAddress)
|
||||
@@ -299,6 +306,7 @@ func (s *Server) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the server without interrupting any active connections.
|
||||
func (s *Server) Shutdown() {
|
||||
err := s.Server.Shutdown(context.TODO())
|
||||
if err != nil {
|
||||
@@ -326,6 +334,16 @@ func (s *Server) getSceneRoutes() chi.Router {
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getGalleryRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return galleryRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
imageFinder: repo.Image,
|
||||
galleryFinder: repo.Gallery,
|
||||
fileGetter: repo.File,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getImageRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return imageRoutes{
|
||||
@@ -343,11 +361,11 @@ func (s *Server) getStudioRoutes() chi.Router {
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getMovieRoutes() chi.Router {
|
||||
func (s *Server) getGroupRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return movieRoutes{
|
||||
return groupRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
movieFinder: repo.Movie,
|
||||
groupFinder: repo.Group,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
|
||||
45
internal/api/stash_box.go
Normal file
45
internal/api/stash_box.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
)
|
||||
|
||||
func (r *Resolver) newStashBoxClient(box models.StashBox) *stashbox.Client {
|
||||
return stashbox.NewClient(box, r.stashboxRepository())
|
||||
}
|
||||
|
||||
func resolveStashBoxFn(indexField, endpointField string) func(index *int, endpoint *string) (*models.StashBox, error) {
|
||||
return func(index *int, endpoint *string) (*models.StashBox, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
// prefer endpoint over index
|
||||
if endpoint != nil {
|
||||
for _, box := range boxes {
|
||||
if strings.EqualFold(*endpoint, box.Endpoint) {
|
||||
return box, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("stash box not found")
|
||||
}
|
||||
|
||||
if index != nil {
|
||||
if *index < 0 || *index >= len(boxes) {
|
||||
return nil, fmt.Errorf("invalid %s %d", indexField, index)
|
||||
}
|
||||
|
||||
return boxes[*index], nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s not provided", endpointField)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
resolveStashBox = resolveStashBoxFn("stash_box_index", "stash_box_endpoint")
|
||||
resolveStashBoxBatchTagInput = resolveStashBoxFn("endpoint", "stash_box_endpoint")
|
||||
)
|
||||
2
internal/api/urlbuilders/doc.go
Normal file
2
internal/api/urlbuilders/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package urlbuilders provides the builders used to build URLs to pass to clients.
|
||||
package urlbuilders
|
||||
27
internal/api/urlbuilders/gallery.go
Normal file
27
internal/api/urlbuilders/gallery.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package urlbuilders
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type GalleryURLBuilder struct {
|
||||
BaseURL string
|
||||
GalleryID string
|
||||
}
|
||||
|
||||
func NewGalleryURLBuilder(baseURL string, gallery *models.Gallery) GalleryURLBuilder {
|
||||
return GalleryURLBuilder{
|
||||
BaseURL: baseURL,
|
||||
GalleryID: strconv.Itoa(gallery.ID),
|
||||
}
|
||||
}
|
||||
|
||||
func (b GalleryURLBuilder) GetPreviewURL() string {
|
||||
return b.BaseURL + "/gallery/" + b.GalleryID + "/preview"
|
||||
}
|
||||
|
||||
func (b GalleryURLBuilder) GetCoverURL() string {
|
||||
return b.BaseURL + "/gallery/" + b.GalleryID + "/cover"
|
||||
}
|
||||
33
internal/api/urlbuilders/group.go
Normal file
33
internal/api/urlbuilders/group.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package urlbuilders
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type GroupURLBuilder struct {
|
||||
BaseURL string
|
||||
GroupID string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
func NewGroupURLBuilder(baseURL string, group *models.Group) GroupURLBuilder {
|
||||
return GroupURLBuilder{
|
||||
BaseURL: baseURL,
|
||||
GroupID: strconv.Itoa(group.ID),
|
||||
UpdatedAt: strconv.FormatInt(group.UpdatedAt.Unix(), 10),
|
||||
}
|
||||
}
|
||||
|
||||
func (b GroupURLBuilder) GetGroupFrontImageURL(hasImage bool) string {
|
||||
url := b.BaseURL + "/group/" + b.GroupID + "/frontimage?t=" + b.UpdatedAt
|
||||
if !hasImage {
|
||||
url += "&default=true"
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func (b GroupURLBuilder) GetGroupBackImageURL() string {
|
||||
return b.BaseURL + "/group/" + b.GroupID + "/backimage?t=" + b.UpdatedAt
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package urlbuilders
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type MovieURLBuilder struct {
|
||||
BaseURL string
|
||||
MovieID string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
func NewMovieURLBuilder(baseURL string, movie *models.Movie) MovieURLBuilder {
|
||||
return MovieURLBuilder{
|
||||
BaseURL: baseURL,
|
||||
MovieID: strconv.Itoa(movie.ID),
|
||||
UpdatedAt: strconv.FormatInt(movie.UpdatedAt.Unix(), 10),
|
||||
}
|
||||
}
|
||||
|
||||
func (b MovieURLBuilder) GetMovieFrontImageURL(hasImage bool) string {
|
||||
url := b.BaseURL + "/movie/" + b.MovieID + "/frontimage?t=" + b.UpdatedAt
|
||||
if !hasImage {
|
||||
url += "&default=true"
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func (b MovieURLBuilder) GetMovieBackImageURL() string {
|
||||
return b.BaseURL + "/movie/" + b.MovieID + "/backimage?t=" + b.UpdatedAt
|
||||
}
|
||||
9
internal/autotag/doc.go
Normal file
9
internal/autotag/doc.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Package autotag provides the autotagging functionality for the application.
|
||||
//
|
||||
// The autotag functionality sets media metadata based on the media's path.
|
||||
// The functions in this package are in the form of {ObjectType}{TagTypes},
|
||||
// where the ObjectType is the single object instance to run on, and TagTypes
|
||||
// are the related types.
|
||||
// For example, PerformerScenes finds and tags scenes with a provided performer,
|
||||
// whereas ScenePerformers tags a single scene with any Performers that match.
|
||||
package autotag
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package build provides the version information for the application.
|
||||
package build
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package desktop provides desktop integration functionality for the application.
|
||||
package desktop
|
||||
|
||||
import (
|
||||
|
||||
@@ -192,7 +192,7 @@ func (me *contentDirectoryService) Handle(action string, argsXML []byte, r *http
|
||||
|
||||
obj, err := me.objectFromID(browse.ObjectID)
|
||||
if err != nil {
|
||||
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error())
|
||||
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, "cannot find object with id %q: %v", browse.ObjectID, err.Error())
|
||||
}
|
||||
|
||||
switch browse.BrowseFlag {
|
||||
@@ -316,13 +316,13 @@ func (me *contentDirectoryService) handleBrowseDirectChildren(obj object, host s
|
||||
objs = me.getPerformerScenes(childPath(paths), host)
|
||||
}
|
||||
|
||||
// Movies
|
||||
if obj.Path == "movies" {
|
||||
objs = me.getMovies()
|
||||
// Groups - deprecated
|
||||
if obj.Path == "groups" {
|
||||
objs = me.getGroups()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(obj.Path, "movies/") {
|
||||
objs = me.getMovieScenes(childPath(paths), host)
|
||||
if strings.HasPrefix(obj.Path, "groups/") {
|
||||
objs = me.getGroupScenes(childPath(paths), host)
|
||||
}
|
||||
|
||||
// Rating
|
||||
@@ -433,7 +433,7 @@ func getRootObjects() []interface{} {
|
||||
objs = append(objs, makeStorageFolder("performers", "performers", rootID))
|
||||
objs = append(objs, makeStorageFolder("tags", "tags", rootID))
|
||||
objs = append(objs, makeStorageFolder("studios", "studios", rootID))
|
||||
objs = append(objs, makeStorageFolder("movies", "movies", rootID))
|
||||
objs = append(objs, makeStorageFolder("groups", "groups", rootID))
|
||||
objs = append(objs, makeStorageFolder("rating", "rating", rootID))
|
||||
|
||||
return objs
|
||||
@@ -658,18 +658,18 @@ func (me *contentDirectoryService) getPerformerScenes(paths []string, host strin
|
||||
return me.getVideos(sceneFilter, parentID, host)
|
||||
}
|
||||
|
||||
func (me *contentDirectoryService) getMovies() []interface{} {
|
||||
func (me *contentDirectoryService) getGroups() []interface{} {
|
||||
var objs []interface{}
|
||||
|
||||
r := me.repository
|
||||
if err := r.WithReadTxn(context.TODO(), func(ctx context.Context) error {
|
||||
movies, err := r.MovieFinder.All(ctx)
|
||||
groups, err := r.GroupFinder.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range movies {
|
||||
objs = append(objs, makeStorageFolder("movies/"+strconv.Itoa(s.ID), s.Name, "movies"))
|
||||
for _, s := range groups {
|
||||
objs = append(objs, makeStorageFolder("groups/"+strconv.Itoa(s.ID), s.Name, "groups"))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -680,15 +680,15 @@ func (me *contentDirectoryService) getMovies() []interface{} {
|
||||
return objs
|
||||
}
|
||||
|
||||
func (me *contentDirectoryService) getMovieScenes(paths []string, host string) []interface{} {
|
||||
func (me *contentDirectoryService) getGroupScenes(paths []string, host string) []interface{} {
|
||||
sceneFilter := &models.SceneFilterType{
|
||||
Movies: &models.MultiCriterionInput{
|
||||
Groups: &models.HierarchicalMultiCriterionInput{
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
Value: []string{paths[0]},
|
||||
},
|
||||
}
|
||||
|
||||
parentID := "movies/" + strings.Join(paths, "/")
|
||||
parentID := "groups/" + strings.Join(paths, "/")
|
||||
|
||||
page := getPageFromID(paths)
|
||||
if page != nil {
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/dms/soap"
|
||||
@@ -67,8 +68,8 @@ type PerformerFinder interface {
|
||||
All(ctx context.Context) ([]*models.Performer, error)
|
||||
}
|
||||
|
||||
type MovieFinder interface {
|
||||
All(ctx context.Context) ([]*models.Movie, error)
|
||||
type GroupFinder interface {
|
||||
All(ctx context.Context) ([]*models.Group, error)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -229,6 +230,10 @@ func (me *Server) ssdpInterface(if_ net.Interface) {
|
||||
stopped := make(chan struct{})
|
||||
go func() {
|
||||
defer close(stopped)
|
||||
// FIXME - this currently blocks forever unless it encounters an error
|
||||
// See https://github.com/anacrolix/dms/pull/150
|
||||
// Needs to be fixed upstream
|
||||
//nolint:staticcheck
|
||||
if err := s.Serve(); err != nil {
|
||||
logger.Errorf("%q: %q\n", if_.Name, err)
|
||||
}
|
||||
@@ -274,6 +279,8 @@ type Server struct {
|
||||
sceneServer sceneServer
|
||||
ipWhitelistManager *ipWhitelistManager
|
||||
VideoSortOrder string
|
||||
|
||||
subscribeLock sync.Mutex
|
||||
}
|
||||
|
||||
// UPnP SOAP service.
|
||||
@@ -537,13 +544,14 @@ func (me *Server) contentDirectoryEventSubHandler(w http.ResponseWriter, r *http
|
||||
// The following code is a work in progress. It partially implements
|
||||
// the spec on eventing but hasn't been completed as I have nothing to
|
||||
// test it with.
|
||||
service := me.services["ContentDirectory"]
|
||||
switch {
|
||||
case r.Method == "SUBSCRIBE" && r.Header.Get("SID") == "":
|
||||
urls := upnp.ParseCallbackURLs(r.Header.Get("CALLBACK"))
|
||||
var timeout int
|
||||
_, _ = fmt.Sscanf(r.Header.Get("TIMEOUT"), "Second-%d", &timeout)
|
||||
sid, timeout, _ := service.Subscribe(urls, timeout)
|
||||
|
||||
sid, timeout, _ := me.subscribe(urls, timeout)
|
||||
|
||||
w.Header()["SID"] = []string{sid}
|
||||
w.Header()["TIMEOUT"] = []string{fmt.Sprintf("Second-%d", timeout)}
|
||||
// TODO: Shouldn't have to do this to get headers logged.
|
||||
@@ -559,6 +567,16 @@ func (me *Server) contentDirectoryEventSubHandler(w http.ResponseWriter, r *http
|
||||
}
|
||||
}
|
||||
|
||||
// wrapper around service.Subscribe which requires concurrency protection
|
||||
// TODO - this should be addressed upstream
|
||||
func (me *Server) subscribe(urls []*url.URL, timeout int) (sid string, actualTimeout int, err error) {
|
||||
me.subscribeLock.Lock()
|
||||
defer me.subscribeLock.Unlock()
|
||||
|
||||
service := me.services["ContentDirectory"]
|
||||
return service.Subscribe(urls, timeout)
|
||||
}
|
||||
|
||||
func (me *Server) initMux(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
|
||||
resp.Header().Set("content-type", "text/html")
|
||||
|
||||
3
internal/dlna/doc.go
Normal file
3
internal/dlna/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package dlna provides the DLNA functionality for the application.
|
||||
// Much of this code is adapted from https://github.com/anacrolix/dms
|
||||
package dlna
|
||||
@@ -22,7 +22,7 @@ type Repository struct {
|
||||
StudioFinder StudioFinder
|
||||
TagFinder TagFinder
|
||||
PerformerFinder PerformerFinder
|
||||
MovieFinder MovieFinder
|
||||
GroupFinder GroupFinder
|
||||
}
|
||||
|
||||
func NewRepository(repo models.Repository) Repository {
|
||||
@@ -33,7 +33,7 @@ func NewRepository(repo models.Repository) Repository {
|
||||
StudioFinder: repo.Studio,
|
||||
TagFinder: repo.Tag,
|
||||
PerformerFinder: repo.Performer,
|
||||
MovieFinder: repo.Movie,
|
||||
GroupFinder: repo.Group,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package identify provides the scene identification functionality for the application.
|
||||
// The identify functionality uses scene scrapers to identify a given scene and
|
||||
// set its metadata based on the scraped data.
|
||||
package identify
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package log provides an implementation of [logger.LoggerImpl], using logrus.
|
||||
package log
|
||||
|
||||
import (
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -232,6 +233,9 @@ const (
|
||||
SecurityTripwireAccessedFromPublicInternet = "security_tripwire_accessed_from_public_internet"
|
||||
securityTripwireAccessedFromPublicInternetDefault = ""
|
||||
|
||||
sslCertPath = "ssl_cert_path"
|
||||
sslKeyPath = "ssl_key_path"
|
||||
|
||||
// DLNA options
|
||||
DLNAServerName = "dlna.server_name"
|
||||
DLNADefaultEnabled = "dlna.default_enabled"
|
||||
@@ -356,8 +360,17 @@ func (i *Config) InitTLS() {
|
||||
paths.GetStashHomeDirectory(),
|
||||
}
|
||||
|
||||
i.certFile = fsutil.FindInPaths(tlsPaths, "stash.crt")
|
||||
i.keyFile = fsutil.FindInPaths(tlsPaths, "stash.key")
|
||||
i.certFile = i.getString(sslCertPath)
|
||||
if i.certFile == "" {
|
||||
// Look for default file
|
||||
i.certFile = fsutil.FindInPaths(tlsPaths, "stash.crt")
|
||||
}
|
||||
|
||||
i.keyFile = i.getString(sslKeyPath)
|
||||
if i.keyFile == "" {
|
||||
// Look for default file
|
||||
i.keyFile = fsutil.FindInPaths(tlsPaths, "stash.key")
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Config) GetTLSFiles() (certFile, keyFile string) {
|
||||
@@ -689,7 +702,8 @@ func (i *Config) GetBackupDirectoryPath() string {
|
||||
func (i *Config) GetBackupDirectoryPathOrDefault() string {
|
||||
ret := i.GetBackupDirectoryPath()
|
||||
if ret == "" {
|
||||
return i.GetConfigPath()
|
||||
// #4915 - default to the same directory as the database
|
||||
return filepath.Dir(i.GetDatabasePath())
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -1085,7 +1099,10 @@ func (i *Config) ValidateCredentials(username string, password string) bool {
|
||||
return username == authUser && err == nil
|
||||
}
|
||||
|
||||
var stashBoxRe = regexp.MustCompile("^http.*graphql$")
|
||||
func stashBoxValidate(str string) bool {
|
||||
u, err := url.Parse(str)
|
||||
return err == nil && u.Scheme != "" && u.Host != "" && strings.HasSuffix(u.Path, "/graphql")
|
||||
}
|
||||
|
||||
type StashBoxInput struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
@@ -1106,7 +1123,7 @@ func (i *Config) ValidateStashBoxes(boxes []*StashBoxInput) error {
|
||||
return &StashBoxError{msg: "endpoint cannot be blank"}
|
||||
}
|
||||
|
||||
if !stashBoxRe.Match([]byte(box.Endpoint)) {
|
||||
if !stashBoxValidate(box.Endpoint) {
|
||||
return &StashBoxError{msg: "endpoint is invalid"}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
@@ -67,6 +68,10 @@ func Initialize(cfg *config.Config, l *log.Logger) (*Manager, error) {
|
||||
Folder: db.Folder,
|
||||
}
|
||||
|
||||
groupService := &group.Service{
|
||||
Repository: db.Group,
|
||||
}
|
||||
|
||||
sceneServer := &SceneServer{
|
||||
TxnManager: repo.TxnManager,
|
||||
SceneCoverGetter: repo.Scene,
|
||||
@@ -99,6 +104,7 @@ func Initialize(cfg *config.Config, l *log.Logger) (*Manager, error) {
|
||||
SceneService: sceneService,
|
||||
ImageService: imageService,
|
||||
GalleryService: galleryService,
|
||||
GroupService: groupService,
|
||||
|
||||
scanSubs: &subscriptionManager{},
|
||||
}
|
||||
@@ -305,7 +311,7 @@ func (s *Manager) RefreshFFMpeg(ctx context.Context) {
|
||||
logger.Debugf("using ffprobe: %s", ffprobePath)
|
||||
|
||||
s.FFMpeg = ffmpeg.NewEncoder(ffmpegPath)
|
||||
s.FFProbe = ffmpeg.FFProbe(ffprobePath)
|
||||
s.FFProbe = ffmpeg.NewFFProbe(ffprobePath)
|
||||
|
||||
s.FFMpeg.InitHWSupport(ctx)
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ func (jp *jsonUtils) saveTag(fn string, tag *jsonschema.Tag) error {
|
||||
return jsonschema.SaveTagFile(filepath.Join(jp.json.Tags, fn), tag)
|
||||
}
|
||||
|
||||
func (jp *jsonUtils) saveMovie(fn string, movie *jsonschema.Movie) error {
|
||||
return jsonschema.SaveMovieFile(filepath.Join(jp.json.Movies, fn), movie)
|
||||
func (jp *jsonUtils) saveGroup(fn string, group *jsonschema.Group) error {
|
||||
return jsonschema.SaveGroupFile(filepath.Join(jp.json.Groups, fn), group)
|
||||
}
|
||||
|
||||
func (jp *jsonUtils) saveScene(fn string, scene *jsonschema.Scene) error {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package manager provides the core manager of the application.
|
||||
// This consolidates all the services and managers into a single struct.
|
||||
package manager
|
||||
|
||||
import (
|
||||
@@ -41,7 +43,7 @@ type Manager struct {
|
||||
Paths *paths.Paths
|
||||
|
||||
FFMpeg *ffmpeg.FFMpeg
|
||||
FFProbe ffmpeg.FFProbe
|
||||
FFProbe *ffmpeg.FFProbe
|
||||
StreamManager *ffmpeg.StreamManager
|
||||
|
||||
JobManager *job.Manager
|
||||
@@ -64,6 +66,7 @@ type Manager struct {
|
||||
SceneService SceneService
|
||||
ImageService ImageService
|
||||
GalleryService GalleryService
|
||||
GroupService GroupService
|
||||
|
||||
scanSubs *subscriptionManager
|
||||
}
|
||||
@@ -297,7 +300,7 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
||||
}
|
||||
|
||||
func (s *Manager) validateFFmpeg() error {
|
||||
if s.FFMpeg == nil || s.FFProbe == "" {
|
||||
if s.FFMpeg == nil || s.FFProbe == nil {
|
||||
return errors.New("missing ffmpeg and/or ffprobe")
|
||||
}
|
||||
return nil
|
||||
@@ -397,7 +400,7 @@ func (s *Manager) GetSystemStatus() *SystemStatus {
|
||||
}
|
||||
|
||||
ffprobePath := ""
|
||||
if s.FFProbe != "" {
|
||||
if s.FFProbe != nil {
|
||||
ffprobePath = s.FFProbe.Path()
|
||||
}
|
||||
|
||||
|
||||
@@ -366,8 +366,9 @@ func (s *Manager) MigrateHash(ctx context.Context) int {
|
||||
|
||||
// If neither ids nor names are set, tag all items
|
||||
type StashBoxBatchTagInput struct {
|
||||
// Stash endpoint to use for the tagging
|
||||
Endpoint int `json:"endpoint"`
|
||||
// Stash endpoint to use for the tagging - deprecated - use StashBoxEndpoint
|
||||
Endpoint *int `json:"endpoint"`
|
||||
StashBoxEndpoint *string `json:"stash_box_endpoint"`
|
||||
// Fields to exclude when executing the tagging
|
||||
ExcludeFields []string `json:"exclude_fields"`
|
||||
// Refresh items already tagged by StashBox if true. Only tag items with no StashBox tagging if false
|
||||
@@ -388,16 +389,10 @@ type StashBoxBatchTagInput struct {
|
||||
PerformerNames []string `json:"performer_names"`
|
||||
}
|
||||
|
||||
func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxBatchTagInput) int {
|
||||
func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, box *models.StashBox, input StashBoxBatchTagInput) int {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
logger.Infof("Initiating stash-box batch performer tag")
|
||||
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
if input.Endpoint < 0 || input.Endpoint >= len(boxes) {
|
||||
return fmt.Errorf("invalid stash_box_index %d", input.Endpoint)
|
||||
}
|
||||
box := boxes[input.Endpoint]
|
||||
|
||||
var tasks []StashBoxBatchTagTask
|
||||
|
||||
// The gocritic linter wants to turn this ifElseChain into a switch.
|
||||
@@ -526,16 +521,10 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxB
|
||||
return s.JobManager.Add(ctx, "Batch stash-box performer tag...", j)
|
||||
}
|
||||
|
||||
func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, input StashBoxBatchTagInput) int {
|
||||
func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, box *models.StashBox, input StashBoxBatchTagInput) int {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
logger.Infof("Initiating stash-box batch studio tag")
|
||||
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
if input.Endpoint < 0 || input.Endpoint >= len(boxes) {
|
||||
return fmt.Errorf("invalid stash_box_index %d", input.Endpoint)
|
||||
}
|
||||
box := boxes[input.Endpoint]
|
||||
|
||||
var tasks []StashBoxBatchTagTask
|
||||
|
||||
// The gocritic linter wants to turn this ifElseChain into a switch.
|
||||
|
||||
@@ -3,6 +3,7 @@ package manager
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
@@ -24,9 +25,21 @@ type GalleryService interface {
|
||||
AddImages(ctx context.Context, g *models.Gallery, toAdd ...int) error
|
||||
RemoveImages(ctx context.Context, g *models.Gallery, toRemove ...int) error
|
||||
|
||||
SetCover(ctx context.Context, g *models.Gallery, coverImageId int) error
|
||||
ResetCover(ctx context.Context, g *models.Gallery) error
|
||||
|
||||
Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error)
|
||||
|
||||
ValidateImageGalleryChange(ctx context.Context, i *models.Image, updateIDs models.UpdateIDs) error
|
||||
|
||||
Updated(ctx context.Context, galleryID int) error
|
||||
}
|
||||
|
||||
type GroupService interface {
|
||||
Create(ctx context.Context, group *models.Group, frontimageData []byte, backimageData []byte) error
|
||||
UpdatePartial(ctx context.Context, id int, updatedGroup models.GroupPartial, frontImage group.ImageInput, backImage group.ImageInput) (*models.Group, error)
|
||||
|
||||
AddSubGroups(ctx context.Context, groupID int, subGroups []models.GroupIDDescription, insertIndex *int) error
|
||||
RemoveSubGroups(ctx context.Context, groupID int, subGroupIDs []int) error
|
||||
ReorderSubGroups(ctx context.Context, groupID int, subGroupIDs []int, insertPointID int, insertAfter bool) error
|
||||
}
|
||||
|
||||
@@ -23,19 +23,27 @@ type MigrateJob struct {
|
||||
Database *sqlite.Database
|
||||
}
|
||||
|
||||
type databaseSchemaInfo struct {
|
||||
CurrentSchemaVersion uint
|
||||
RequiredSchemaVersion uint
|
||||
StepsRequired uint
|
||||
}
|
||||
|
||||
func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
required, err := s.required()
|
||||
schemaInfo, err := s.required()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if required == 0 {
|
||||
if schemaInfo.StepsRequired == 0 {
|
||||
logger.Infof("database is already at the latest schema version")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Migrating database from %d to %d", schemaInfo.CurrentSchemaVersion, schemaInfo.RequiredSchemaVersion)
|
||||
|
||||
// set the number of tasks = required steps + optimise
|
||||
progress.SetTotal(int(required + 1))
|
||||
progress.SetTotal(int(schemaInfo.StepsRequired + 1))
|
||||
|
||||
database := s.Database
|
||||
|
||||
@@ -79,28 +87,31 @@ func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("Database migration complete")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MigrateJob) required() (uint, error) {
|
||||
func (s *MigrateJob) required() (ret databaseSchemaInfo, err error) {
|
||||
database := s.Database
|
||||
|
||||
m, err := sqlite.NewMigrator(database)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return
|
||||
}
|
||||
|
||||
defer m.Close()
|
||||
|
||||
currentSchemaVersion := m.CurrentSchemaVersion()
|
||||
targetSchemaVersion := m.RequiredSchemaVersion()
|
||||
ret.CurrentSchemaVersion = m.CurrentSchemaVersion()
|
||||
ret.RequiredSchemaVersion = m.RequiredSchemaVersion()
|
||||
|
||||
if targetSchemaVersion < currentSchemaVersion {
|
||||
if ret.RequiredSchemaVersion < ret.CurrentSchemaVersion {
|
||||
// shouldn't happen
|
||||
return 0, nil
|
||||
return
|
||||
}
|
||||
|
||||
return targetSchemaVersion - currentSchemaVersion, nil
|
||||
ret.StepsRequired = ret.RequiredSchemaVersion - ret.CurrentSchemaVersion
|
||||
return
|
||||
}
|
||||
|
||||
func (s *MigrateJob) runMigrations(ctx context.Context, progress *job.Progress) error {
|
||||
|
||||
@@ -15,13 +15,13 @@ import (
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/json"
|
||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
"github.com/stashapp/stash/pkg/movie"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
@@ -42,7 +42,7 @@ type ExportTask struct {
|
||||
scenes *exportSpec
|
||||
images *exportSpec
|
||||
performers *exportSpec
|
||||
movies *exportSpec
|
||||
groups *exportSpec
|
||||
tags *exportSpec
|
||||
studios *exportSpec
|
||||
galleries *exportSpec
|
||||
@@ -63,7 +63,8 @@ type ExportObjectsInput struct {
|
||||
Studios *ExportObjectTypeInput `json:"studios"`
|
||||
Performers *ExportObjectTypeInput `json:"performers"`
|
||||
Tags *ExportObjectTypeInput `json:"tags"`
|
||||
Movies *ExportObjectTypeInput `json:"movies"`
|
||||
Groups *ExportObjectTypeInput `json:"groups"`
|
||||
Movies *ExportObjectTypeInput `json:"movies"` // deprecated
|
||||
Galleries *ExportObjectTypeInput `json:"galleries"`
|
||||
IncludeDependencies *bool `json:"includeDependencies"`
|
||||
}
|
||||
@@ -97,13 +98,19 @@ func CreateExportTask(a models.HashAlgorithm, input ExportObjectsInput) *ExportT
|
||||
includeDeps = *input.IncludeDependencies
|
||||
}
|
||||
|
||||
// handle deprecated Movies field
|
||||
groupSpec := input.Groups
|
||||
if groupSpec == nil && input.Movies != nil {
|
||||
groupSpec = input.Movies
|
||||
}
|
||||
|
||||
return &ExportTask{
|
||||
repository: GetInstance().Repository,
|
||||
fileNamingAlgorithm: a,
|
||||
scenes: newExportSpec(input.Scenes),
|
||||
images: newExportSpec(input.Images),
|
||||
performers: newExportSpec(input.Performers),
|
||||
movies: newExportSpec(input.Movies),
|
||||
groups: newExportSpec(groupSpec),
|
||||
tags: newExportSpec(input.Tags),
|
||||
studios: newExportSpec(input.Studios),
|
||||
galleries: newExportSpec(input.Galleries),
|
||||
@@ -113,7 +120,7 @@ func CreateExportTask(a models.HashAlgorithm, input ExportObjectsInput) *ExportT
|
||||
|
||||
func (t *ExportTask) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
// @manager.total = Scene.count + Gallery.count + Performer.count + Studio.count + Movie.count
|
||||
// @manager.total = Scene.count + Gallery.count + Performer.count + Studio.count + Group.count
|
||||
workerCount := runtime.GOMAXPROCS(0) // set worker count to number of cpus available
|
||||
|
||||
startTime := time.Now()
|
||||
@@ -149,11 +156,11 @@ func (t *ExportTask) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
paths.EnsureJSONDirs(t.baseDir)
|
||||
|
||||
txnErr := t.repository.WithTxn(ctx, func(ctx context.Context) error {
|
||||
// include movie scenes and gallery images
|
||||
// include group scenes and gallery images
|
||||
if !t.full {
|
||||
// only include movie scenes if includeDependencies is also set
|
||||
// only include group scenes if includeDependencies is also set
|
||||
if !t.scenes.all && t.includeDependencies {
|
||||
t.populateMovieScenes(ctx)
|
||||
t.populateGroupScenes(ctx)
|
||||
}
|
||||
|
||||
// always export gallery images
|
||||
@@ -165,7 +172,7 @@ func (t *ExportTask) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
t.ExportScenes(ctx, workerCount)
|
||||
t.ExportImages(ctx, workerCount)
|
||||
t.ExportGalleries(ctx, workerCount)
|
||||
t.ExportMovies(ctx, workerCount)
|
||||
t.ExportGroups(ctx, workerCount)
|
||||
t.ExportPerformers(ctx, workerCount)
|
||||
t.ExportStudios(ctx, workerCount)
|
||||
t.ExportTags(ctx, workerCount)
|
||||
@@ -222,7 +229,7 @@ func (t *ExportTask) zipFiles(w io.Writer) error {
|
||||
walkWarn(t.json.json.Galleries, t.zipWalkFunc(u.json.Galleries, z))
|
||||
walkWarn(t.json.json.Performers, t.zipWalkFunc(u.json.Performers, z))
|
||||
walkWarn(t.json.json.Studios, t.zipWalkFunc(u.json.Studios, z))
|
||||
walkWarn(t.json.json.Movies, t.zipWalkFunc(u.json.Movies, z))
|
||||
walkWarn(t.json.json.Groups, t.zipWalkFunc(u.json.Groups, z))
|
||||
walkWarn(t.json.json.Scenes, t.zipWalkFunc(u.json.Scenes, z))
|
||||
walkWarn(t.json.json.Images, t.zipWalkFunc(u.json.Images, z))
|
||||
|
||||
@@ -275,28 +282,28 @@ func (t *ExportTask) zipFile(fn, outDir string, z *zip.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ExportTask) populateMovieScenes(ctx context.Context) {
|
||||
func (t *ExportTask) populateGroupScenes(ctx context.Context) {
|
||||
r := t.repository
|
||||
reader := r.Movie
|
||||
reader := r.Group
|
||||
sceneReader := r.Scene
|
||||
|
||||
var movies []*models.Movie
|
||||
var groups []*models.Group
|
||||
var err error
|
||||
all := t.full || (t.movies != nil && t.movies.all)
|
||||
all := t.full || (t.groups != nil && t.groups.all)
|
||||
if all {
|
||||
movies, err = reader.All(ctx)
|
||||
} else if t.movies != nil && len(t.movies.IDs) > 0 {
|
||||
movies, err = reader.FindMany(ctx, t.movies.IDs)
|
||||
groups, err = reader.All(ctx)
|
||||
} else if t.groups != nil && len(t.groups.IDs) > 0 {
|
||||
groups, err = reader.FindMany(ctx, t.groups.IDs)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("[movies] failed to fetch movies: %v", err)
|
||||
logger.Errorf("[groups] failed to fetch groups: %v", err)
|
||||
}
|
||||
|
||||
for _, m := range movies {
|
||||
scenes, err := sceneReader.FindByMovieID(ctx, m.ID)
|
||||
for _, m := range groups {
|
||||
scenes, err := sceneReader.FindByGroupID(ctx, m.ID)
|
||||
if err != nil {
|
||||
logger.Errorf("[movies] <%s> failed to fetch scenes for movie: %v", m.Name, err)
|
||||
logger.Errorf("[groups] <%s> failed to fetch scenes for group: %v", m.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -481,7 +488,7 @@ func (t *ExportTask) exportScene(ctx context.Context, wg *sync.WaitGroup, jobCha
|
||||
r := t.repository
|
||||
sceneReader := r.Scene
|
||||
studioReader := r.Studio
|
||||
movieReader := r.Movie
|
||||
groupReader := r.Group
|
||||
galleryReader := r.Gallery
|
||||
performerReader := r.Performer
|
||||
tagReader := r.Tag
|
||||
@@ -549,9 +556,9 @@ func (t *ExportTask) exportScene(ctx context.Context, wg *sync.WaitGroup, jobCha
|
||||
continue
|
||||
}
|
||||
|
||||
newSceneJSON.Movies, err = scene.GetSceneMoviesJSON(ctx, movieReader, s)
|
||||
newSceneJSON.Groups, err = scene.GetSceneGroupsJSON(ctx, groupReader, s)
|
||||
if err != nil {
|
||||
logger.Errorf("[scenes] <%s> error getting scene movies JSON: %v", sceneHash, err)
|
||||
logger.Errorf("[scenes] <%s> error getting scene groups JSON: %v", sceneHash, err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -569,12 +576,12 @@ func (t *ExportTask) exportScene(ctx context.Context, wg *sync.WaitGroup, jobCha
|
||||
}
|
||||
t.tags.IDs = sliceutil.AppendUniques(t.tags.IDs, tagIDs)
|
||||
|
||||
movieIDs, err := scene.GetDependentMovieIDs(ctx, s)
|
||||
groupIDs, err := scene.GetDependentGroupIDs(ctx, s)
|
||||
if err != nil {
|
||||
logger.Errorf("[scenes] <%s> error getting scene movies: %v", sceneHash, err)
|
||||
logger.Errorf("[scenes] <%s> error getting scene groups: %v", sceneHash, err)
|
||||
continue
|
||||
}
|
||||
t.movies.IDs = sliceutil.AppendUniques(t.movies.IDs, movieIDs)
|
||||
t.groups.IDs = sliceutil.AppendUniques(t.groups.IDs, groupIDs)
|
||||
|
||||
t.performers.IDs = sliceutil.AppendUniques(t.performers.IDs, performer.GetIDs(performers))
|
||||
}
|
||||
@@ -982,6 +989,7 @@ func (t *ExportTask) ExportStudios(ctx context.Context, workers int) {
|
||||
func (t *ExportTask) exportStudio(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.Studio) {
|
||||
defer wg.Done()
|
||||
|
||||
r := t.repository
|
||||
studioReader := t.repository.Studio
|
||||
|
||||
for s := range jobChan {
|
||||
@@ -992,6 +1000,18 @@ func (t *ExportTask) exportStudio(ctx context.Context, wg *sync.WaitGroup, jobCh
|
||||
continue
|
||||
}
|
||||
|
||||
tags, err := r.Tag.FindByStudioID(ctx, s.ID)
|
||||
if err != nil {
|
||||
logger.Errorf("[studios] <%s> error getting studio tags: %s", s.Name, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
newStudioJSON.Tags = tag.GetNames(tags)
|
||||
|
||||
if t.includeDependencies {
|
||||
t.tags.IDs = sliceutil.AppendUniques(t.tags.IDs, tag.GetIDs(tags))
|
||||
}
|
||||
|
||||
fn := newStudioJSON.Filename()
|
||||
|
||||
if err := t.json.saveStudio(fn, newStudioJSON); err != nil {
|
||||
@@ -1061,71 +1081,108 @@ func (t *ExportTask) exportTag(ctx context.Context, wg *sync.WaitGroup, jobChan
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ExportTask) ExportMovies(ctx context.Context, workers int) {
|
||||
var moviesWg sync.WaitGroup
|
||||
func (t *ExportTask) ExportGroups(ctx context.Context, workers int) {
|
||||
var groupsWg sync.WaitGroup
|
||||
|
||||
reader := t.repository.Movie
|
||||
var movies []*models.Movie
|
||||
reader := t.repository.Group
|
||||
var groups []*models.Group
|
||||
var err error
|
||||
all := t.full || (t.movies != nil && t.movies.all)
|
||||
all := t.full || (t.groups != nil && t.groups.all)
|
||||
if all {
|
||||
movies, err = reader.All(ctx)
|
||||
} else if t.movies != nil && len(t.movies.IDs) > 0 {
|
||||
movies, err = reader.FindMany(ctx, t.movies.IDs)
|
||||
groups, err = reader.All(ctx)
|
||||
} else if t.groups != nil && len(t.groups.IDs) > 0 {
|
||||
groups, err = reader.FindMany(ctx, t.groups.IDs)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("[movies] failed to fetch movies: %v", err)
|
||||
logger.Errorf("[groups] failed to fetch groups: %v", err)
|
||||
}
|
||||
|
||||
logger.Info("[movies] exporting")
|
||||
logger.Info("[groups] exporting")
|
||||
startTime := time.Now()
|
||||
|
||||
jobCh := make(chan *models.Movie, workers*2) // make a buffered channel to feed workers
|
||||
jobCh := make(chan *models.Group, workers*2) // make a buffered channel to feed workers
|
||||
|
||||
for w := 0; w < workers; w++ { // create export Studio workers
|
||||
moviesWg.Add(1)
|
||||
go t.exportMovie(ctx, &moviesWg, jobCh)
|
||||
groupsWg.Add(1)
|
||||
go t.exportGroup(ctx, &groupsWg, jobCh)
|
||||
}
|
||||
|
||||
for i, movie := range movies {
|
||||
for i, group := range groups {
|
||||
index := i + 1
|
||||
logger.Progressf("[movies] %d of %d", index, len(movies))
|
||||
logger.Progressf("[groups] %d of %d", index, len(groups))
|
||||
|
||||
jobCh <- movie // feed workers
|
||||
jobCh <- group // feed workers
|
||||
}
|
||||
|
||||
close(jobCh)
|
||||
moviesWg.Wait()
|
||||
groupsWg.Wait()
|
||||
|
||||
logger.Infof("[movies] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
||||
logger.Infof("[groups] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
||||
|
||||
}
|
||||
func (t *ExportTask) exportMovie(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.Movie) {
|
||||
func (t *ExportTask) exportGroup(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.Group) {
|
||||
defer wg.Done()
|
||||
|
||||
r := t.repository
|
||||
movieReader := r.Movie
|
||||
groupReader := r.Group
|
||||
studioReader := r.Studio
|
||||
tagReader := r.Tag
|
||||
|
||||
for m := range jobChan {
|
||||
newMovieJSON, err := movie.ToJSON(ctx, movieReader, studioReader, m)
|
||||
if err := m.LoadURLs(ctx, r.Group); err != nil {
|
||||
logger.Errorf("[groups] <%s> error getting group urls: %v", m.Name, err)
|
||||
continue
|
||||
}
|
||||
if err := m.LoadSubGroupIDs(ctx, r.Group); err != nil {
|
||||
logger.Errorf("[groups] <%s> error getting group sub-groups: %v", m.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
newGroupJSON, err := group.ToJSON(ctx, groupReader, studioReader, m)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("[movies] <%s> error getting tag JSON: %v", m.Name, err)
|
||||
logger.Errorf("[groups] <%s> error getting tag JSON: %v", m.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
tags, err := tagReader.FindByGroupID(ctx, m.ID)
|
||||
if err != nil {
|
||||
logger.Errorf("[groups] <%s> error getting image tag names: %v", m.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
newGroupJSON.Tags = tag.GetNames(tags)
|
||||
|
||||
subGroups := m.SubGroups.List()
|
||||
if err := func() error {
|
||||
for _, sg := range subGroups {
|
||||
subGroup, err := groupReader.Find(ctx, sg.GroupID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting sub group: %v", err)
|
||||
}
|
||||
|
||||
newGroupJSON.SubGroups = append(newGroupJSON.SubGroups, jsonschema.SubGroupDescription{
|
||||
// TODO - this won't be unique
|
||||
Group: subGroup.Name,
|
||||
Description: sg.Description,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
logger.Errorf("[groups] <%s> %v", m.Name, err)
|
||||
}
|
||||
|
||||
if t.includeDependencies {
|
||||
if m.StudioID != nil {
|
||||
t.studios.IDs = sliceutil.AppendUnique(t.studios.IDs, *m.StudioID)
|
||||
}
|
||||
}
|
||||
|
||||
fn := newMovieJSON.Filename()
|
||||
fn := newGroupJSON.Filename()
|
||||
|
||||
if err := t.json.saveMovie(fn, newMovieJSON); err != nil {
|
||||
logger.Errorf("[movies] <%s> failed to save json: %v", m.Name, err)
|
||||
if err := t.json.saveGroup(fn, newGroupJSON); err != nil {
|
||||
logger.Errorf("[groups] <%s> failed to save json: %v", m.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
"github.com/stashapp/stash/pkg/movie"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/studio"
|
||||
@@ -127,7 +127,7 @@ func (t *ImportTask) Start(ctx context.Context) {
|
||||
t.ImportTags(ctx)
|
||||
t.ImportPerformers(ctx)
|
||||
t.ImportStudios(ctx)
|
||||
t.ImportMovies(ctx)
|
||||
t.ImportGroups(ctx)
|
||||
t.ImportFiles(ctx)
|
||||
t.ImportGalleries(ctx)
|
||||
|
||||
@@ -292,8 +292,11 @@ func (t *ImportTask) ImportStudios(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (t *ImportTask) importStudio(ctx context.Context, studioJSON *jsonschema.Studio, pendingParent map[string][]*jsonschema.Studio) error {
|
||||
r := t.repository
|
||||
|
||||
importer := &studio.Importer{
|
||||
ReaderWriter: t.repository.Studio,
|
||||
TagWriter: r.Tag,
|
||||
Input: *studioJSON,
|
||||
MissingRefBehaviour: t.MissingRefBehaviour,
|
||||
}
|
||||
@@ -322,14 +325,15 @@ func (t *ImportTask) importStudio(ctx context.Context, studioJSON *jsonschema.St
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ImportTask) ImportMovies(ctx context.Context) {
|
||||
logger.Info("[movies] importing")
|
||||
func (t *ImportTask) ImportGroups(ctx context.Context) {
|
||||
logger.Info("[groups] importing")
|
||||
pendingSubs := make(map[string][]*jsonschema.Group)
|
||||
|
||||
path := t.json.json.Movies
|
||||
path := t.json.json.Groups
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
logger.Errorf("[movies] failed to read movies directory: %v", err)
|
||||
logger.Errorf("[groups] failed to read movies directory: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -339,30 +343,79 @@ func (t *ImportTask) ImportMovies(ctx context.Context) {
|
||||
|
||||
for i, fi := range files {
|
||||
index := i + 1
|
||||
movieJSON, err := jsonschema.LoadMovieFile(filepath.Join(path, fi.Name()))
|
||||
groupJSON, err := jsonschema.LoadGroupFile(filepath.Join(path, fi.Name()))
|
||||
if err != nil {
|
||||
logger.Errorf("[movies] failed to read json: %v", err)
|
||||
logger.Errorf("[groups] failed to read json: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Progressf("[movies] %d of %d", index, len(files))
|
||||
logger.Progressf("[groups] %d of %d", index, len(files))
|
||||
|
||||
if err := r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
movieImporter := &movie.Importer{
|
||||
ReaderWriter: r.Movie,
|
||||
StudioWriter: r.Studio,
|
||||
Input: *movieJSON,
|
||||
MissingRefBehaviour: t.MissingRefBehaviour,
|
||||
return t.importGroup(ctx, groupJSON, pendingSubs, false)
|
||||
}); err != nil {
|
||||
var subError group.SubGroupNotExistError
|
||||
if errors.As(err, &subError) {
|
||||
missingSub := subError.MissingSubGroup()
|
||||
pendingSubs[missingSub] = append(pendingSubs[missingSub], groupJSON)
|
||||
continue
|
||||
}
|
||||
|
||||
return performImport(ctx, movieImporter, t.DuplicateBehaviour)
|
||||
}); err != nil {
|
||||
logger.Errorf("[movies] <%s> import failed: %v", fi.Name(), err)
|
||||
logger.Errorf("[groups] <%s> failed to import: %v", fi.Name(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("[movies] import complete")
|
||||
for _, s := range pendingSubs {
|
||||
for _, orphanGroupJSON := range s {
|
||||
if err := r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
return t.importGroup(ctx, orphanGroupJSON, nil, true)
|
||||
}); err != nil {
|
||||
logger.Errorf("[groups] <%s> failed to create: %v", orphanGroupJSON.Name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("[groups] import complete")
|
||||
}
|
||||
|
||||
func (t *ImportTask) importGroup(ctx context.Context, groupJSON *jsonschema.Group, pendingSub map[string][]*jsonschema.Group, fail bool) error {
|
||||
r := t.repository
|
||||
|
||||
importer := &group.Importer{
|
||||
ReaderWriter: r.Group,
|
||||
StudioWriter: r.Studio,
|
||||
TagWriter: r.Tag,
|
||||
Input: *groupJSON,
|
||||
MissingRefBehaviour: t.MissingRefBehaviour,
|
||||
}
|
||||
|
||||
// first phase: return error if parent does not exist
|
||||
if !fail {
|
||||
importer.MissingRefBehaviour = models.ImportMissingRefEnumFail
|
||||
}
|
||||
|
||||
if err := performImport(ctx, importer, t.DuplicateBehaviour); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, containingGroupJSON := range pendingSub[groupJSON.Name] {
|
||||
if err := t.importGroup(ctx, containingGroupJSON, pendingSub, fail); err != nil {
|
||||
var subError group.SubGroupNotExistError
|
||||
if errors.As(err, &subError) {
|
||||
missingSub := subError.MissingSubGroup()
|
||||
pendingSub[missingSub] = append(pendingSub[missingSub], containingGroupJSON)
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to create containing group <%s>: %v", containingGroupJSON.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
delete(pendingSub, groupJSON.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ImportTask) ImportFiles(ctx context.Context) {
|
||||
@@ -644,7 +697,7 @@ func (t *ImportTask) ImportScenes(ctx context.Context) {
|
||||
MissingRefBehaviour: t.MissingRefBehaviour,
|
||||
|
||||
GalleryFinder: r.Gallery,
|
||||
MovieWriter: r.Movie,
|
||||
GroupWriter: r.Group,
|
||||
PerformerWriter: r.Performer,
|
||||
StudioWriter: r.Studio,
|
||||
TagWriter: r.Tag,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package static provides the static files embedded in the application.
|
||||
package static
|
||||
|
||||
import (
|
||||
@@ -7,7 +8,7 @@ import (
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed performer performer_male scene image tag studio movie
|
||||
//go:embed performer performer_male scene image gallery tag studio group
|
||||
var data embed.FS
|
||||
|
||||
const (
|
||||
@@ -20,14 +21,17 @@ const (
|
||||
Image = "image"
|
||||
DefaultImageImage = "image/image.svg"
|
||||
|
||||
Gallery = "gallery"
|
||||
DefaultGalleryImage = "gallery/gallery.svg"
|
||||
|
||||
Tag = "tag"
|
||||
DefaultTagImage = "tag/tag.svg"
|
||||
|
||||
Studio = "studio"
|
||||
DefaultStudioImage = "studio/studio.svg"
|
||||
|
||||
Movie = "movie"
|
||||
DefaultMovieImage = "movie/movie.png"
|
||||
Group = "group"
|
||||
DefaultGroupImage = "group/group.svg"
|
||||
)
|
||||
|
||||
// Sub returns an FS rooted at path, using fs.Sub.
|
||||
|
||||
6
internal/static/gallery/gallery.svg
Normal file
6
internal/static/gallery/gallery.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-352 -104 1280 720">
|
||||
<!--! Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc.
|
||||
Modified from https://github.com/FortAwesome/Font-Awesome/blob/6.x/svgs/solid/images.svg
|
||||
Changed view box and fill style.
|
||||
-->
|
||||
<path d="M160 32c-35.3 0-64 28.7-64 64l0 224c0 35.3 28.7 64 64 64l352 0c35.3 0 64-28.7 64-64l0-224c0-35.3-28.7-64-64-64L160 32zM396 138.7l96 144c4.9 7.4 5.4 16.8 1.2 24.6S480.9 320 472 320l-144 0-48 0-80 0c-9.2 0-17.6-5.3-21.6-13.6s-2.9-18.2 2.9-25.4l64-80c4.6-5.7 11.4-9 18.7-9s14.2 3.3 18.7 9l17.3 21.6 56-84C360.5 132 368 128 376 128s15.5 4 20 10.7zM192 128a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM48 120c0-13.3-10.7-24-24-24S0 106.7 0 120L0 344c0 75.1 60.9 136 136 136l320 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-320 0c-48.6 0-88-39.4-88-88l0-224z" style="fill:#ffffff;fill-opacity:1"/></svg>
|
||||
|
After Width: | Height: | Size: 996 B |
6
internal/static/group/group.svg
Normal file
6
internal/static/group/group.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-384 -104 1280 720">
|
||||
<!--! Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc.
|
||||
Modified from https://github.com/FortAwesome/Font-Awesome/blob/6.x/svgs/solid/film.svg
|
||||
Changed view box and added fill style.
|
||||
-->
|
||||
<path d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM48 368l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm368-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM48 240l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0c-8.8 0-16 7.2-16 16zm368-16c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM48 112l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16L64 96c-8.8 0-16 7.2-16 16zM416 96c-8.8 0-16 7.2-16 16l0 32c0 8.8 7.2 16 16 16l32 0c8.8 0 16-7.2 16-16l0-32c0-8.8-7.2-16-16-16l-32 0zM160 128l0 64c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-64c0-17.7-14.3-32-32-32L192 96c-17.7 0-32 14.3-32 32zm32 160c-17.7 0-32 14.3-32 32l0 64c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32l0-64c0-17.7-14.3-32-32-32l-128 0z" style="fill:#ffffff;fill-opacity:1"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 405 B |
@@ -20,7 +20,7 @@ var validForHevc = []Container{Mp4}
|
||||
|
||||
var validAudioForMkv = []ProbeAudioCodec{Aac, Mp3, Vorbis, Opus}
|
||||
var validAudioForWebm = []ProbeAudioCodec{Vorbis, Opus}
|
||||
var validAudioForMp4 = []ProbeAudioCodec{Aac, Mp3}
|
||||
var validAudioForMp4 = []ProbeAudioCodec{Aac, Mp3, Opus}
|
||||
|
||||
var (
|
||||
// ErrUnsupportedVideoCodecForBrowser is returned when the video codec is not supported for browser streaming.
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
package ffmpeg
|
||||
|
||||
type VideoCodec string
|
||||
type VideoCodec struct {
|
||||
Name string // The full name of the codec including profile/quality
|
||||
CodeName string // The core codec name without profile/quality suffix
|
||||
}
|
||||
|
||||
func makeVideoCodec(name string, codename string) VideoCodec {
|
||||
return VideoCodec{name, codename}
|
||||
}
|
||||
|
||||
func (c VideoCodec) Args() []string {
|
||||
if c == "" {
|
||||
if c.CodeName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []string{"-c:v", string(c)}
|
||||
return []string{"-c:v", string(c.CodeName)}
|
||||
}
|
||||
|
||||
var (
|
||||
// Software codec's
|
||||
VideoCodecLibX264 VideoCodec = "libx264"
|
||||
VideoCodecLibWebP VideoCodec = "libwebp"
|
||||
VideoCodecBMP VideoCodec = "bmp"
|
||||
VideoCodecMJpeg VideoCodec = "mjpeg"
|
||||
VideoCodecVP9 VideoCodec = "libvpx-vp9"
|
||||
VideoCodecVPX VideoCodec = "libvpx"
|
||||
VideoCodecLibX265 VideoCodec = "libx265"
|
||||
VideoCodecCopy VideoCodec = "copy"
|
||||
VideoCodecLibX264 = makeVideoCodec("x264", "libx264")
|
||||
VideoCodecLibWebP = makeVideoCodec("WebP", "libwebp")
|
||||
VideoCodecBMP = makeVideoCodec("BMP", "bmp")
|
||||
VideoCodecMJpeg = makeVideoCodec("Jpeg", "mjpeg")
|
||||
VideoCodecVP9 = makeVideoCodec("VPX-VP9", "libvpx-vp9")
|
||||
VideoCodecVPX = makeVideoCodec("VPX-VP8", "libvpx")
|
||||
VideoCodecLibX265 = makeVideoCodec("x265", "libx265")
|
||||
VideoCodecCopy = makeVideoCodec("Copy", "copy")
|
||||
)
|
||||
|
||||
type AudioCodec string
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
@@ -13,42 +15,49 @@ import (
|
||||
|
||||
var (
|
||||
// Hardware codec's
|
||||
VideoCodecN264 VideoCodec = "h264_nvenc"
|
||||
VideoCodecI264 VideoCodec = "h264_qsv"
|
||||
VideoCodecA264 VideoCodec = "h264_amf"
|
||||
VideoCodecM264 VideoCodec = "h264_videotoolbox"
|
||||
VideoCodecV264 VideoCodec = "h264_vaapi"
|
||||
VideoCodecR264 VideoCodec = "h264_v4l2m2m"
|
||||
VideoCodecO264 VideoCodec = "h264_omx"
|
||||
VideoCodecIVP9 VideoCodec = "vp9_qsv"
|
||||
VideoCodecVVP9 VideoCodec = "vp9_vaapi"
|
||||
VideoCodecVVPX VideoCodec = "vp8_vaapi"
|
||||
VideoCodecN264 = makeVideoCodec("H264 NVENC", "h264_nvenc")
|
||||
VideoCodecN264H = makeVideoCodec("H264 NVENC HQ profile", "h264_nvenc")
|
||||
VideoCodecI264 = makeVideoCodec("H264 Intel Quick Sync Video (QSV)", "h264_qsv")
|
||||
VideoCodecI264C = makeVideoCodec("H264 Intel Quick Sync Video (QSV) Compatibility profile", "h264_qsv")
|
||||
VideoCodecA264 = makeVideoCodec("H264 Advanced Media Framework (AMF)", "h264_amf")
|
||||
VideoCodecM264 = makeVideoCodec("H264 VideoToolbox", "h264_videotoolbox")
|
||||
VideoCodecV264 = makeVideoCodec("H264 VAAPI", "h264_vaapi")
|
||||
VideoCodecR264 = makeVideoCodec("H264 V4L2M2M", "h264_v4l2m2m")
|
||||
VideoCodecO264 = makeVideoCodec("H264 OMX", "h264_omx")
|
||||
VideoCodecIVP9 = makeVideoCodec("VP9 Intel Quick Sync Video (QSV)", "vp9_qsv")
|
||||
VideoCodecVVP9 = makeVideoCodec("VP9 VAAPI", "vp9_vaapi")
|
||||
VideoCodecVVPX = makeVideoCodec("VP8 VAAPI", "vp8_vaapi")
|
||||
)
|
||||
|
||||
const minHeight int = 256
|
||||
const minHeight int = 480
|
||||
|
||||
// Tests all (given) hardware codec's
|
||||
func (f *FFMpeg) InitHWSupport(ctx context.Context) {
|
||||
var hwCodecSupport []VideoCodec
|
||||
|
||||
// Note that the first compatible codec is returned, so order is important
|
||||
for _, codec := range []VideoCodec{
|
||||
VideoCodecN264H,
|
||||
VideoCodecN264,
|
||||
VideoCodecI264,
|
||||
VideoCodecI264C,
|
||||
VideoCodecV264,
|
||||
VideoCodecR264,
|
||||
VideoCodecIVP9,
|
||||
VideoCodecVVP9,
|
||||
VideoCodecM264,
|
||||
} {
|
||||
var args Args
|
||||
args = append(args, "-hide_banner")
|
||||
args = args.LogLevel(LogLevelWarning)
|
||||
args = f.hwDeviceInit(args, codec, false)
|
||||
args = args.Format("lavfi")
|
||||
args = args.Input(fmt.Sprintf("color=c=red:s=%dx%d", 1280, 720))
|
||||
vFile := &models.VideoFile{Width: 1280, Height: 720}
|
||||
args = args.Input(fmt.Sprintf("color=c=red:s=%dx%d", vFile.Width, vFile.Height))
|
||||
args = args.Duration(0.1)
|
||||
|
||||
// Test scaling
|
||||
videoFilter := f.hwMaxResFilter(codec, 1280, 720, minHeight, false)
|
||||
videoFilter := f.hwMaxResFilter(codec, vFile, minHeight, false)
|
||||
args = append(args, CodecInit(codec)...)
|
||||
args = args.VideoFilter(videoFilter)
|
||||
|
||||
@@ -75,7 +84,7 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
|
||||
|
||||
outstr := fmt.Sprintf("[InitHWSupport] Supported HW codecs [%d]:\n", len(hwCodecSupport))
|
||||
for _, codec := range hwCodecSupport {
|
||||
outstr += fmt.Sprintf("\t%s\n", codec)
|
||||
outstr += fmt.Sprintf("\t%s - %s\n", codec.Name, codec.CodeName)
|
||||
}
|
||||
logger.Info(outstr)
|
||||
|
||||
@@ -93,9 +102,9 @@ func (f *FFMpeg) hwCanFullHWTranscode(ctx context.Context, codec VideoCodec, vf
|
||||
args = args.XError()
|
||||
args = f.hwDeviceInit(args, codec, true)
|
||||
args = args.Input(vf.Path)
|
||||
args = args.Duration(0.1)
|
||||
args = args.Duration(1)
|
||||
|
||||
videoFilter := f.hwMaxResFilter(codec, vf.Width, vf.Height, reqHeight, true)
|
||||
videoFilter := f.hwMaxResFilter(codec, vf, reqHeight, true)
|
||||
args = append(args, CodecInit(codec)...)
|
||||
args = args.VideoFilter(videoFilter)
|
||||
|
||||
@@ -124,16 +133,17 @@ func (f *FFMpeg) hwCanFullHWTranscode(ctx context.Context, codec VideoCodec, vf
|
||||
// Prepend input for hardware encoding only
|
||||
func (f *FFMpeg) hwDeviceInit(args Args, toCodec VideoCodec, fullhw bool) Args {
|
||||
switch toCodec {
|
||||
case VideoCodecN264:
|
||||
case VideoCodecN264,
|
||||
VideoCodecN264H:
|
||||
args = append(args, "-hwaccel_device")
|
||||
args = append(args, "0")
|
||||
if fullhw {
|
||||
args = append(args, "-threads")
|
||||
args = append(args, "1")
|
||||
args = append(args, "-hwaccel")
|
||||
args = append(args, "cuda")
|
||||
args = append(args, "-hwaccel_output_format")
|
||||
args = append(args, "cuda")
|
||||
args = append(args, "-extra_hw_frames")
|
||||
args = append(args, "5")
|
||||
}
|
||||
case VideoCodecV264,
|
||||
VideoCodecVVP9:
|
||||
@@ -146,6 +156,7 @@ func (f *FFMpeg) hwDeviceInit(args Args, toCodec VideoCodec, fullhw bool) Args {
|
||||
args = append(args, "vaapi")
|
||||
}
|
||||
case VideoCodecI264,
|
||||
VideoCodecI264C,
|
||||
VideoCodecIVP9:
|
||||
if fullhw {
|
||||
args = append(args, "-hwaccel")
|
||||
@@ -158,6 +169,16 @@ func (f *FFMpeg) hwDeviceInit(args Args, toCodec VideoCodec, fullhw bool) Args {
|
||||
args = append(args, "-filter_hw_device")
|
||||
args = append(args, "hw")
|
||||
}
|
||||
case VideoCodecM264:
|
||||
if fullhw {
|
||||
args = append(args, "-hwaccel")
|
||||
args = append(args, "videotoolbox")
|
||||
args = append(args, "-hwaccel_output_format")
|
||||
args = append(args, "videotoolbox_vld")
|
||||
} else {
|
||||
args = append(args, "-init_hw_device")
|
||||
args = append(args, "videotoolbox=vt")
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
@@ -173,91 +194,160 @@ func (f *FFMpeg) hwFilterInit(toCodec VideoCodec, fullhw bool) VideoFilter {
|
||||
videoFilter = videoFilter.Append("format=nv12")
|
||||
videoFilter = videoFilter.Append("hwupload")
|
||||
}
|
||||
case VideoCodecN264:
|
||||
case VideoCodecN264, VideoCodecN264H:
|
||||
if !fullhw {
|
||||
videoFilter = videoFilter.Append("format=nv12")
|
||||
videoFilter = videoFilter.Append("hwupload_cuda")
|
||||
}
|
||||
case VideoCodecI264,
|
||||
VideoCodecI264C,
|
||||
VideoCodecIVP9:
|
||||
if !fullhw {
|
||||
videoFilter = videoFilter.Append("hwupload=extra_hw_frames=64")
|
||||
videoFilter = videoFilter.Append("format=qsv")
|
||||
}
|
||||
case VideoCodecM264:
|
||||
if !fullhw {
|
||||
videoFilter = videoFilter.Append("format=nv12")
|
||||
videoFilter = videoFilter.Append("hwupload")
|
||||
}
|
||||
}
|
||||
|
||||
return videoFilter
|
||||
}
|
||||
|
||||
var scaler_re = regexp.MustCompile(`scale=(?P<value>[-\d]+:[-\d]+)`)
|
||||
var scaler_re = regexp.MustCompile(`scale=(?P<value>([-\d]+):([-\d]+))`)
|
||||
|
||||
func templateReplaceScale(input string, template string, match []int, minusonehack bool) string {
|
||||
func templateReplaceScale(input string, template string, match []int, vf *models.VideoFile, minusonehack bool) string {
|
||||
result := []byte{}
|
||||
|
||||
res := string(scaler_re.ExpandString(result, template, input, match))
|
||||
|
||||
// BUG: [scale_qsv]: Size values less than -1 are not acceptable.
|
||||
// Fix: Replace all instances of -2 with -1 in a scale operation
|
||||
if minusonehack {
|
||||
res = strings.ReplaceAll(res, "-2", "-1")
|
||||
// Parse width and height
|
||||
w, err := strconv.Atoi(input[match[4]:match[5]])
|
||||
if err != nil {
|
||||
logger.Error("failed to parse width")
|
||||
return input
|
||||
}
|
||||
h, err := strconv.Atoi(input[match[6]:match[7]])
|
||||
if err != nil {
|
||||
logger.Error("failed to parse height")
|
||||
return input
|
||||
}
|
||||
|
||||
// Calculate ratio
|
||||
ratio := float64(vf.Width) / float64(vf.Height)
|
||||
if w < 0 {
|
||||
w = int(math.Round(float64(h) * ratio))
|
||||
} else if h < 0 {
|
||||
h = int(math.Round(float64(w) / ratio))
|
||||
}
|
||||
|
||||
// Fix not divisible by 2 errors
|
||||
if w%2 != 0 {
|
||||
w++
|
||||
}
|
||||
if h%2 != 0 {
|
||||
h++
|
||||
}
|
||||
|
||||
template = strings.ReplaceAll(template, "$value", fmt.Sprintf("%d:%d", w, h))
|
||||
}
|
||||
|
||||
res := string(scaler_re.ExpandString(result, template, input, match))
|
||||
|
||||
matchStart := match[0]
|
||||
matchEnd := match[1]
|
||||
|
||||
return input[0:matchStart] + res + input[matchEnd:]
|
||||
}
|
||||
|
||||
// Replace video filter scaling with hardware scaling for full hardware transcoding
|
||||
func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec, fullhw bool) VideoFilter {
|
||||
// Replace video filter scaling with hardware scaling for full hardware transcoding (also fixes the format)
|
||||
func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec, vf *models.VideoFile, fullhw bool) VideoFilter {
|
||||
sargs := string(args)
|
||||
|
||||
match := scaler_re.FindStringSubmatchIndex(sargs)
|
||||
if match == nil {
|
||||
return args
|
||||
return f.hwApplyFullHWFilter(args, codec, fullhw)
|
||||
}
|
||||
|
||||
return f.hwApplyScaleTemplate(sargs, codec, match, vf, fullhw)
|
||||
}
|
||||
|
||||
// Apply format switching if applicable
|
||||
func (f *FFMpeg) hwApplyFullHWFilter(args VideoFilter, codec VideoCodec, fullhw bool) VideoFilter {
|
||||
switch codec {
|
||||
case VideoCodecN264:
|
||||
template := "scale_cuda=$value"
|
||||
// In 10bit inputs you might get an error like "10 bit encode not supported"
|
||||
if fullhw && f.version.major >= 5 {
|
||||
template += ":format=nv12"
|
||||
case VideoCodecN264, VideoCodecN264H:
|
||||
if fullhw && f.version.Gteq(Version{major: 5}) { // Added in FFMpeg 5
|
||||
args = args.Append("scale_cuda=format=yuv420p")
|
||||
}
|
||||
case VideoCodecV264, VideoCodecVVP9:
|
||||
if fullhw && f.version.Gteq(Version{major: 3, minor: 1}) { // Added in FFMpeg 3.1
|
||||
args = args.Append("scale_vaapi=format=nv12")
|
||||
}
|
||||
case VideoCodecI264, VideoCodecI264C, VideoCodecIVP9:
|
||||
if fullhw && f.version.Gteq(Version{major: 3, minor: 3}) { // Added in FFMpeg 3.3
|
||||
args = args.Append("scale_qsv=format=nv12")
|
||||
}
|
||||
args = VideoFilter(templateReplaceScale(sargs, template, match, false))
|
||||
case VideoCodecV264,
|
||||
VideoCodecVVP9:
|
||||
template := "scale_vaapi=$value"
|
||||
args = VideoFilter(templateReplaceScale(sargs, template, match, false))
|
||||
case VideoCodecI264,
|
||||
VideoCodecIVP9:
|
||||
template := "scale_qsv=$value"
|
||||
args = VideoFilter(templateReplaceScale(sargs, template, match, true))
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// Switch scaler
|
||||
func (f *FFMpeg) hwApplyScaleTemplate(sargs string, codec VideoCodec, match []int, vf *models.VideoFile, fullhw bool) VideoFilter {
|
||||
var template string
|
||||
|
||||
switch codec {
|
||||
case VideoCodecN264, VideoCodecN264H:
|
||||
template = "scale_cuda=$value"
|
||||
if fullhw && f.version.Gteq(Version{major: 5}) { // Added in FFMpeg 5
|
||||
template += ":format=yuv420p"
|
||||
}
|
||||
case VideoCodecV264, VideoCodecVVP9:
|
||||
template = "scale_vaapi=$value"
|
||||
if fullhw && f.version.Gteq(Version{major: 3, minor: 1}) { // Added in FFMpeg 3.1
|
||||
template += ":format=nv12"
|
||||
}
|
||||
case VideoCodecI264, VideoCodecI264C, VideoCodecIVP9:
|
||||
template = "scale_qsv=$value"
|
||||
if fullhw && f.version.Gteq(Version{major: 3, minor: 3}) { // Added in FFMpeg 3.3
|
||||
template += ":format=nv12"
|
||||
}
|
||||
case VideoCodecM264:
|
||||
template = "scale_vt=$value"
|
||||
default:
|
||||
return VideoFilter(sargs)
|
||||
}
|
||||
|
||||
// BUG: [scale_qsv]: Size values less than -1 are not acceptable.
|
||||
isIntel := codec == VideoCodecI264 || codec == VideoCodecI264C || codec == VideoCodecIVP9
|
||||
// BUG: scale_vt doesn't call ff_scale_adjust_dimensions, thus cant accept negative size values
|
||||
isApple := codec == VideoCodecM264
|
||||
return VideoFilter(templateReplaceScale(sargs, template, match, vf, isIntel || isApple))
|
||||
}
|
||||
|
||||
// Returns the max resolution for a given codec, or a default
|
||||
func (f *FFMpeg) hwCodecMaxRes(codec VideoCodec, dW int, dH int) (int, int) {
|
||||
func (f *FFMpeg) hwCodecMaxRes(codec VideoCodec) (int, int) {
|
||||
switch codec {
|
||||
case VideoCodecN264,
|
||||
VideoCodecI264:
|
||||
VideoCodecN264H,
|
||||
VideoCodecI264,
|
||||
VideoCodecI264C:
|
||||
return 4096, 4096
|
||||
}
|
||||
|
||||
return dW, dH
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Return a maxres filter
|
||||
func (f *FFMpeg) hwMaxResFilter(toCodec VideoCodec, width int, height int, reqHeight int, fullhw bool) VideoFilter {
|
||||
if width == 0 || height == 0 {
|
||||
func (f *FFMpeg) hwMaxResFilter(toCodec VideoCodec, vf *models.VideoFile, reqHeight int, fullhw bool) VideoFilter {
|
||||
if vf.Width == 0 || vf.Height == 0 {
|
||||
return ""
|
||||
}
|
||||
videoFilter := f.hwFilterInit(toCodec, fullhw)
|
||||
maxWidth, maxHeight := f.hwCodecMaxRes(toCodec, width, height)
|
||||
videoFilter = videoFilter.ScaleMaxLM(width, height, reqHeight, maxWidth, maxHeight)
|
||||
return f.hwCodecFilter(videoFilter, toCodec, fullhw)
|
||||
maxWidth, maxHeight := f.hwCodecMaxRes(toCodec)
|
||||
videoFilter = videoFilter.ScaleMaxLM(vf.Width, vf.Height, reqHeight, maxWidth, maxHeight)
|
||||
return f.hwCodecFilter(videoFilter, toCodec, vf, fullhw)
|
||||
}
|
||||
|
||||
// Return if a hardware accelerated for HLS is available
|
||||
@@ -265,9 +355,12 @@ func (f *FFMpeg) hwCodecHLSCompatible() *VideoCodec {
|
||||
for _, element := range f.hwCodecSupport {
|
||||
switch element {
|
||||
case VideoCodecN264,
|
||||
VideoCodecN264H,
|
||||
VideoCodecI264,
|
||||
VideoCodecI264C,
|
||||
VideoCodecV264,
|
||||
VideoCodecR264:
|
||||
VideoCodecR264,
|
||||
VideoCodecM264: // Note that the Apple encoder sucks at startup, thus HLS quality is crap
|
||||
return &element
|
||||
}
|
||||
}
|
||||
@@ -279,7 +372,10 @@ func (f *FFMpeg) hwCodecMP4Compatible() *VideoCodec {
|
||||
for _, element := range f.hwCodecSupport {
|
||||
switch element {
|
||||
case VideoCodecN264,
|
||||
VideoCodecI264:
|
||||
VideoCodecN264H,
|
||||
VideoCodecI264,
|
||||
VideoCodecI264C,
|
||||
VideoCodecM264:
|
||||
return &element
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ func GetFFmpegURL() []string {
|
||||
case "linux":
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
urls = []string{"https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-linux-64.zip", "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2.1/ffprobe-4.2.1-linux-64.zip"}
|
||||
urls = []string{"https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v6.1/ffmpeg-6.1-linux-64.zip", "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v6.1/ffprobe-6.1-linux-64.zip"}
|
||||
case "arm":
|
||||
urls = []string{"https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-linux-armhf-32.zip", "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2.1/ffprobe-4.2.1-linux-armhf-32.zip"}
|
||||
urls = []string{"https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v6.1/ffmpeg-6.1-linux-armhf-32.zip", "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v6.1/ffprobe-6.1-linux-armhf-32.zip"}
|
||||
case "arm64":
|
||||
urls = []string{"https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-linux-arm-64.zip", "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2.1/ffprobe-4.2.1-linux-arm-64.zip"}
|
||||
urls = []string{"https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v6.1/ffmpeg-6.1-linux-arm-64.zip", "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v6.1/ffprobe-6.1-linux-arm-64.zip"}
|
||||
}
|
||||
case "windows":
|
||||
urls = []string{"https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"}
|
||||
|
||||
@@ -145,6 +145,8 @@ func ResolveFFMpeg(path string, fallbackPath string) string {
|
||||
return ret
|
||||
}
|
||||
|
||||
var version_re = regexp.MustCompile(`ffmpeg version n?((\d+)\.(\d+)(?:\.(\d+))?)`)
|
||||
|
||||
func (f *FFMpeg) getVersion() error {
|
||||
var args Args
|
||||
args = append(args, "-version")
|
||||
@@ -158,7 +160,6 @@ func (f *FFMpeg) getVersion() error {
|
||||
return err
|
||||
}
|
||||
|
||||
version_re := regexp.MustCompile(`ffmpeg version ((\d+)\.(\d+)(?:\.(\d+))?)`)
|
||||
stdoutStr := stdout.String()
|
||||
match := version_re.FindStringSubmatchIndex(stdoutStr)
|
||||
if match == nil {
|
||||
@@ -183,22 +184,40 @@ func (f *FFMpeg) getVersion() error {
|
||||
if i, err := strconv.Atoi(patchS); err == nil {
|
||||
f.version.patch = i
|
||||
}
|
||||
logger.Debugf("FFMpeg version %d.%d.%d detected", f.version.major, f.version.minor, f.version.patch)
|
||||
logger.Debugf("FFMpeg version %s detected", f.version.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FFMpeg version params
|
||||
type FFMpegVersion struct {
|
||||
type Version struct {
|
||||
major int
|
||||
minor int
|
||||
patch int
|
||||
}
|
||||
|
||||
// Gteq returns true if the version is greater than or equal to the other version.
|
||||
func (v Version) Gteq(other Version) bool {
|
||||
if v.major > other.major {
|
||||
return true
|
||||
}
|
||||
if v.major == other.major && v.minor > other.minor {
|
||||
return true
|
||||
}
|
||||
if v.major == other.major && v.minor == other.minor && v.patch >= other.patch {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch)
|
||||
}
|
||||
|
||||
// FFMpeg provides an interface to ffmpeg.
|
||||
type FFMpeg struct {
|
||||
ffmpeg string
|
||||
version FFMpegVersion
|
||||
version Version
|
||||
hwCodecSupport []VideoCodec
|
||||
}
|
||||
|
||||
|
||||
75
pkg/ffmpeg/ffmpeg_test.go
Normal file
75
pkg/ffmpeg/ffmpeg_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Package ffmpeg provides a wrapper around the ffmpeg and ffprobe executables.
|
||||
package ffmpeg
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFFMpegVersion_GreaterThan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
this Version
|
||||
other Version
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"major greater, minor equal, patch equal",
|
||||
Version{2, 0, 0},
|
||||
Version{1, 0, 0},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"major greater, minor less, patch less",
|
||||
Version{2, 1, 1},
|
||||
Version{1, 0, 0},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"major equal, minor greater, patch equal",
|
||||
Version{1, 1, 0},
|
||||
Version{1, 0, 0},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"major equal, minor equal, patch greater",
|
||||
Version{1, 0, 1},
|
||||
Version{1, 0, 0},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"major equal, minor equal, patch equal",
|
||||
Version{1, 0, 0},
|
||||
Version{1, 0, 0},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"major less, minor equal, patch equal",
|
||||
Version{1, 0, 0},
|
||||
Version{2, 0, 0},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"major equal, minor less, patch equal",
|
||||
Version{1, 0, 0},
|
||||
Version{1, 1, 0},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"major equal, minor equal, patch less",
|
||||
Version{1, 0, 0},
|
||||
Version{1, 0, 1},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"major less, minor less, patch less",
|
||||
Version{1, 0, 0},
|
||||
Version{2, 1, 1},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.this.Gteq(tt.other); got != tt.want {
|
||||
t.Errorf("FFMpegVersion.GreaterThan() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -16,6 +18,8 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
const minimumFFProbeVersion = 5
|
||||
|
||||
func ValidateFFProbe(ffprobePath string) error {
|
||||
cmd := stashExec.Command(ffprobePath, "-h")
|
||||
bytes, err := cmd.CombinedOutput()
|
||||
@@ -139,16 +143,94 @@ func (v *VideoFile) TranscodeScale(maxSize int) (int, int) {
|
||||
}
|
||||
|
||||
// FFProbe provides an interface to the ffprobe executable.
|
||||
type FFProbe string
|
||||
type FFProbe struct {
|
||||
path string
|
||||
version Version
|
||||
}
|
||||
|
||||
func (f *FFProbe) Path() string {
|
||||
return string(*f)
|
||||
return f.path
|
||||
}
|
||||
|
||||
var ffprobeVersionRE = regexp.MustCompile(`ffprobe version n?((\d+)\.(\d+)(?:\.(\d+))?)`)
|
||||
|
||||
func (f *FFProbe) getVersion() error {
|
||||
var args []string
|
||||
args = append(args, "-version")
|
||||
cmd := stashExec.Command(f.path, args...)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
var err error
|
||||
if err = cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdoutStr := stdout.String()
|
||||
match := ffprobeVersionRE.FindStringSubmatchIndex(stdoutStr)
|
||||
if match == nil {
|
||||
return errors.New("version string malformed")
|
||||
}
|
||||
|
||||
majorS := stdoutStr[match[4]:match[5]]
|
||||
minorS := stdoutStr[match[6]:match[7]]
|
||||
|
||||
// patch is optional
|
||||
var patchS string
|
||||
if match[8] != -1 && match[9] != -1 {
|
||||
patchS = stdoutStr[match[8]:match[9]]
|
||||
}
|
||||
|
||||
if i, err := strconv.Atoi(majorS); err == nil {
|
||||
f.version.major = i
|
||||
}
|
||||
if i, err := strconv.Atoi(minorS); err == nil {
|
||||
f.version.minor = i
|
||||
}
|
||||
if i, err := strconv.Atoi(patchS); err == nil {
|
||||
f.version.patch = i
|
||||
}
|
||||
logger.Debugf("FFProbe version %s detected", f.version.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Creates a new FFProbe instance.
|
||||
func NewFFProbe(path string) *FFProbe {
|
||||
ret := &FFProbe{
|
||||
path: path,
|
||||
}
|
||||
if err := ret.getVersion(); err != nil {
|
||||
logger.Warnf("FFProbe version not detected %v", err)
|
||||
}
|
||||
|
||||
if ret.version.major != 0 && ret.version.major < minimumFFProbeVersion {
|
||||
logger.Warnf("FFProbe version %d.%d.%d detected, but %d.x or later is required", ret.version.major, ret.version.minor, ret.version.patch, minimumFFProbeVersion)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// NewVideoFile runs ffprobe on the given path and returns a VideoFile.
|
||||
func (f *FFProbe) NewVideoFile(videoPath string) (*VideoFile, error) {
|
||||
args := []string{"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "-show_error", videoPath}
|
||||
cmd := stashExec.Command(string(*f), args...)
|
||||
args := []string{
|
||||
"-v",
|
||||
"quiet",
|
||||
"-print_format", "json",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
"-show_error",
|
||||
}
|
||||
|
||||
// show_entries stream_side_data=rotation requires 5.x or later ffprobe
|
||||
if f.version.major >= 5 {
|
||||
args = append(args, "-show_entries", "stream_side_data=rotation")
|
||||
}
|
||||
|
||||
args = append(args, videoPath)
|
||||
|
||||
cmd := stashExec.Command(f.path, args...)
|
||||
out, err := cmd.Output()
|
||||
|
||||
if err != nil {
|
||||
@@ -167,7 +249,7 @@ func (f *FFProbe) NewVideoFile(videoPath string) (*VideoFile, error) {
|
||||
// Used when the frame count is missing or incorrect.
|
||||
func (f *FFProbe) GetReadFrameCount(path string) (int64, error) {
|
||||
args := []string{"-v", "quiet", "-print_format", "json", "-count_frames", "-show_format", "-show_streams", "-show_error", path}
|
||||
out, err := stashExec.Command(string(*f), args...).Output()
|
||||
out, err := stashExec.Command(f.path, args...).Output()
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", path, string(out), err.Error())
|
||||
@@ -246,13 +328,14 @@ func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
|
||||
framerate = 0
|
||||
}
|
||||
result.FrameRate = math.Round(framerate*100) / 100
|
||||
if rotate, err := strconv.ParseInt(videoStream.Tags.Rotate, 10, 64); err == nil && rotate != 180 {
|
||||
result.Width = videoStream.Width
|
||||
result.Height = videoStream.Height
|
||||
|
||||
if isRotated(videoStream) {
|
||||
result.Width = videoStream.Height
|
||||
result.Height = videoStream.Width
|
||||
} else {
|
||||
result.Width = videoStream.Width
|
||||
result.Height = videoStream.Height
|
||||
}
|
||||
|
||||
result.VideoStreamDuration, err = strconv.ParseFloat(videoStream.Duration, 64)
|
||||
if err != nil {
|
||||
// Revert to the historical behaviour, which is still correct in the vast majority of cases.
|
||||
@@ -263,6 +346,25 @@ func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isRotated(s *FFProbeStream) bool {
|
||||
rotate, _ := strconv.ParseInt(s.Tags.Rotate, 10, 64)
|
||||
if rotate != 180 && rotate != 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, sd := range s.SideDataList {
|
||||
r := sd.Rotation
|
||||
if r < 0 {
|
||||
r = -r
|
||||
}
|
||||
if r != 0 && r != 180 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *VideoFile) getAudioStream() *FFProbeStream {
|
||||
index := v.getStreamIndex("audio", v.JSON)
|
||||
if index != -1 {
|
||||
|
||||
@@ -59,33 +59,28 @@ func (f VideoFilter) ScaleMax(inputWidth, inputHeight, maxSize int) VideoFilter
|
||||
return f.ScaleDimensions(maxSize, -2)
|
||||
}
|
||||
|
||||
// ScaleMaxLM returns a VideoFilter scaling to maxSize with respect to a max size.
|
||||
// ScaleMaxLM scales an image to fit within specified maximum dimensions while maintaining its aspect ratio.
|
||||
func (f VideoFilter) ScaleMaxLM(width int, height int, reqHeight int, maxWidth int, maxHeight int) VideoFilter {
|
||||
// calculate the aspect ratio of the current resolution
|
||||
aspectRatio := width / height
|
||||
if maxWidth == 0 || maxHeight == 0 {
|
||||
return f.ScaleMax(width, height, reqHeight)
|
||||
}
|
||||
|
||||
// find the max height
|
||||
aspectRatio := float64(width) / float64(height)
|
||||
desiredHeight := reqHeight
|
||||
if desiredHeight == 0 {
|
||||
desiredHeight = height
|
||||
}
|
||||
desiredWidth := int(float64(desiredHeight) * aspectRatio)
|
||||
|
||||
// calculate the desired width based on the desired height and the aspect ratio
|
||||
desiredWidth := int(desiredHeight * aspectRatio)
|
||||
|
||||
// check which dimension to scale based on the maximum resolution
|
||||
if desiredHeight > maxHeight || desiredWidth > maxWidth {
|
||||
if desiredHeight-maxHeight > desiredWidth-maxWidth {
|
||||
// scale the height down to the maximum height
|
||||
return f.ScaleDimensions(-2, maxHeight)
|
||||
} else {
|
||||
// scale the width down to the maximum width
|
||||
return f.ScaleDimensions(maxWidth, -2)
|
||||
}
|
||||
if desiredHeight <= maxHeight && desiredWidth <= maxWidth {
|
||||
return f.ScaleMax(width, height, reqHeight)
|
||||
}
|
||||
|
||||
// the current resolution can be scaled to the desired height without exceeding the maximum resolution
|
||||
return f.ScaleMax(width, height, reqHeight)
|
||||
if float64(desiredHeight-maxHeight) > float64(desiredWidth-maxWidth) {
|
||||
return f.ScaleDimensions(-2, maxHeight)
|
||||
} else {
|
||||
return f.ScaleDimensions(maxWidth, -2)
|
||||
}
|
||||
}
|
||||
|
||||
// Fps returns a VideoFilter setting the frames per second.
|
||||
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
type StreamManager struct {
|
||||
cacheDir string
|
||||
encoder *FFMpeg
|
||||
ffprobe FFProbe
|
||||
ffprobe *FFProbe
|
||||
|
||||
config StreamManagerConfig
|
||||
lockManager *fsutil.ReadLockManager
|
||||
@@ -42,7 +42,7 @@ type StreamManagerConfig interface {
|
||||
GetTranscodeHardwareAcceleration() bool
|
||||
}
|
||||
|
||||
func NewStreamManager(cacheDir string, encoder *FFMpeg, ffprobe FFProbe, config StreamManagerConfig, lockManager *fsutil.ReadLockManager) *StreamManager {
|
||||
func NewStreamManager(cacheDir string, encoder *FFMpeg, ffprobe *FFProbe, config StreamManagerConfig, lockManager *fsutil.ReadLockManager) *StreamManager {
|
||||
if cacheDir == "" {
|
||||
logger.Warn("cache directory is not set. Live HLS/DASH transcoding will be disabled")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -45,6 +46,10 @@ const (
|
||||
// maximum idle time between segment requests before
|
||||
// stopping transcode and deleting cache folder
|
||||
maxIdleTime = 30 * time.Second
|
||||
|
||||
resolutionParamKey = "resolution"
|
||||
// TODO - setting the apikey in here isn't ideal
|
||||
apiKeyParamKey = "apikey"
|
||||
)
|
||||
|
||||
type StreamType struct {
|
||||
@@ -342,7 +347,7 @@ func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args {
|
||||
|
||||
videoOnly := ProbeAudioCodec(s.vf.AudioCodec) == MissingUnsupported
|
||||
|
||||
videoFilter := sm.encoder.hwMaxResFilter(codec, s.vf.Width, s.vf.Height, s.maxTranscodeSize, fullhw)
|
||||
videoFilter := sm.encoder.hwMaxResFilter(codec, s.vf, s.maxTranscodeSize, fullhw)
|
||||
|
||||
args = append(args, s.streamType.Args(codec, segment, videoFilter, videoOnly, s.outputDir)...)
|
||||
|
||||
@@ -425,9 +430,21 @@ func serveHLSManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request,
|
||||
baseUrl.RawQuery = ""
|
||||
baseURL := baseUrl.String()
|
||||
|
||||
var urlQuery string
|
||||
urlQuery := url.Values{}
|
||||
apikey := r.URL.Query().Get(apiKeyParamKey)
|
||||
|
||||
if resolution != "" {
|
||||
urlQuery = fmt.Sprintf("?resolution=%s", resolution)
|
||||
urlQuery.Set(resolutionParamKey, resolution)
|
||||
}
|
||||
|
||||
// TODO - this needs to be handled outside of this package
|
||||
if apikey != "" {
|
||||
urlQuery.Set(apiKeyParamKey, apikey)
|
||||
}
|
||||
|
||||
urlQueryString := ""
|
||||
if len(urlQuery) > 0 {
|
||||
urlQueryString = "?" + urlQuery.Encode()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -449,7 +466,7 @@ func serveHLSManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
fmt.Fprintf(&buf, "#EXTINF:%f,\n", thisLength)
|
||||
fmt.Fprintf(&buf, "%s/%d.ts%s\n", baseURL, segment, urlQuery)
|
||||
fmt.Fprintf(&buf, "%s/%d.ts%s\n", baseURL, segment, urlQueryString)
|
||||
|
||||
leftover -= thisLength
|
||||
segment++
|
||||
@@ -508,11 +525,18 @@ func serveDASHManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request
|
||||
videoWidth = vf.Width
|
||||
}
|
||||
|
||||
var urlQuery string
|
||||
urlQuery := url.Values{}
|
||||
|
||||
// TODO - this needs to be handled outside of this package
|
||||
apikey := r.URL.Query().Get(apiKeyParamKey)
|
||||
if apikey != "" {
|
||||
urlQuery.Set(apiKeyParamKey, apikey)
|
||||
}
|
||||
|
||||
maxTranscodeSize := sm.config.GetMaxStreamingTranscodeSize().GetMaxResolution()
|
||||
if resolution != "" {
|
||||
maxTranscodeSize = models.StreamingResolutionEnum(resolution).GetMaxResolution()
|
||||
urlQuery = fmt.Sprintf("?resolution=%s", resolution)
|
||||
urlQuery.Set(resolutionParamKey, resolution)
|
||||
}
|
||||
if maxTranscodeSize != 0 {
|
||||
videoSize := videoHeight
|
||||
@@ -527,6 +551,11 @@ func serveDASHManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
urlQueryString := ""
|
||||
if len(urlQuery) > 0 {
|
||||
urlQueryString = "?" + urlQuery.Encode()
|
||||
}
|
||||
|
||||
mediaDuration := mpd.Duration(time.Duration(probeResult.FileDuration * float64(time.Second)))
|
||||
m := mpd.NewMPD(mpd.DASH_PROFILE_LIVE, mediaDuration.String(), "PT4.0S")
|
||||
|
||||
@@ -536,12 +565,12 @@ func serveDASHManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request
|
||||
|
||||
video, _ := m.AddNewAdaptationSetVideo(MimeWebmVideo, "progressive", true, 1)
|
||||
|
||||
_, _ = video.SetNewSegmentTemplate(2, "init_v.webm"+urlQuery, "$Number$_v.webm"+urlQuery, 0, 1)
|
||||
_, _ = video.SetNewSegmentTemplate(2, "init_v.webm"+urlQueryString, "$Number$_v.webm"+urlQueryString, 0, 1)
|
||||
_, _ = video.AddNewRepresentationVideo(200000, "vp09.00.40.08", "0", framerate, int64(videoWidth), int64(videoHeight))
|
||||
|
||||
if ProbeAudioCodec(vf.AudioCodec) != MissingUnsupported {
|
||||
audio, _ := m.AddNewAdaptationSetAudio(MimeWebmAudio, true, 1, "und")
|
||||
_, _ = audio.SetNewSegmentTemplate(2, "init_a.webm"+urlQuery, "$Number$_a.webm"+urlQuery, 0, 1)
|
||||
_, _ = audio.SetNewSegmentTemplate(2, "init_a.webm"+urlQueryString, "$Number$_a.webm"+urlQueryString, 0, 1)
|
||||
_, _ = audio.AddNewRepresentationAudio(48000, 96000, "opus", "1")
|
||||
}
|
||||
|
||||
|
||||
@@ -45,12 +45,31 @@ func CodecInit(codec VideoCodec) (args Args) {
|
||||
"-rc", "vbr",
|
||||
"-cq", "15",
|
||||
)
|
||||
case VideoCodecI264:
|
||||
case VideoCodecN264H:
|
||||
args = append(args,
|
||||
"-profile", "p7",
|
||||
"-tune", "hq",
|
||||
"-profile", "high",
|
||||
"-rc", "vbr",
|
||||
"-rc-lookahead", "60",
|
||||
"-surfaces", "64",
|
||||
"-spatial-aq", "1",
|
||||
"-aq-strength", "15",
|
||||
"-cq", "15",
|
||||
"-coder", "cabac",
|
||||
"-b_ref_mode", "middle",
|
||||
)
|
||||
case VideoCodecI264, VideoCodecIVP9:
|
||||
args = append(args,
|
||||
"-global_quality", "20",
|
||||
"-preset", "faster",
|
||||
)
|
||||
case VideoCodecV264:
|
||||
case VideoCodecI264C:
|
||||
args = append(args,
|
||||
"-q", "20",
|
||||
"-preset", "faster",
|
||||
)
|
||||
case VideoCodecV264, VideoCodecVVP9:
|
||||
args = append(args,
|
||||
"-qp", "20",
|
||||
)
|
||||
@@ -60,22 +79,13 @@ func CodecInit(codec VideoCodec) (args Args) {
|
||||
)
|
||||
case VideoCodecM264:
|
||||
args = append(args,
|
||||
"-prio_speed", "1",
|
||||
"-realtime", "1",
|
||||
)
|
||||
case VideoCodecO264:
|
||||
args = append(args,
|
||||
"-preset", "superfast",
|
||||
"-crf", "25",
|
||||
)
|
||||
case VideoCodecIVP9:
|
||||
args = append(args,
|
||||
"-global_quality", "20",
|
||||
"-preset", "faster",
|
||||
)
|
||||
case VideoCodecVVP9:
|
||||
args = append(args,
|
||||
"-qp", "20",
|
||||
)
|
||||
}
|
||||
|
||||
return args
|
||||
@@ -198,7 +208,7 @@ func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args {
|
||||
|
||||
videoOnly := ProbeAudioCodec(o.VideoFile.AudioCodec) == MissingUnsupported
|
||||
|
||||
videoFilter := sm.encoder.hwMaxResFilter(codec, o.VideoFile.Width, o.VideoFile.Height, maxTranscodeSize, fullhw)
|
||||
videoFilter := sm.encoder.hwMaxResFilter(codec, o.VideoFile, maxTranscodeSize, fullhw)
|
||||
|
||||
args = append(args, o.StreamType.Args(codec, videoFilter, videoOnly)...)
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@ func (o *ScreenshotOptions) setDefaults() {
|
||||
}
|
||||
|
||||
type ScreenshotOutputType struct {
|
||||
codec ffmpeg.VideoCodec
|
||||
codec *ffmpeg.VideoCodec
|
||||
format ffmpeg.Format
|
||||
}
|
||||
|
||||
func (t ScreenshotOutputType) Args() []string {
|
||||
var ret []string
|
||||
if t.codec != "" {
|
||||
if t.codec != nil {
|
||||
ret = append(ret, t.codec.Args()...)
|
||||
}
|
||||
if t.format != "" {
|
||||
@@ -45,7 +45,7 @@ var (
|
||||
format: "image2",
|
||||
}
|
||||
ScreenshotOutputTypeBMP = ScreenshotOutputType{
|
||||
codec: ffmpeg.VideoCodecBMP,
|
||||
codec: &ffmpeg.VideoCodecBMP,
|
||||
format: "rawvideo",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ type SpliceOptions struct {
|
||||
OutputPath string
|
||||
Format ffmpeg.Format
|
||||
|
||||
VideoCodec ffmpeg.VideoCodec
|
||||
VideoCodec *ffmpeg.VideoCodec
|
||||
VideoArgs ffmpeg.Args
|
||||
|
||||
AudioCodec ffmpeg.AudioCodec
|
||||
@@ -45,11 +45,11 @@ func Splice(concatFile string, options SpliceOptions) ffmpeg.Args {
|
||||
args = args.Overwrite()
|
||||
|
||||
// if video codec is not provided, then use copy
|
||||
if options.VideoCodec == "" {
|
||||
options.VideoCodec = ffmpeg.VideoCodecCopy
|
||||
if options.VideoCodec == nil {
|
||||
options.VideoCodec = &ffmpeg.VideoCodecCopy
|
||||
}
|
||||
|
||||
args = args.VideoCodec(options.VideoCodec)
|
||||
args = args.VideoCodec(*options.VideoCodec)
|
||||
args = args.AppendArgs(options.VideoArgs)
|
||||
|
||||
// if audio codec is not provided, then use copy
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user