mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
261 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76648fee66 | ||
|
|
6d07ecf751 | ||
|
|
5283eb8ce3 | ||
|
|
32c48443b5 | ||
|
|
ad00bee393 | ||
|
|
a54996d8a2 | ||
|
|
b6db4c31ca | ||
|
|
f82e24762b | ||
|
|
35b74be585 | ||
|
|
7199d2b5ac | ||
|
|
4697271294 | ||
|
|
3e4515e62a | ||
|
|
58c58beb4a | ||
|
|
f05518860f | ||
|
|
9b567fa6f4 | ||
|
|
c92de09ece | ||
|
|
9765b6d50e | ||
|
|
c6c3754f02 | ||
|
|
76a5b2a06a | ||
|
|
93a2ee1277 | ||
|
|
be6431ac13 | ||
|
|
4dd8dd948e | ||
|
|
e253ba71f8 | ||
|
|
30fc2d1209 | ||
|
|
cef5b46f93 | ||
|
|
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 | ||
|
|
2a373a25ca | ||
|
|
e116775d60 | ||
|
|
b7f938531b | ||
|
|
c5bafeb15c | ||
|
|
205b24499b | ||
|
|
48035061ec | ||
|
|
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 | ||
|
|
e4b89064b1 | ||
|
|
efede32dd7 | ||
|
|
d1998cb5b0 | ||
|
|
60446af145 | ||
|
|
dbfa450ace | ||
|
|
4b8af18fab | ||
|
|
124ea609fe | ||
|
|
0a07194110 | ||
|
|
b232e58b06 | ||
|
|
b3f8839ef7 | ||
|
|
540e80c86b | ||
|
|
eec31723bd | ||
|
|
3b146588c6 | ||
|
|
2b699fcf95 | ||
|
|
d6158d70a9 | ||
|
|
cf45ac883e | ||
|
|
e4267a0d83 | ||
|
|
2ca53714a6 | ||
|
|
0ff0f9c8ec | ||
|
|
9c8bd853c5 | ||
|
|
bf0e0f2210 | ||
|
|
c314515b8f | ||
|
|
28b5fbfd4d | ||
|
|
3dd218e1ba | ||
|
|
eb67f7f4d6 | ||
|
|
98d210f7f9 | ||
|
|
4794a1d453 | ||
|
|
77ef16570b | ||
|
|
99d97804f4 | ||
|
|
89553864f5 | ||
|
|
865208844c | ||
|
|
062d566195 | ||
|
|
bfc60bb23f | ||
|
|
0fa71be697 | ||
|
|
5ba1ea8fbc | ||
|
|
4d3dc0aec8 | ||
|
|
b12269e477 | ||
|
|
e32593023e | ||
|
|
3e3e8b95e2 | ||
|
|
769540be55 | ||
|
|
1ffca39e1d | ||
|
|
dd84714a16 | ||
|
|
ad844a225c | ||
|
|
ca5febc65b | ||
|
|
c8aeb7966a | ||
|
|
1d565a7cbd | ||
|
|
408d6fc988 | ||
|
|
237a904ca4 | ||
|
|
12af7d6515 | ||
|
|
77ee620877 | ||
|
|
c5fef3977e | ||
|
|
29859fa4ad | ||
|
|
1cee1ccfe2 | ||
|
|
9cc26f7b75 | ||
|
|
c5abe28375 | ||
|
|
1b99a03847 | ||
|
|
22d14fd89e | ||
|
|
0bba8889b8 | ||
|
|
141f60f8fb | ||
|
|
560bdcd60d | ||
|
|
c43e7b4351 | ||
|
|
4c0d9d0a07 | ||
|
|
157b2e7bae | ||
|
|
ec6acab2f4 | ||
|
|
911da87264 | ||
|
|
f7b87379d4 | ||
|
|
ad60f0ebd6 | ||
|
|
c83635c7a8 | ||
|
|
034fd4407d | ||
|
|
7086109d78 | ||
|
|
a369613d42 | ||
|
|
62b8ffb2b6 | ||
|
|
213c2830d1 | ||
|
|
32770203ba | ||
|
|
8c454582c7 | ||
|
|
e5929389b4 | ||
|
|
fa172c2dfd | ||
|
|
9ceea952b6 | ||
|
|
49cd214c9d | ||
|
|
3d0a8f653a | ||
|
|
ae6d1a8109 | ||
|
|
7ac7963972 | ||
|
|
bf7cb78d6d | ||
|
|
95d0e5dd34 | ||
|
|
d995ce7ecb | ||
|
|
3521dc133e | ||
|
|
9f5b1c33f6 | ||
|
|
c5bc106c1a | ||
|
|
9735d0fad1 | ||
|
|
353d889fd5 | ||
|
|
c7b2314bb1 | ||
|
|
4614471ad9 |
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: stashapp
|
||||
# patreon: # Replace with a single Patreon username
|
||||
open_collective: stashapp
|
||||
# ko_fi: # Replace with a single Ko-fi username
|
||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
# liberapay: StashApp
|
||||
# issuehunt: # Replace with a single IssueHunt username
|
||||
# otechie: # Replace with a single Otechie username
|
||||
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
28
.github/workflows/build.yml
vendored
28
.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
|
||||
|
||||
@@ -92,20 +97,23 @@ jobs:
|
||||
docker exec -t build /bin/bash -c "make build-cc-linux-arm32v6"
|
||||
docker exec -t build /bin/bash -c "make build-cc-freebsd"
|
||||
|
||||
- name: Zip UI
|
||||
run: docker exec -t build /bin/bash -c "make zip-ui"
|
||||
|
||||
- name: Cleanup build container
|
||||
run: docker rm -f -v build
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
|
||||
sha1sum dist/Stash.app.zip dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
|
||||
sha1sum dist/Stash.app.zip dist/stash-* dist/stash-ui.zip | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
|
||||
echo "STASH_VERSION=$(git describe --tags --exclude latest_develop)" >> $GITHUB_ENV
|
||||
echo "RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Windows binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stash-win.exe
|
||||
path: dist/stash-win.exe
|
||||
@@ -113,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
|
||||
@@ -121,11 +129,19 @@ 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
|
||||
|
||||
- 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@v4
|
||||
with:
|
||||
name: stash-ui.zip
|
||||
path: dist/stash-ui.zip
|
||||
|
||||
- name: Update latest_develop tag
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
run : git tag -f latest_develop; git push -f --tags
|
||||
@@ -147,6 +163,7 @@ jobs:
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-linux-arm32v6
|
||||
dist/stash-freebsd
|
||||
dist/stash-ui.zip
|
||||
CHECKSUMS_SHA1
|
||||
|
||||
- name: Master release
|
||||
@@ -166,6 +183,7 @@ jobs:
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-linux-arm32v6
|
||||
dist/stash-freebsd
|
||||
dist/stash-ui.zip
|
||||
CHECKSUMS_SHA1
|
||||
gzip: false
|
||||
|
||||
|
||||
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
|
||||
|
||||
22
Makefile
22
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
|
||||
@@ -353,6 +354,11 @@ endif
|
||||
ui: ui-env
|
||||
cd ui/v2.5 && yarn build
|
||||
|
||||
.PHONY: zip-ui
|
||||
zip-ui:
|
||||
rm -f dist/stash-ui.zip
|
||||
cd ui/v2.5/build && zip -r ../../../dist/stash-ui.zip .
|
||||
|
||||
.PHONY: ui-start
|
||||
ui-start: ui-env
|
||||
cd ui/v2.5 && yarn start --host
|
||||
@@ -366,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
|
||||
|
||||
20
README.md
20
README.md
@@ -24,6 +24,11 @@ For further information you can consult the [documentation](https://docs.stashap
|
||||
|
||||
# Installing Stash
|
||||
|
||||
#### Windows Users:
|
||||
|
||||
As of version 0.27.0, Stash doesn't support anymore _Windows 7, 8, Server 2008 and Server 2012._
|
||||
Windows 10 or Server 2016 are at least required.
|
||||
|
||||
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS | <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
|
||||
:---:|:---:|:---:|:---:
|
||||
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release](https://github.com/stashapp/stash/releases/latest/download/Stash.app.zip) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/Stash.app.zip)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub>[Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
|
||||
@@ -48,17 +53,20 @@ Stash is a web-based application. Once the application is running, the interface
|
||||
|
||||
On first run, Stash will prompt you for some configuration options and media directories to index, called "Scanning" in Stash. After scanning, your media will be available for browsing, curating, editing, and tagging.
|
||||
|
||||
Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en/Scraping.md), which integrate directly into Stash.
|
||||
|
||||
Many community-maintained scrapers are available for download from [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers). The community also maintains StashDB, a crowd-sourced repository of scene, studio, and performer information, that can automatically identify much of a typical media collection. Inquire in the Discord for details. Identifying an entire collection will typically require a mix of multiple sources.
|
||||
Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/docs/en/Manual/Scraping.md), which integrate directly into Stash. Identifying an entire collection will typically require a mix of multiple sources:
|
||||
- The project maintains [StashDB](https://stashdb.org/), a crowd-sourced repository of scene, studio, and performer information. Connecting it to Stash will allow you to automatically identify much of a typical media collection. It runs on our stash-box software and is primarily focused on mainstream digital scenes and studios. Instructions, invite codes, and more can be found in this guide to [Accessing StashDB](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stashdb/).
|
||||
- Several community-managed stash-box databases can also be connected to Stash in a similar manner. Each one serves a slightly different niche and follows their own methodology. A rundown of each stash-box, their differences, and the information you need to sign up can be found in this guide to [Accessing Stash-Boxes](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stash-boxes/).
|
||||
- Many community-maintained scrapers can also be downloaded, installed, and updated from within Stash, allowing you to pull data from a wide range of other websites and databases. They can be found by navigating to Settings -> Metadata Providers -> Available Scrapers -> Community (stable). These can be trickier to use than a stash-box because every scraper works a little differently. For more information, please visit the [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers).
|
||||
- All of the above methods of scraping data into Stash are also covered in more detail in our [Guide to Scraping](https://docs.stashapp.cc/beginner-guides/guide-to-scraping/).
|
||||
|
||||
<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)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
@@ -17,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
|
||||
@@ -45,6 +46,13 @@ func printPhash(ff *ffmpeg.FFMpeg, ffp ffmpeg.FFProbe, inputfile string, quiet *
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPaths() (string, string) {
|
||||
ffmpegPath, _ := exec.LookPath("ffmpeg")
|
||||
ffprobePath, _ := exec.LookPath("ffprobe")
|
||||
|
||||
return ffmpegPath, ffprobePath
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = customUsage
|
||||
quiet := flag.BoolP("quiet", "q", false, "print only the phash")
|
||||
@@ -69,10 +77,10 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, "Example: parallel %v ::: *.mp4\n", os.Args[0])
|
||||
}
|
||||
|
||||
ffmpegPath, ffprobePath := ffmpeg.GetPaths(nil)
|
||||
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 {
|
||||
|
||||
@@ -37,6 +37,8 @@ func main() {
|
||||
|
||||
defer recoverPanic()
|
||||
|
||||
initLogTemp()
|
||||
|
||||
helpFlag := false
|
||||
pflag.BoolVarP(&helpFlag, "help", "h", false, "show this help text and exit")
|
||||
|
||||
@@ -104,6 +106,16 @@ func main() {
|
||||
exitCode = <-exit
|
||||
}
|
||||
|
||||
// initLogTemp initializes a temporary logger for use before the config is loaded.
|
||||
// Logs only error level message to stderr.
|
||||
func initLogTemp() *log.Logger {
|
||||
l := log.NewLogger()
|
||||
l.Init("", true, "Error")
|
||||
logger.Logger = l
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func initLog(cfg *config.Config) *log.Logger {
|
||||
l := log.NewLogger()
|
||||
l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel())
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -18,10 +18,18 @@ COPY --from=binary /stash /usr/bin/
|
||||
RUN apk add --no-cache --virtual .build-deps gcc python3-dev musl-dev \
|
||||
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg ruby tzdata \
|
||||
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.18/community vips=8.14.3-r0 vips-tools=8.14.3-r0 \
|
||||
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx \
|
||||
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx stashapp-tools \
|
||||
&& gem install faraday \
|
||||
&& apk del .build-deps
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
# Basic build-time metadata as defined at https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
|
||||
LABEL org.opencontainers.image.title="Stash" \
|
||||
org.opencontainers.image.description="An organizer for your porn, written in Go." \
|
||||
org.opencontainers.image.url="https://stashapp.cc" \
|
||||
org.opencontainers.image.documentation="https://docs.stashapp.cc" \
|
||||
org.opencontainers.image.source="https://github.com/stashapp/stash" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0"
|
||||
|
||||
EXPOSE 9999
|
||||
CMD ["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.
|
||||
|
||||
67
go.mod
67
go.mod
@@ -1,60 +1,62 @@
|
||||
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
|
||||
github.com/antchfx/htmlquery v1.3.0
|
||||
github.com/asticode/go-astisub v0.26.0
|
||||
github.com/asticode/go-astisub v0.25.1
|
||||
github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617
|
||||
github.com/chromedp/chromedp v0.9.2
|
||||
github.com/corona10/goimagehash v1.1.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/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/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
|
||||
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/spf13/viper v1.16.0
|
||||
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.17.0
|
||||
golang.org/x/image v0.12.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sys v0.15.0
|
||||
golang.org/x/term v0.15.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,26 +67,29 @@ 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
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
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
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
@@ -94,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/stretchr/objx v0.5.0 // indirect
|
||||
github.com/spf13/viper v1.16.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/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
252
go.sum
252
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=
|
||||
@@ -70,6 +75,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/anacrolix/dms v1.2.2 h1:0mk2/DXNqa5KDDbaLgFPf3oMV6VCGdFNh3d/gt4oafM=
|
||||
github.com/anacrolix/dms v1.2.2/go.mod h1:msPKAoppoNRfrYplJqx63FZ+VipDZ4Xsj3KzIQxyU7k=
|
||||
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||
@@ -80,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=
|
||||
@@ -94,15 +103,26 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/asticode/go-astikit v0.20.0 h1:+7N+J4E4lWx2QOkRdOf6DafWJMv6O4RRfgClwQokrH8=
|
||||
github.com/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astisub v0.26.0 h1:Ka1oUyWzo/lIx7RX97GI1QdbClqYVxI0ExKuZRN/cDk=
|
||||
github.com/asticode/go-astisub v0.26.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
|
||||
github.com/asticode/go-astisub v0.25.1 h1:RZMGfZPp7CXOkI6g+zCU7DRLuciGPGup921uKZnMXPI=
|
||||
github.com/asticode/go-astisub v0.25.1/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
|
||||
github.com/asticode/go-astits v1.8.0 h1:rf6aiiGn/QhlFjNON1n5plqF3Fs025XLUwiQ0NB6oZg=
|
||||
github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
|
||||
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
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=
|
||||
@@ -118,8 +138,11 @@ github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmt
|
||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
@@ -140,8 +163,10 @@ 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=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -150,10 +175,19 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
||||
github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY=
|
||||
github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
|
||||
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -168,7 +202,10 @@ github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
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/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
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=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
@@ -176,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=
|
||||
@@ -187,11 +224,18 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
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/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=
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
@@ -202,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=
|
||||
@@ -244,6 +288,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
@@ -260,7 +305,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
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=
|
||||
@@ -281,8 +328,12 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
||||
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=
|
||||
@@ -296,9 +347,11 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
|
||||
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
|
||||
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
|
||||
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
@@ -306,6 +359,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
||||
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
@@ -315,18 +370,22 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
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=
|
||||
@@ -336,21 +395,34 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
|
||||
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
|
||||
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
|
||||
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
|
||||
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
|
||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
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.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=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
@@ -358,6 +430,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kermieisinthehouse/gosx-notifier v0.1.2 h1:KV0KBeKK2B24kIHY7iK0jgS64Q05f4oB+hUZmsPodxQ=
|
||||
github.com/kermieisinthehouse/gosx-notifier v0.1.2/go.mod h1:xyWT07azFtUOcHl96qMVvKhvKzsMcS7rKTHQyv8WTho=
|
||||
github.com/kermieisinthehouse/systray v1.2.4 h1:pdH5vnl+KKjRrVCRU4g/2W1/0HVzuuJ6WXHlPPHYY6s=
|
||||
@@ -365,20 +438,27 @@ github.com/kermieisinthehouse/systray v1.2.4/go.mod h1:axh6C/jNuSyC0QGtidZJURc9h
|
||||
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
|
||||
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
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=
|
||||
@@ -388,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=
|
||||
@@ -404,26 +483,35 @@ 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=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -433,20 +521,26 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007 h1:Ohgj9L0EYOgXxkDp+bczlMBiulwmqYzQpvQNUdtt3oc=
|
||||
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007/go.mod h1:wKCOWMb6iNlvKiOToY2cNuaovSXvIiv1zDi9QDR7aGQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -463,22 +557,29 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
|
||||
github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
|
||||
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac h1:kYPjbEN6YPYWWHI6ky1J813KzIq/8+Wg4TO4xU7A/KU=
|
||||
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
|
||||
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
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=
|
||||
@@ -489,22 +590,30 @@ 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=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||
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=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
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=
|
||||
@@ -512,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=
|
||||
@@ -528,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=
|
||||
@@ -539,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=
|
||||
@@ -554,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=
|
||||
@@ -582,8 +693,11 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
||||
github.com/zencoder/go-dash/v3 v3.0.2 h1:oP1+dOh+Gp57PkvdCyMfbHtrHaxfl3w4kR3KBBbuqQE=
|
||||
github.com/zencoder/go-dash/v3 v3.0.2/go.mod h1:30R5bKy1aUYY45yesjtZ9l8trNc2TwNqbS17WVQmCzk=
|
||||
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
|
||||
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -614,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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
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=
|
||||
@@ -629,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=
|
||||
@@ -658,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=
|
||||
@@ -710,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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
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=
|
||||
@@ -742,16 +854,18 @@ 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=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -763,10 +877,12 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -781,6 +897,8 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -788,6 +906,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -800,6 +919,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -817,6 +937,7 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -824,31 +945,29 @@ 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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.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.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
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=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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=
|
||||
@@ -914,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=
|
||||
@@ -963,6 +1081,7 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@@ -1026,9 +1145,11 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6
|
||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
@@ -1069,10 +1190,12 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg=
|
||||
gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI=
|
||||
@@ -1080,14 +1203,14 @@ gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.3/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1104,3 +1227,4 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -226,7 +245,12 @@ type Query {
|
||||
|
||||
type Mutation {
|
||||
setup(input: SetupInput!): Boolean!
|
||||
migrate(input: MigrateInput!): Boolean!
|
||||
|
||||
"Migrates the schema to the required version. Returns the job ID"
|
||||
migrate(input: MigrateInput!): ID!
|
||||
|
||||
"Downloads and installs ffmpeg and ffprobe binaries into the configuration directory. Returns the job ID."
|
||||
downloadFFMpeg: ID!
|
||||
|
||||
sceneCreate(input: SceneCreateInput!): Scene
|
||||
sceneUpdate(input: SceneUpdateInput!): Scene
|
||||
@@ -252,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")
|
||||
@@ -293,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
|
||||
@@ -310,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.
|
||||
@@ -339,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!
|
||||
@@ -349,12 +401,19 @@ type Mutation {
|
||||
input: ConfigDefaultSettingsInput!
|
||||
): ConfigDefaultSettingsResult!
|
||||
|
||||
# overwrites the entire plugin configuration for the given plugin
|
||||
"overwrites the entire plugin configuration for the given plugin"
|
||||
configurePlugin(plugin_id: ID!, input: Map!): Map!
|
||||
|
||||
# overwrites the entire UI configuration
|
||||
configureUI(input: Map!): Map!
|
||||
# sets a single UI key value
|
||||
"""
|
||||
overwrites the UI configuration
|
||||
if input is provided, then the entire UI configuration is replaced
|
||||
if partial is provided, then the partial UI configuration is merged into the existing UI configuration
|
||||
"""
|
||||
configureUI(input: Map, partial: Map): Map!
|
||||
"""
|
||||
sets a single UI key value
|
||||
key is a dot separated path to the value
|
||||
"""
|
||||
configureUISetting(key: String!, value: Any): Map!
|
||||
|
||||
"Generate and set (or clear) API key"
|
||||
|
||||
@@ -81,6 +81,10 @@ input ConfigGeneralInput {
|
||||
blobsPath: String
|
||||
"Where to store blobs"
|
||||
blobsStorage: BlobsStorageType
|
||||
"Path to the ffmpeg binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffmpegPath: String
|
||||
"Path to the ffprobe binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffprobePath: String
|
||||
"Whether to calculate MD5 checksums for scene video files"
|
||||
calculateMD5: Boolean
|
||||
"Hash algorithm to use for generated file naming"
|
||||
@@ -199,6 +203,10 @@ type ConfigGeneralResult {
|
||||
blobsPath: String!
|
||||
"Where to store blobs"
|
||||
blobsStorage: BlobsStorageType!
|
||||
"Path to the ffmpeg binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffmpegPath: String!
|
||||
"Path to the ffprobe binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffprobePath: String!
|
||||
"Whether to calculate MD5 checksums for scene video files"
|
||||
calculateMD5: Boolean!
|
||||
"Hash algorithm to use for generated file naming"
|
||||
@@ -461,6 +469,8 @@ input ConfigDLNAInput {
|
||||
serverName: String
|
||||
"True if DLNA service should be enabled by default"
|
||||
enabled: Boolean
|
||||
"Defaults to 1338"
|
||||
port: Int
|
||||
"List of IPs whitelisted for DLNA service"
|
||||
whitelistedIPs: [String!]
|
||||
"List of interfaces to run DLNA on. Empty for all"
|
||||
@@ -473,6 +483,8 @@ type ConfigDLNAResult {
|
||||
serverName: String!
|
||||
"True if DLNA service should be enabled by default"
|
||||
enabled: Boolean!
|
||||
"Defaults to 1338"
|
||||
port: Int!
|
||||
"List of IPs whitelisted for DLNA service"
|
||||
whitelistedIPs: [String!]!
|
||||
"List of interfaces to run DLNA on. Empty for all"
|
||||
|
||||
@@ -8,6 +8,7 @@ input FindFilterType {
|
||||
page: Int
|
||||
"use per_page = -1 to indicate all results. Defaults to 25."
|
||||
per_page: Int
|
||||
# TODO - this should be refactored to not use a string
|
||||
sort: String
|
||||
direction: SortDirectionEnum
|
||||
}
|
||||
@@ -143,6 +144,8 @@ input PerformerFilterType {
|
||||
image_count: IntCriterionInput
|
||||
"Filter by gallery count"
|
||||
gallery_count: IntCriterionInput
|
||||
"Filter by play count"
|
||||
play_count: IntCriterionInput
|
||||
"Filter by o count"
|
||||
o_counter: IntCriterionInput
|
||||
"Filter by StashID"
|
||||
@@ -167,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"
|
||||
@@ -180,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"
|
||||
@@ -190,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 {
|
||||
@@ -229,6 +244,8 @@ input SceneFilterType {
|
||||
orientation: OrientationCriterionInput
|
||||
"Filter by frame rate"
|
||||
framerate: IntCriterionInput
|
||||
"Filter by bit rate"
|
||||
bitrate: IntCriterionInput
|
||||
"Filter by video codec"
|
||||
video_codec: StringCriterionInput
|
||||
"Filter by audio codec"
|
||||
@@ -242,7 +259,11 @@ 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"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter by tag count"
|
||||
@@ -273,15 +294,37 @@ input SceneFilterType {
|
||||
play_count: IntCriterionInput
|
||||
"Filter by play duration (in seconds)"
|
||||
play_duration: IntCriterionInput
|
||||
"Filter by scene last played time"
|
||||
last_played_at: TimestampCriterionInput
|
||||
"Filter by date"
|
||||
date: DateCriterionInput
|
||||
"Filter by creation time"
|
||||
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
|
||||
@@ -298,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 {
|
||||
@@ -317,16 +416,22 @@ 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
|
||||
rating100: IntCriterionInput
|
||||
"Filter by favorite"
|
||||
favorite: Boolean
|
||||
"Filter by scene count"
|
||||
scene_count: IntCriterionInput
|
||||
"Filter by image count"
|
||||
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"
|
||||
@@ -335,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"
|
||||
@@ -368,6 +479,8 @@ input GalleryFilterType {
|
||||
average_resolution: ResolutionCriterionInput
|
||||
"Filter to only include galleries that have chapters. `true` or `false`"
|
||||
has_chapters: String
|
||||
"Filter to only include galleries with these scenes"
|
||||
scenes: MultiCriterionInput
|
||||
"Filter to only include galleries with this studio"
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"Filter to only include galleries with these tags"
|
||||
@@ -398,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 {
|
||||
@@ -411,6 +535,9 @@ input TagFilterType {
|
||||
"Filter by tag aliases"
|
||||
aliases: StringCriterionInput
|
||||
|
||||
"Filter by favorite"
|
||||
favorite: Boolean
|
||||
|
||||
"Filter by tag description"
|
||||
description: StringCriterionInput
|
||||
|
||||
@@ -429,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
|
||||
|
||||
@@ -447,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
|
||||
|
||||
@@ -512,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 {
|
||||
@@ -607,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!]!
|
||||
}
|
||||
@@ -4,6 +4,7 @@ enum JobStatus {
|
||||
FINISHED
|
||||
STOPPING
|
||||
CANCELLED
|
||||
FAILED
|
||||
}
|
||||
|
||||
type Job {
|
||||
@@ -15,6 +16,7 @@ type Job {
|
||||
startTime: Time
|
||||
endTime: Time
|
||||
addTime: Time!
|
||||
error: String
|
||||
}
|
||||
|
||||
input FindJobInput {
|
||||
|
||||
@@ -75,6 +75,8 @@ input ScanMetaDataFilterInput {
|
||||
input ScanMetadataInput {
|
||||
paths: [String!]
|
||||
|
||||
"Forces a rescan on files even if modification time is unchanged"
|
||||
rescan: Boolean
|
||||
"Generate covers during scan"
|
||||
scanGenerateCovers: Boolean
|
||||
"Generate previews during scan"
|
||||
@@ -95,6 +97,8 @@ input ScanMetadataInput {
|
||||
}
|
||||
|
||||
type ScanMetadataOptions {
|
||||
"Forces a rescan on files even if modification time is unchanged"
|
||||
rescan: Boolean!
|
||||
"Generate covers during scan"
|
||||
scanGenerateCovers: Boolean!
|
||||
"Generate previews during scan"
|
||||
@@ -280,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
|
||||
}
|
||||
@@ -326,6 +331,8 @@ type SystemStatus {
|
||||
os: String!
|
||||
workingDir: String!
|
||||
homeDir: String!
|
||||
ffmpegPath: String
|
||||
ffprobePath: String
|
||||
}
|
||||
|
||||
input MigrateInput {
|
||||
|
||||
@@ -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,14 +13,17 @@ 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
|
||||
favorite: Boolean!
|
||||
details: String
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
movies: [Movie!]!
|
||||
groups: [Group!]!
|
||||
movies: [Movie!]! @deprecated(reason: "use groups instead")
|
||||
}
|
||||
|
||||
input StudioCreateInput {
|
||||
@@ -31,8 +35,10 @@ input StudioCreateInput {
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
favorite: Boolean
|
||||
details: String
|
||||
aliases: [String!]
|
||||
tag_ids: [ID!]
|
||||
ignore_auto_tag: Boolean
|
||||
}
|
||||
|
||||
@@ -46,8 +52,10 @@ input StudioUpdateInput {
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
favorite: Boolean
|
||||
details: String
|
||||
aliases: [String!]
|
||||
tag_ids: [ID!]
|
||||
ignore_auto_tag: Boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,16 @@ type Tag {
|
||||
ignore_auto_tag: Boolean!
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
|
||||
favorite: Boolean!
|
||||
image_path: String # Resolver
|
||||
scene_count(depth: Int): Int! # Resolver
|
||||
scene_marker_count(depth: Int): Int! # Resolver
|
||||
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!]!
|
||||
|
||||
@@ -25,7 +28,7 @@ input TagCreateInput {
|
||||
description: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
favorite: Boolean
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
|
||||
@@ -39,7 +42,7 @@ input TagUpdateInput {
|
||||
description: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
favorite: Boolean
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func authenticateHandler() func(http.Handler) http.Handler {
|
||||
|
||||
userID, err := manager.GetInstance().SessionStore.Authenticate(w, r)
|
||||
if err != nil {
|
||||
if errors.Is(err, session.ErrUnauthorized) {
|
||||
if !errors.Is(err, session.ErrUnauthorized) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,12 +6,16 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/internal/manager/task"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
var ErrOverriddenConfig = errors.New("cannot set overridden value")
|
||||
@@ -21,9 +25,60 @@ func (r *mutationResolver) Setup(ctx context.Context, input manager.SetupInput)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) Migrate(ctx context.Context, input manager.MigrateInput) (bool, error) {
|
||||
err := manager.GetInstance().Migrate(ctx, input)
|
||||
return err == nil, err
|
||||
func (r *mutationResolver) DownloadFFMpeg(ctx context.Context) (string, error) {
|
||||
mgr := manager.GetInstance()
|
||||
configDir := mgr.Config.GetConfigPathAbs()
|
||||
|
||||
// don't run if ffmpeg is already installed
|
||||
ffmpegPath := ffmpeg.FindFFMpeg(configDir)
|
||||
ffprobePath := ffmpeg.FindFFProbe(configDir)
|
||||
if ffmpegPath != "" && ffprobePath != "" {
|
||||
return "", fmt.Errorf("ffmpeg and ffprobe already installed at %s and %s", ffmpegPath, ffprobePath)
|
||||
}
|
||||
|
||||
t := &task.DownloadFFmpegJob{
|
||||
ConfigDirectory: configDir,
|
||||
OnComplete: func(ctx context.Context) {
|
||||
// clear the ffmpeg and ffprobe paths
|
||||
logger.Infof("Clearing ffmpeg and ffprobe config paths so they are resolved from the config directory")
|
||||
mgr.Config.SetString(config.FFMpegPath, "")
|
||||
mgr.Config.SetString(config.FFProbePath, "")
|
||||
mgr.RefreshFFMpeg(ctx)
|
||||
mgr.RefreshStreamManager()
|
||||
},
|
||||
}
|
||||
|
||||
jobID := mgr.JobManager.Add(ctx, "Downloading ffmpeg...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) setConfigString(key string, value *string) {
|
||||
c := config.GetInstance()
|
||||
if value != nil {
|
||||
c.SetString(key, *value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) setConfigBool(key string, value *bool) {
|
||||
c := config.GetInstance()
|
||||
if value != nil {
|
||||
c.SetBool(key, *value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) setConfigInt(key string, value *int) {
|
||||
c := config.GetInstance()
|
||||
if value != nil {
|
||||
c.SetInt(key, *value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) setConfigFloat(key string, value *float64) {
|
||||
c := config.GetInstance()
|
||||
if value != nil {
|
||||
c.SetFloat(key, *value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGeneralInput) (*ConfigGeneralResult, error) {
|
||||
@@ -47,7 +102,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Set(config.Stash, input.Stashes)
|
||||
c.SetInterface(config.Stash, input.Stashes)
|
||||
}
|
||||
|
||||
checkConfigOverride := func(key string) error {
|
||||
@@ -82,7 +137,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
if ext != ".db" && ext != ".sqlite" && ext != ".sqlite3" {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("invalid database path, use extension db, sqlite, or sqlite3")
|
||||
}
|
||||
c.Set(config.Database, input.DatabasePath)
|
||||
c.SetString(config.Database, *input.DatabasePath)
|
||||
}
|
||||
|
||||
existingBackupDirectoryPath := c.GetBackupDirectoryPath()
|
||||
@@ -91,7 +146,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.BackupDirectoryPath, input.BackupDirectoryPath)
|
||||
c.SetString(config.BackupDirectoryPath, *input.BackupDirectoryPath)
|
||||
}
|
||||
|
||||
existingGeneratedPath := c.GetGeneratedPath()
|
||||
@@ -100,7 +155,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.Generated, input.GeneratedPath)
|
||||
c.SetString(config.Generated, *input.GeneratedPath)
|
||||
}
|
||||
|
||||
refreshScraperCache := false
|
||||
@@ -113,7 +168,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
|
||||
refreshScraperCache = true
|
||||
refreshScraperSource = true
|
||||
c.Set(config.ScrapersPath, input.ScrapersPath)
|
||||
c.SetString(config.ScrapersPath, *input.ScrapersPath)
|
||||
}
|
||||
|
||||
refreshPluginCache := false
|
||||
@@ -126,7 +181,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
|
||||
refreshPluginCache = true
|
||||
refreshPluginSource = true
|
||||
c.Set(config.PluginsPath, input.PluginsPath)
|
||||
c.SetString(config.PluginsPath, *input.PluginsPath)
|
||||
}
|
||||
|
||||
existingMetadataPath := c.GetMetadataPath()
|
||||
@@ -135,7 +190,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.Metadata, input.MetadataPath)
|
||||
c.SetString(config.Metadata, *input.MetadataPath)
|
||||
}
|
||||
|
||||
refreshStreamManager := false
|
||||
@@ -145,7 +200,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.Cache, input.CachePath)
|
||||
c.SetString(config.Cache, *input.CachePath)
|
||||
refreshStreamManager = true
|
||||
}
|
||||
|
||||
@@ -156,7 +211,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.BlobsPath, input.BlobsPath)
|
||||
c.SetString(config.BlobsPath, *input.BlobsPath)
|
||||
refreshBlobStorage = true
|
||||
}
|
||||
|
||||
@@ -165,12 +220,34 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), fmt.Errorf("blobs path must be set when using filesystem storage")
|
||||
}
|
||||
|
||||
// TODO - migrate between systems
|
||||
c.Set(config.BlobsStorage, input.BlobsStorage)
|
||||
c.SetInterface(config.BlobsStorage, *input.BlobsStorage)
|
||||
|
||||
refreshBlobStorage = true
|
||||
}
|
||||
|
||||
refreshFfmpeg := false
|
||||
if input.FfmpegPath != nil && *input.FfmpegPath != c.GetFFMpegPath() {
|
||||
if *input.FfmpegPath != "" {
|
||||
if err := ffmpeg.ValidateFFMpeg(*input.FfmpegPath); err != nil {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("invalid ffmpeg path: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.SetString(config.FFMpegPath, *input.FfmpegPath)
|
||||
refreshFfmpeg = true
|
||||
}
|
||||
|
||||
if input.FfprobePath != nil && *input.FfprobePath != c.GetFFProbePath() {
|
||||
if *input.FfprobePath != "" {
|
||||
if err := ffmpeg.ValidateFFProbe(*input.FfprobePath); err != nil {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("invalid ffprobe path: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.SetString(config.FFProbePath, *input.FfprobePath)
|
||||
refreshFfmpeg = true
|
||||
}
|
||||
|
||||
if input.VideoFileNamingAlgorithm != nil && *input.VideoFileNamingAlgorithm != c.GetVideoFileNamingAlgorithm() {
|
||||
calculateMD5 := c.IsCalculateMD5()
|
||||
if input.CalculateMd5 != nil {
|
||||
@@ -187,68 +264,42 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.VideoFileNamingAlgorithm, *input.VideoFileNamingAlgorithm)
|
||||
c.SetInterface(config.VideoFileNamingAlgorithm, *input.VideoFileNamingAlgorithm)
|
||||
}
|
||||
|
||||
if input.CalculateMd5 != nil {
|
||||
c.Set(config.CalculateMD5, *input.CalculateMd5)
|
||||
}
|
||||
|
||||
if input.ParallelTasks != nil {
|
||||
c.Set(config.ParallelTasks, *input.ParallelTasks)
|
||||
}
|
||||
|
||||
if input.PreviewAudio != nil {
|
||||
c.Set(config.PreviewAudio, *input.PreviewAudio)
|
||||
}
|
||||
|
||||
if input.PreviewSegments != nil {
|
||||
c.Set(config.PreviewSegments, *input.PreviewSegments)
|
||||
}
|
||||
if input.PreviewSegmentDuration != nil {
|
||||
c.Set(config.PreviewSegmentDuration, *input.PreviewSegmentDuration)
|
||||
}
|
||||
if input.PreviewExcludeStart != nil {
|
||||
c.Set(config.PreviewExcludeStart, *input.PreviewExcludeStart)
|
||||
}
|
||||
if input.PreviewExcludeEnd != nil {
|
||||
c.Set(config.PreviewExcludeEnd, *input.PreviewExcludeEnd)
|
||||
}
|
||||
r.setConfigBool(config.CalculateMD5, input.CalculateMd5)
|
||||
r.setConfigInt(config.ParallelTasks, input.ParallelTasks)
|
||||
r.setConfigBool(config.PreviewAudio, input.PreviewAudio)
|
||||
r.setConfigInt(config.PreviewSegments, input.PreviewSegments)
|
||||
r.setConfigFloat(config.PreviewSegmentDuration, input.PreviewSegmentDuration)
|
||||
r.setConfigString(config.PreviewExcludeStart, input.PreviewExcludeStart)
|
||||
r.setConfigString(config.PreviewExcludeEnd, input.PreviewExcludeEnd)
|
||||
if input.PreviewPreset != nil {
|
||||
c.Set(config.PreviewPreset, input.PreviewPreset.String())
|
||||
c.SetString(config.PreviewPreset, input.PreviewPreset.String())
|
||||
}
|
||||
|
||||
if input.TranscodeHardwareAcceleration != nil {
|
||||
c.Set(config.TranscodeHardwareAcceleration, *input.TranscodeHardwareAcceleration)
|
||||
}
|
||||
r.setConfigBool(config.TranscodeHardwareAcceleration, input.TranscodeHardwareAcceleration)
|
||||
if input.MaxTranscodeSize != nil {
|
||||
c.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||
c.SetString(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||
}
|
||||
|
||||
if input.MaxStreamingTranscodeSize != nil {
|
||||
c.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
|
||||
}
|
||||
|
||||
if input.WriteImageThumbnails != nil {
|
||||
c.Set(config.WriteImageThumbnails, *input.WriteImageThumbnails)
|
||||
}
|
||||
|
||||
if input.CreateImageClipsFromVideos != nil {
|
||||
c.Set(config.CreateImageClipsFromVideos, *input.CreateImageClipsFromVideos)
|
||||
c.SetString(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
|
||||
}
|
||||
r.setConfigBool(config.WriteImageThumbnails, input.WriteImageThumbnails)
|
||||
r.setConfigBool(config.CreateImageClipsFromVideos, input.CreateImageClipsFromVideos)
|
||||
|
||||
if input.GalleryCoverRegex != nil {
|
||||
|
||||
_, err := regexp.Compile(*input.GalleryCoverRegex)
|
||||
if err != nil {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("Gallery cover regex '%v' invalid, '%v'", *input.GalleryCoverRegex, err.Error())
|
||||
}
|
||||
|
||||
c.Set(config.GalleryCoverRegex, *input.GalleryCoverRegex)
|
||||
c.SetString(config.GalleryCoverRegex, *input.GalleryCoverRegex)
|
||||
}
|
||||
|
||||
if input.Username != nil && *input.Username != c.GetUsername() {
|
||||
c.Set(config.Username, input.Username)
|
||||
c.SetString(config.Username, *input.Username)
|
||||
if *input.Password == "" {
|
||||
logger.Info("Username cleared")
|
||||
} else {
|
||||
@@ -271,24 +322,13 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
}
|
||||
}
|
||||
|
||||
if input.MaxSessionAge != nil {
|
||||
c.Set(config.MaxSessionAge, *input.MaxSessionAge)
|
||||
}
|
||||
|
||||
if input.LogFile != nil {
|
||||
c.Set(config.LogFile, input.LogFile)
|
||||
}
|
||||
|
||||
if input.LogOut != nil {
|
||||
c.Set(config.LogOut, *input.LogOut)
|
||||
}
|
||||
|
||||
if input.LogAccess != nil {
|
||||
c.Set(config.LogAccess, *input.LogAccess)
|
||||
}
|
||||
r.setConfigInt(config.MaxSessionAge, input.MaxSessionAge)
|
||||
r.setConfigString(config.LogFile, input.LogFile)
|
||||
r.setConfigBool(config.LogOut, input.LogOut)
|
||||
r.setConfigBool(config.LogAccess, input.LogAccess)
|
||||
|
||||
if input.LogLevel != nil && *input.LogLevel != c.GetLogLevel() {
|
||||
c.Set(config.LogLevel, input.LogLevel)
|
||||
c.SetString(config.LogLevel, *input.LogLevel)
|
||||
logger := manager.GetInstance().Logger
|
||||
logger.SetLogLevel(*input.LogLevel)
|
||||
}
|
||||
@@ -300,7 +340,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), fmt.Errorf("video exclusion pattern '%v' invalid: %w", r, err)
|
||||
}
|
||||
}
|
||||
c.Set(config.Exclude, input.Excludes)
|
||||
c.SetInterface(config.Exclude, input.Excludes)
|
||||
}
|
||||
|
||||
if input.ImageExcludes != nil {
|
||||
@@ -310,27 +350,25 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), fmt.Errorf("image/gallery exclusion pattern '%v' invalid: %w", r, err)
|
||||
}
|
||||
}
|
||||
c.Set(config.ImageExclude, input.ImageExcludes)
|
||||
c.SetInterface(config.ImageExclude, input.ImageExcludes)
|
||||
}
|
||||
|
||||
if input.VideoExtensions != nil {
|
||||
c.Set(config.VideoExtensions, input.VideoExtensions)
|
||||
c.SetInterface(config.VideoExtensions, input.VideoExtensions)
|
||||
}
|
||||
|
||||
if input.ImageExtensions != nil {
|
||||
c.Set(config.ImageExtensions, input.ImageExtensions)
|
||||
c.SetInterface(config.ImageExtensions, input.ImageExtensions)
|
||||
}
|
||||
|
||||
if input.GalleryExtensions != nil {
|
||||
c.Set(config.GalleryExtensions, input.GalleryExtensions)
|
||||
c.SetInterface(config.GalleryExtensions, input.GalleryExtensions)
|
||||
}
|
||||
|
||||
if input.CreateGalleriesFromFolders != nil {
|
||||
c.Set(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
|
||||
}
|
||||
r.setConfigBool(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
|
||||
|
||||
if input.CustomPerformerImageLocation != nil {
|
||||
c.Set(config.CustomPerformerImageLocation, *input.CustomPerformerImageLocation)
|
||||
c.SetString(config.CustomPerformerImageLocation, *input.CustomPerformerImageLocation)
|
||||
initCustomPerformerImages(*input.CustomPerformerImageLocation)
|
||||
}
|
||||
|
||||
@@ -338,37 +376,35 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
if err := c.ValidateStashBoxes(input.StashBoxes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Set(config.StashBoxes, input.StashBoxes)
|
||||
c.SetInterface(config.StashBoxes, input.StashBoxes)
|
||||
}
|
||||
|
||||
if input.PythonPath != nil {
|
||||
c.Set(config.PythonPath, input.PythonPath)
|
||||
r.setConfigString(config.PythonPath, input.PythonPath)
|
||||
}
|
||||
|
||||
if input.TranscodeInputArgs != nil {
|
||||
c.Set(config.TranscodeInputArgs, input.TranscodeInputArgs)
|
||||
c.SetInterface(config.TranscodeInputArgs, input.TranscodeInputArgs)
|
||||
}
|
||||
if input.TranscodeOutputArgs != nil {
|
||||
c.Set(config.TranscodeOutputArgs, input.TranscodeOutputArgs)
|
||||
c.SetInterface(config.TranscodeOutputArgs, input.TranscodeOutputArgs)
|
||||
}
|
||||
if input.LiveTranscodeInputArgs != nil {
|
||||
c.Set(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs)
|
||||
c.SetInterface(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs)
|
||||
}
|
||||
if input.LiveTranscodeOutputArgs != nil {
|
||||
c.Set(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs)
|
||||
c.SetInterface(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs)
|
||||
}
|
||||
|
||||
if input.DrawFunscriptHeatmapRange != nil {
|
||||
c.Set(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
|
||||
}
|
||||
r.setConfigBool(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
|
||||
|
||||
if input.ScraperPackageSources != nil {
|
||||
c.Set(config.ScraperPackageSources, input.ScraperPackageSources)
|
||||
c.SetInterface(config.ScraperPackageSources, input.ScraperPackageSources)
|
||||
refreshScraperSource = true
|
||||
}
|
||||
|
||||
if input.PluginPackageSources != nil {
|
||||
c.Set(config.PluginPackageSources, input.PluginPackageSources)
|
||||
c.SetInterface(config.PluginPackageSources, input.PluginPackageSources)
|
||||
refreshPluginSource = true
|
||||
}
|
||||
|
||||
@@ -383,6 +419,12 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
if refreshPluginCache {
|
||||
manager.GetInstance().RefreshPluginCache()
|
||||
}
|
||||
if refreshFfmpeg {
|
||||
manager.GetInstance().RefreshFFMpeg(ctx)
|
||||
|
||||
// refresh stream manager is required since ffmpeg changed
|
||||
refreshStreamManager = true
|
||||
}
|
||||
if refreshStreamManager {
|
||||
manager.GetInstance().RefreshStreamManager()
|
||||
}
|
||||
@@ -402,102 +444,70 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
setBool := func(key string, v *bool) {
|
||||
if v != nil {
|
||||
c.Set(key, *v)
|
||||
}
|
||||
}
|
||||
|
||||
setString := func(key string, v *string) {
|
||||
if v != nil {
|
||||
c.Set(key, *v)
|
||||
}
|
||||
}
|
||||
|
||||
if input.MenuItems != nil {
|
||||
c.Set(config.MenuItems, input.MenuItems)
|
||||
c.SetInterface(config.MenuItems, input.MenuItems)
|
||||
}
|
||||
|
||||
setBool(config.SoundOnPreview, input.SoundOnPreview)
|
||||
setBool(config.WallShowTitle, input.WallShowTitle)
|
||||
r.setConfigBool(config.SoundOnPreview, input.SoundOnPreview)
|
||||
r.setConfigBool(config.WallShowTitle, input.WallShowTitle)
|
||||
|
||||
setBool(config.NoBrowser, input.NoBrowser)
|
||||
r.setConfigBool(config.NoBrowser, input.NoBrowser)
|
||||
|
||||
setBool(config.NotificationsEnabled, input.NotificationsEnabled)
|
||||
r.setConfigBool(config.NotificationsEnabled, input.NotificationsEnabled)
|
||||
|
||||
setBool(config.ShowScrubber, input.ShowScrubber)
|
||||
r.setConfigBool(config.ShowScrubber, input.ShowScrubber)
|
||||
|
||||
if input.WallPlayback != nil {
|
||||
c.Set(config.WallPlayback, *input.WallPlayback)
|
||||
}
|
||||
r.setConfigString(config.WallPlayback, input.WallPlayback)
|
||||
r.setConfigInt(config.MaximumLoopDuration, input.MaximumLoopDuration)
|
||||
r.setConfigBool(config.AutostartVideo, input.AutostartVideo)
|
||||
r.setConfigBool(config.ShowStudioAsText, input.ShowStudioAsText)
|
||||
r.setConfigBool(config.AutostartVideoOnPlaySelected, input.AutostartVideoOnPlaySelected)
|
||||
r.setConfigBool(config.ContinuePlaylistDefault, input.ContinuePlaylistDefault)
|
||||
|
||||
if input.MaximumLoopDuration != nil {
|
||||
c.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
|
||||
}
|
||||
|
||||
setBool(config.AutostartVideo, input.AutostartVideo)
|
||||
setBool(config.ShowStudioAsText, input.ShowStudioAsText)
|
||||
setBool(config.AutostartVideoOnPlaySelected, input.AutostartVideoOnPlaySelected)
|
||||
setBool(config.ContinuePlaylistDefault, input.ContinuePlaylistDefault)
|
||||
|
||||
if input.Language != nil {
|
||||
c.Set(config.Language, *input.Language)
|
||||
}
|
||||
r.setConfigString(config.Language, input.Language)
|
||||
|
||||
if input.ImageLightbox != nil {
|
||||
options := input.ImageLightbox
|
||||
|
||||
if options.SlideshowDelay != nil {
|
||||
c.Set(config.ImageLightboxSlideshowDelay, *options.SlideshowDelay)
|
||||
}
|
||||
r.setConfigInt(config.ImageLightboxSlideshowDelay, options.SlideshowDelay)
|
||||
|
||||
setString(config.ImageLightboxDisplayModeKey, (*string)(options.DisplayMode))
|
||||
setBool(config.ImageLightboxScaleUp, options.ScaleUp)
|
||||
setBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
|
||||
setString(config.ImageLightboxScrollModeKey, (*string)(options.ScrollMode))
|
||||
r.setConfigString(config.ImageLightboxDisplayModeKey, (*string)(options.DisplayMode))
|
||||
r.setConfigBool(config.ImageLightboxScaleUp, options.ScaleUp)
|
||||
r.setConfigBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
|
||||
r.setConfigString(config.ImageLightboxScrollModeKey, (*string)(options.ScrollMode))
|
||||
|
||||
if options.ScrollAttemptsBeforeChange != nil {
|
||||
c.Set(config.ImageLightboxScrollAttemptsBeforeChange, *options.ScrollAttemptsBeforeChange)
|
||||
}
|
||||
r.setConfigInt(config.ImageLightboxScrollAttemptsBeforeChange, options.ScrollAttemptsBeforeChange)
|
||||
}
|
||||
|
||||
if input.CSS != nil {
|
||||
c.SetCSS(*input.CSS)
|
||||
}
|
||||
|
||||
setBool(config.CSSEnabled, input.CSSEnabled)
|
||||
r.setConfigBool(config.CSSEnabled, input.CSSEnabled)
|
||||
|
||||
if input.Javascript != nil {
|
||||
c.SetJavascript(*input.Javascript)
|
||||
}
|
||||
|
||||
setBool(config.JavascriptEnabled, input.JavascriptEnabled)
|
||||
r.setConfigBool(config.JavascriptEnabled, input.JavascriptEnabled)
|
||||
|
||||
if input.CustomLocales != nil {
|
||||
c.SetCustomLocales(*input.CustomLocales)
|
||||
}
|
||||
|
||||
setBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
|
||||
r.setConfigBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
|
||||
|
||||
if input.DisableDropdownCreate != nil {
|
||||
ddc := input.DisableDropdownCreate
|
||||
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
|
||||
setBool(config.DisableDropdownCreateStudio, ddc.Studio)
|
||||
setBool(config.DisableDropdownCreateTag, ddc.Tag)
|
||||
setBool(config.DisableDropdownCreateMovie, ddc.Movie)
|
||||
r.setConfigBool(config.DisableDropdownCreatePerformer, ddc.Performer)
|
||||
r.setConfigBool(config.DisableDropdownCreateStudio, ddc.Studio)
|
||||
r.setConfigBool(config.DisableDropdownCreateTag, ddc.Tag)
|
||||
r.setConfigBool(config.DisableDropdownCreateMovie, ddc.Movie)
|
||||
}
|
||||
|
||||
if input.HandyKey != nil {
|
||||
c.Set(config.HandyKey, *input.HandyKey)
|
||||
}
|
||||
|
||||
if input.FunscriptOffset != nil {
|
||||
c.Set(config.FunscriptOffset, *input.FunscriptOffset)
|
||||
}
|
||||
|
||||
if input.UseStashHostedFunscript != nil {
|
||||
c.Set(config.UseStashHostedFunscript, *input.UseStashHostedFunscript)
|
||||
}
|
||||
r.setConfigString(config.HandyKey, input.HandyKey)
|
||||
r.setConfigInt(config.FunscriptOffset, input.FunscriptOffset)
|
||||
r.setConfigBool(config.UseStashHostedFunscript, input.UseStashHostedFunscript)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigInterfaceResult(), err
|
||||
@@ -509,26 +519,23 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
|
||||
func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAInput) (*ConfigDLNAResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
if input.ServerName != nil {
|
||||
c.Set(config.DLNAServerName, *input.ServerName)
|
||||
}
|
||||
r.setConfigString(config.DLNAServerName, input.ServerName)
|
||||
|
||||
if input.WhitelistedIPs != nil {
|
||||
c.Set(config.DLNADefaultIPWhitelist, input.WhitelistedIPs)
|
||||
c.SetInterface(config.DLNADefaultIPWhitelist, input.WhitelistedIPs)
|
||||
}
|
||||
|
||||
if input.VideoSortOrder != nil {
|
||||
c.Set(config.DLNAVideoSortOrder, input.VideoSortOrder)
|
||||
}
|
||||
r.setConfigString(config.DLNAVideoSortOrder, input.VideoSortOrder)
|
||||
r.setConfigInt(config.DLNAPort, input.Port)
|
||||
|
||||
refresh := false
|
||||
if input.Enabled != nil {
|
||||
c.Set(config.DLNADefaultEnabled, *input.Enabled)
|
||||
c.SetBool(config.DLNADefaultEnabled, *input.Enabled)
|
||||
refresh = true
|
||||
}
|
||||
|
||||
if input.Interfaces != nil {
|
||||
c.Set(config.DLNAInterfaces, input.Interfaces)
|
||||
c.SetInterface(config.DLNAInterfaces, input.Interfaces)
|
||||
}
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
@@ -547,12 +554,12 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigSc
|
||||
|
||||
refreshScraperCache := false
|
||||
if input.ScraperUserAgent != nil {
|
||||
c.Set(config.ScraperUserAgent, input.ScraperUserAgent)
|
||||
c.SetString(config.ScraperUserAgent, *input.ScraperUserAgent)
|
||||
refreshScraperCache = true
|
||||
}
|
||||
|
||||
if input.ScraperCDPPath != nil {
|
||||
c.Set(config.ScraperCDPPath, input.ScraperCDPPath)
|
||||
c.SetString(config.ScraperCDPPath, *input.ScraperCDPPath)
|
||||
refreshScraperCache = true
|
||||
}
|
||||
|
||||
@@ -563,12 +570,10 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigSc
|
||||
return makeConfigScrapingResult(), fmt.Errorf("tag exclusion pattern '%v' invalid: %w", r, err)
|
||||
}
|
||||
}
|
||||
c.Set(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns)
|
||||
c.SetInterface(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns)
|
||||
}
|
||||
|
||||
if input.ScraperCertCheck != nil {
|
||||
c.Set(config.ScraperCertCheck, input.ScraperCertCheck)
|
||||
}
|
||||
r.setConfigBool(config.ScraperCertCheck, input.ScraperCertCheck)
|
||||
|
||||
if refreshScraperCache {
|
||||
manager.GetInstance().RefreshScraperCache()
|
||||
@@ -584,30 +589,25 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input ConfigDe
|
||||
c := config.GetInstance()
|
||||
|
||||
if input.Identify != nil {
|
||||
c.Set(config.DefaultIdentifySettings, input.Identify)
|
||||
c.SetInterface(config.DefaultIdentifySettings, input.Identify)
|
||||
}
|
||||
|
||||
if input.Scan != nil {
|
||||
// if input.Scan is used then ScanMetadataOptions is included in the config file
|
||||
// this causes the values to not be read correctly
|
||||
c.Set(config.DefaultScanSettings, input.Scan.ScanMetadataOptions)
|
||||
c.SetInterface(config.DefaultScanSettings, input.Scan.ScanMetadataOptions)
|
||||
}
|
||||
|
||||
if input.AutoTag != nil {
|
||||
c.Set(config.DefaultAutoTagSettings, input.AutoTag)
|
||||
c.SetInterface(config.DefaultAutoTagSettings, input.AutoTag)
|
||||
}
|
||||
|
||||
if input.Generate != nil {
|
||||
c.Set(config.DefaultGenerateSettings, input.Generate)
|
||||
c.SetInterface(config.DefaultGenerateSettings, input.Generate)
|
||||
}
|
||||
|
||||
if input.DeleteFile != nil {
|
||||
c.Set(config.DeleteFileDefault, *input.DeleteFile)
|
||||
}
|
||||
|
||||
if input.DeleteGenerated != nil {
|
||||
c.Set(config.DeleteGeneratedDefault, *input.DeleteGenerated)
|
||||
}
|
||||
r.setConfigBool(config.DeleteFileDefault, input.DeleteFile)
|
||||
r.setConfigBool(config.DeleteGeneratedDefault, input.DeleteGenerated)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigDefaultsResult(), err
|
||||
@@ -631,7 +631,7 @@ func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input GenerateAPI
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(config.ApiKey, newAPIKey)
|
||||
c.SetString(config.ApiKey, newAPIKey)
|
||||
if err := c.Write(); err != nil {
|
||||
return newAPIKey, err
|
||||
}
|
||||
@@ -639,9 +639,19 @@ func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input GenerateAPI
|
||||
return newAPIKey, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
|
||||
func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]interface{}, partial map[string]interface{}) (map[string]interface{}, error) {
|
||||
c := config.GetInstance()
|
||||
c.SetUIConfiguration(input)
|
||||
|
||||
if input != nil {
|
||||
c.SetUIConfiguration(input)
|
||||
}
|
||||
|
||||
if partial != nil {
|
||||
// merge partial into existing config
|
||||
existing := c.GetUIConfiguration()
|
||||
utils.MergeMaps(existing, partial)
|
||||
c.SetUIConfiguration(existing)
|
||||
}
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return c.GetUIConfiguration(), err
|
||||
@@ -653,10 +663,10 @@ func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]int
|
||||
func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, value interface{}) (map[string]interface{}, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
cfg := c.GetUIConfiguration()
|
||||
cfg[key] = value
|
||||
cfg := utils.NestedMap(c.GetUIConfiguration())
|
||||
cfg.Set(key, value)
|
||||
|
||||
return r.ConfigureUI(ctx, cfg)
|
||||
return r.ConfigureUI(ctx, cfg, nil)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigurePlugin(ctx context.Context, pluginID string, input map[string]interface{}) (map[string]interface{}, 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
|
||||
}
|
||||
@@ -38,3 +38,16 @@ func (r *mutationResolver) MigrateBlobs(ctx context.Context, input MigrateBlobsI
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) Migrate(ctx context.Context, input manager.MigrateInput) (string, error) {
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.MigrateJob{
|
||||
BackupPath: input.BackupPath,
|
||||
Config: mgr.Config,
|
||||
Database: mgr.Database,
|
||||
}
|
||||
|
||||
jobID := mgr.JobManager.Add(ctx, "Migrating database...", t)
|
||||
|
||||
return strconv.Itoa(jobID), 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
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (r *mutationResolver) SetPluginsEnabled(ctx context.Context, enabledMap map
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(config.DisabledPlugins, newDisabled)
|
||||
c.SetInterface(config.DisabledPlugins, newDisabled)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return false, 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
|
||||
})
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
newStudio.Name = input.Name
|
||||
newStudio.URL = translator.string(input.URL)
|
||||
newStudio.Rating = input.Rating100
|
||||
newStudio.Favorite = translator.bool(input.Favorite)
|
||||
newStudio.Details = translator.string(input.Details)
|
||||
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
newStudio.Aliases = models.NewRelatedStrings(input.Aliases)
|
||||
@@ -47,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 {
|
||||
@@ -103,6 +109,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
updatedStudio.URL = translator.optionalString(input.URL, "url")
|
||||
updatedStudio.Details = translator.optionalString(input.Details, "details")
|
||||
updatedStudio.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedStudio.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases")
|
||||
updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
|
||||
@@ -112,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,25 +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
|
||||
@@ -67,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
|
||||
}
|
||||
|
||||
@@ -84,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
|
||||
@@ -136,23 +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
|
||||
@@ -169,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
|
||||
@@ -199,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
|
||||
@@ -239,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 {
|
||||
@@ -329,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
|
||||
|
||||
@@ -91,6 +91,8 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
||||
CachePath: config.GetCachePath(),
|
||||
BlobsPath: config.GetBlobsPath(),
|
||||
BlobsStorage: config.GetBlobsStorage(),
|
||||
FfmpegPath: config.GetFFMpegPath(),
|
||||
FfprobePath: config.GetFFProbePath(),
|
||||
CalculateMd5: config.IsCalculateMD5(),
|
||||
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
ParallelTasks: config.GetParallelTasks(),
|
||||
@@ -197,6 +199,7 @@ func makeConfigDLNAResult() *ConfigDLNAResult {
|
||||
return &ConfigDLNAResult{
|
||||
ServerName: config.GetDLNAServerName(),
|
||||
Enabled: config.GetDLNADefaultEnabled(),
|
||||
Port: config.GetDLNAPort(),
|
||||
WhitelistedIPs: config.GetDLNADefaultIPWhitelist(),
|
||||
Interfaces: config.GetDLNAInterfaces(),
|
||||
VideoSortOrder: config.GetVideoSortOrder(),
|
||||
|
||||
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
|
||||
}
|
||||
@@ -106,6 +106,10 @@ func (r *queryResolver) FindImages(
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &FindImagesResultType{
|
||||
Count: result.Count,
|
||||
Images: images,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ func jobToJobModel(j job.Job) *Job {
|
||||
StartTime: j.StartTime,
|
||||
EndTime: j.EndTime,
|
||||
AddTime: j.AddTime,
|
||||
Error: j.Error,
|
||||
}
|
||||
|
||||
if j.Progress != -1 {
|
||||
|
||||
@@ -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"
|
||||
@@ -54,9 +53,8 @@ func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string,
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// filterSceneTags removes tags matching excluded tag patterns from the provided scraped scenes
|
||||
func filterSceneTags(scenes []*scraper.ScrapedScene) {
|
||||
excludePatterns := manager.GetInstance().Config.GetScraperExcludeTagPatterns()
|
||||
func compileRegexps(patterns []string) []*regexp.Regexp {
|
||||
excludePatterns := patterns
|
||||
var excludeRegexps []*regexp.Regexp
|
||||
|
||||
for _, excludePattern := range excludePatterns {
|
||||
@@ -68,30 +66,94 @@ func filterSceneTags(scenes []*scraper.ScrapedScene) {
|
||||
}
|
||||
}
|
||||
|
||||
return excludeRegexps
|
||||
}
|
||||
|
||||
// filterSceneTags removes tags matching excluded tag patterns from the provided scraped scenes
|
||||
func filterTags(excludeRegexps []*regexp.Regexp, tags []*models.ScrapedTag) (newTags []*models.ScrapedTag, ignoredTags []string) {
|
||||
if len(excludeRegexps) == 0 {
|
||||
return
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
ignore := false
|
||||
for _, reg := range excludeRegexps {
|
||||
if reg.MatchString(strings.ToLower(t.Name)) {
|
||||
ignore = true
|
||||
ignoredTags = sliceutil.AppendUnique(ignoredTags, t.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !ignore {
|
||||
newTags = append(newTags, t)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// filterSceneTags removes tags matching excluded tag patterns from the provided scraped scenes
|
||||
func filterSceneTags(scenes []*scraper.ScrapedScene) {
|
||||
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())
|
||||
|
||||
var ignoredTags []string
|
||||
|
||||
for _, s := range scenes {
|
||||
var newTags []*models.ScrapedTag
|
||||
for _, t := range s.Tags {
|
||||
ignore := false
|
||||
for _, reg := range excludeRegexps {
|
||||
if reg.MatchString(strings.ToLower(t.Name)) {
|
||||
ignore = true
|
||||
ignoredTags = sliceutil.AppendUnique(ignoredTags, t.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
var ignored []string
|
||||
s.Tags, ignored = filterTags(excludeRegexps, s.Tags)
|
||||
ignoredTags = sliceutil.AppendUniques(ignoredTags, ignored)
|
||||
}
|
||||
|
||||
if !ignore {
|
||||
newTags = append(newTags, t)
|
||||
}
|
||||
}
|
||||
if len(ignoredTags) > 0 {
|
||||
logger.Debugf("Scraping ignored tags: %s", strings.Join(ignoredTags, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
s.Tags = newTags
|
||||
// filterGalleryTags removes tags matching excluded tag patterns from the provided scraped galleries
|
||||
func filterGalleryTags(g []*scraper.ScrapedGallery) {
|
||||
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())
|
||||
|
||||
var ignoredTags []string
|
||||
|
||||
for _, s := range g {
|
||||
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, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// filterGalleryTags removes tags matching excluded tag patterns from the provided scraped galleries
|
||||
func filterPerformerTags(p []*models.ScrapedPerformer) {
|
||||
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, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -123,7 +185,16 @@ func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*scra
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return marshalScrapedGallery(content)
|
||||
ret, err := marshalScrapedGallery(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ret != nil {
|
||||
filterGalleryTags([]*scraper.ScrapedGallery{ret})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models.ScrapedMovie, error) {
|
||||
@@ -132,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
|
||||
@@ -190,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)
|
||||
@@ -220,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
|
||||
@@ -238,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)
|
||||
|
||||
@@ -264,39 +369,47 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scraper.Source, input ScrapeSinglePerformerInput) ([]*models.ScrapedPerformer, error) {
|
||||
if source.ScraperID != nil {
|
||||
if input.PerformerInput != nil {
|
||||
var ret []*models.ScrapedPerformer
|
||||
switch {
|
||||
case source.ScraperID != nil:
|
||||
switch {
|
||||
case input.PerformerInput != nil:
|
||||
performer, err := r.scraperCache().ScrapeFragment(ctx, *source.ScraperID, scraper.Input{Performer: input.PerformerInput})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return marshalScrapedPerformers([]scraper.ScrapedContent{performer})
|
||||
}
|
||||
|
||||
if input.Query != nil {
|
||||
ret, err = marshalScrapedPerformers([]scraper.ScrapedContent{performer})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case input.Query != nil:
|
||||
content, err := r.scraperCache().ScrapeName(ctx, *source.ScraperID, *input.Query, scraper.ScrapeContentTypePerformer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return marshalScrapedPerformers(content)
|
||||
ret, err = marshalScrapedPerformers(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
return nil, ErrNotImplemented
|
||||
// FIXME - we're relying on a deprecated field and not processing the endpoint input
|
||||
} else if source.StashBoxIndex != nil {
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
case source.StashBoxIndex != nil || source.StashBoxEndpoint != nil:
|
||||
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []*stashbox.StashBoxPerformerQueryResult
|
||||
client := r.newStashBoxClient(*b)
|
||||
|
||||
var res []*stashbox.StashBoxPerformerQueryResult
|
||||
switch {
|
||||
case input.PerformerID != nil:
|
||||
ret, err = client.FindStashBoxPerformersByNames(ctx, []string{*input.PerformerID})
|
||||
res, err = client.FindStashBoxPerformersByNames(ctx, []string{*input.PerformerID})
|
||||
case input.Query != nil:
|
||||
ret, err = client.QueryStashBoxPerformer(ctx, *input.Query)
|
||||
res, err = client.QueryStashBoxPerformer(ctx, *input.Query)
|
||||
default:
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
@@ -305,25 +418,29 @@ func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scrape
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ret) > 0 {
|
||||
return ret[0].Results, nil
|
||||
if len(res) > 0 {
|
||||
ret = res[0].Results
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, errors.New("scraper_id or stash_box_index must be set")
|
||||
}
|
||||
|
||||
return nil, errors.New("scraper_id or stash_box_index must be set")
|
||||
filterPerformerTags(ret)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -331,7 +448,9 @@ func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source scrape
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.Source, input ScrapeSingleGalleryInput) ([]*scraper.ScrapedGallery, error) {
|
||||
if source.StashBoxIndex != nil {
|
||||
var ret []*scraper.ScrapedGallery
|
||||
|
||||
if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
@@ -351,18 +470,31 @@ func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marshalScrapedGalleries([]scraper.ScrapedContent{c})
|
||||
ret, err = marshalScrapedGalleries([]scraper.ScrapedContent{c})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case input.GalleryInput != nil:
|
||||
c, err := r.scraperCache().ScrapeFragment(ctx, *source.ScraperID, scraper.Input{Gallery: input.GalleryInput})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return marshalScrapedGalleries([]scraper.ScrapedContent{c})
|
||||
ret, err = marshalScrapedGalleries([]scraper.ScrapedContent{c})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
filterGalleryTags(ret)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -53,7 +53,30 @@ type Server struct {
|
||||
manager *manager.Manager
|
||||
}
|
||||
|
||||
// Called at startup
|
||||
// TODO - os.DirFS doesn't implement ReadDir, so re-implement it here
|
||||
// This can be removed when we upgrade go
|
||||
type osFS string
|
||||
|
||||
func (dir osFS) ReadDir(name string) ([]os.DirEntry, error) {
|
||||
fullname := string(dir) + "/" + name
|
||||
entries, err := os.ReadDir(fullname)
|
||||
if err != nil {
|
||||
var e *os.PathError
|
||||
if errors.As(err, &e) {
|
||||
// See comment in dirFS.Open.
|
||||
e.Path = name
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (dir osFS) Open(name string) (fs.File, error) {
|
||||
return os.DirFS(string(dir)).Open(name)
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -135,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,
|
||||
}
|
||||
|
||||
@@ -185,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())
|
||||
@@ -213,25 +239,31 @@ func Initialize() (*Server, error) {
|
||||
r.Mount("/custom", getCustomRoutes(customServedFolders))
|
||||
}
|
||||
|
||||
customUILocation := cfg.GetCustomUILocation()
|
||||
staticUI := statigz.FileServer(ui.UIBox.(fs.ReadDirFS))
|
||||
var uiFS fs.FS
|
||||
var staticUI *statigz.Server
|
||||
customUILocation := cfg.GetUILocation()
|
||||
if customUILocation != "" {
|
||||
logger.Debugf("Serving UI from %s", customUILocation)
|
||||
uiFS = osFS(customUILocation)
|
||||
staticUI = statigz.FileServer(uiFS.(fs.ReadDirFS))
|
||||
} else {
|
||||
logger.Debug("Serving embedded UI")
|
||||
uiFS = ui.UIBox
|
||||
staticUI = statigz.FileServer(ui.UIBox.(fs.ReadDirFS))
|
||||
}
|
||||
|
||||
// Serve the web app
|
||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
ext := path.Ext(r.URL.Path)
|
||||
|
||||
if customUILocation != "" {
|
||||
if r.URL.Path == "index.html" || ext == "" {
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
|
||||
http.FileServer(http.Dir(customUILocation)).ServeHTTP(w, r)
|
||||
return
|
||||
if ext == ".html" || ext == "" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
setPageSecurityHeaders(w, r, pluginCache.ListPlugins())
|
||||
}
|
||||
|
||||
if ext == ".html" || ext == "" {
|
||||
if ext == "" || r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
||||
themeColor := cfg.GetThemeColor()
|
||||
data, err := fs.ReadFile(ui.UIBox, "index.html")
|
||||
data, err := fs.ReadFile(uiFS, "index.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -241,9 +273,6 @@ func Initialize() (*Server, error) {
|
||||
indexHtml = strings.ReplaceAll(indexHtml, "%COLOR%", themeColor)
|
||||
indexHtml = strings.Replace(indexHtml, `<base href="/"`, fmt.Sprintf(`<base href="%s/"`, prefix), 1)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
setPageSecurityHeaders(w, r, pluginCache.ListPlugins())
|
||||
|
||||
utils.ServeStaticContent(w, r, []byte(indexHtml))
|
||||
} else {
|
||||
isStatic, _ := path.Match("/assets/*", r.URL.Path)
|
||||
@@ -263,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)
|
||||
@@ -274,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 {
|
||||
@@ -301,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{
|
||||
@@ -318,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 (
|
||||
|
||||
@@ -21,7 +21,7 @@ func startSystray(exit chan int, faviconProvider FaviconProvider) {
|
||||
c := config.GetInstance()
|
||||
if c.GetShowOneTimeMovedNotification() {
|
||||
SendNotification("Stash has moved!", "Stash now runs in your tray, instead of a terminal window.")
|
||||
c.Set(config.ShowOneTimeMovedNotification, false)
|
||||
c.SetBool(config.ShowOneTimeMovedNotification, false)
|
||||
if err := c.Write(); err != nil {
|
||||
logger.Errorf("Error while writing configuration file: %v", err)
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ func sceneToContainer(scene *models.Scene, parent string, host string) interface
|
||||
Path: iconPath,
|
||||
RawQuery: url.Values{
|
||||
"scene": {strconv.Itoa(scene.ID)},
|
||||
"c": {"jpeg"},
|
||||
}.Encode(),
|
||||
}).String()
|
||||
|
||||
@@ -193,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 {
|
||||
@@ -317,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
|
||||
@@ -434,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
|
||||
@@ -659,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
|
||||
@@ -681,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)
|
||||
_, _ = fmt.Sscanf(r.Header.Get("TIMEOUT"), "Second-%d", &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")
|
||||
@@ -595,6 +613,8 @@ func (me *Server) initMux(mux *http.ServeMux) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("transferMode.dlna.org", "Streaming")
|
||||
w.Header().Set("contentFeatures.dlna.org", "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000")
|
||||
me.sceneServer.StreamSceneDirect(scene, w, r)
|
||||
})
|
||||
mux.HandleFunc(rootDescPath, func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ type Config interface {
|
||||
GetDLNAServerName() string
|
||||
GetDLNADefaultIPWhitelist() []string
|
||||
GetVideoSortOrder() string
|
||||
GetDLNAPortAsString() string
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
@@ -138,7 +139,7 @@ func (s *Service) init() error {
|
||||
var dmsConfig = &dmsConfig{
|
||||
Path: "",
|
||||
IfNames: s.config.GetDLNADefaultIPWhitelist(),
|
||||
Http: ":1338",
|
||||
Http: s.config.GetDLNAPortAsString(),
|
||||
FriendlyName: friendlyName,
|
||||
LogHeaders: false,
|
||||
NotifyInterval: 30 * time.Second,
|
||||
@@ -241,7 +242,7 @@ func (s *Service) Start(duration *time.Duration) error {
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Info("Starting DLNA")
|
||||
logger.Info("Starting DLNA " + s.server.HTTPConn.Addr().String())
|
||||
if err := s.server.Serve(); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
@@ -252,7 +255,8 @@ func (t *SceneIdentifier) getSceneUpdater(ctx context.Context, s *models.Scene,
|
||||
}
|
||||
}
|
||||
|
||||
if utils.IsTrue(options.SetCoverImage) {
|
||||
// SetCoverImage defaults to true if unset
|
||||
if options.SetCoverImage == nil || *options.SetCoverImage {
|
||||
ret.CoverImage, err = rel.cover(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -46,17 +46,17 @@ func createMissingStudio(ctx context.Context, endpoint string, w models.StudioRe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
studioPartial := s.Parent.ToPartial(s.Parent.StoredID, endpoint, nil, existingStashIDs)
|
||||
studioPartial := s.Parent.ToPartial(*s.Parent.StoredID, endpoint, nil, existingStashIDs)
|
||||
parentImage, err := s.Parent.GetImage(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := studio.ValidateModify(ctx, *studioPartial, w); err != nil {
|
||||
if err := studio.ValidateModify(ctx, studioPartial, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = w.UpdatePartial(ctx, *studioPartial)
|
||||
_, err = w.UpdatePartial(ctx, studioPartial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package log provides an implementation of [logger.LoggerImpl], using logrus.
|
||||
package log
|
||||
|
||||
import (
|
||||
|
||||
@@ -2,10 +2,13 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sync"
|
||||
@@ -13,7 +16,9 @@ import (
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
|
||||
"github.com/stashapp/stash/internal/identify"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
@@ -38,6 +43,9 @@ const (
|
||||
Password = "password"
|
||||
MaxSessionAge = "max_session_age"
|
||||
|
||||
FFMpegPath = "ffmpeg_path"
|
||||
FFProbePath = "ffprobe_path"
|
||||
|
||||
BlobsStorage = "blobs_storage"
|
||||
|
||||
DefaultMaxSessionAge = 60 * 60 * 1 // 1 hours
|
||||
@@ -156,7 +164,10 @@ const (
|
||||
|
||||
// UI directory. Overrides to serve the UI from a specific location
|
||||
// rather than use the embedded UI.
|
||||
CustomUILocation = "custom_ui_location"
|
||||
UILocation = "ui_location"
|
||||
|
||||
// backwards compatible name
|
||||
LegacyCustomUILocation = "custom_ui_location"
|
||||
|
||||
// Gallery Cover Regex
|
||||
GalleryCoverRegex = "gallery_cover_regex"
|
||||
@@ -177,9 +188,9 @@ const (
|
||||
autostartVideoOnPlaySelectedDefault = true
|
||||
ContinuePlaylistDefault = "continue_playlist_default"
|
||||
ShowStudioAsText = "show_studio_as_text"
|
||||
CSSEnabled = "cssEnabled"
|
||||
JavascriptEnabled = "javascriptEnabled"
|
||||
CustomLocalesEnabled = "customLocalesEnabled"
|
||||
CSSEnabled = "cssenabled"
|
||||
JavascriptEnabled = "javascriptenabled"
|
||||
CustomLocalesEnabled = "customlocalesenabled"
|
||||
|
||||
ShowScrubber = "show_scrubber"
|
||||
showScrubberDefault = true
|
||||
@@ -222,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"
|
||||
@@ -231,13 +245,16 @@ const (
|
||||
DLNAVideoSortOrder = "dlna.video_sort_order"
|
||||
dlnaVideoSortOrderDefault = "title"
|
||||
|
||||
DLNAPort = "dlna.port"
|
||||
DLNAPortDefault = 1338
|
||||
|
||||
// Logging options
|
||||
LogFile = "logFile"
|
||||
LogOut = "logOut"
|
||||
LogFile = "logfile"
|
||||
LogOut = "logout"
|
||||
defaultLogOut = true
|
||||
LogLevel = "logLevel"
|
||||
LogLevel = "loglevel"
|
||||
defaultLogLevel = "Info"
|
||||
LogAccess = "logAccess"
|
||||
LogAccess = "logaccess"
|
||||
defaultLogAccess = true
|
||||
|
||||
// Default settings
|
||||
@@ -251,7 +268,7 @@ const (
|
||||
deleteGeneratedDefaultDefault = true
|
||||
|
||||
// Desktop Integration Options
|
||||
NoBrowser = "noBrowser"
|
||||
NoBrowser = "nobrowser"
|
||||
NoBrowserDefault = false
|
||||
NotificationsEnabled = "notifications_enabled"
|
||||
NotificationsEnabledDefault = true
|
||||
@@ -293,12 +310,13 @@ func (s *StashBoxError) Error() string {
|
||||
|
||||
type Config struct {
|
||||
// main instance - backed by config file
|
||||
main *viper.Viper
|
||||
main *koanf.Koanf
|
||||
|
||||
// override instance - populated from flags/environment
|
||||
// not written to config file
|
||||
overrides *viper.Viper
|
||||
overrides *koanf.Koanf
|
||||
|
||||
filePath string
|
||||
isNewSystem bool
|
||||
// configUpdates chan int
|
||||
certFile string
|
||||
@@ -316,6 +334,15 @@ func GetInstance() *Config {
|
||||
return instance
|
||||
}
|
||||
|
||||
func (i *Config) load(f string) error {
|
||||
if err := i.main.Load(file.Provider(f), yaml.Parser()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.filePath = f
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Config) IsNewSystem() bool {
|
||||
return i.isNewSystem
|
||||
}
|
||||
@@ -323,7 +350,7 @@ func (i *Config) IsNewSystem() bool {
|
||||
func (i *Config) SetConfigFile(fn string) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
i.main.SetConfigFile(fn)
|
||||
i.filePath = fn
|
||||
}
|
||||
|
||||
func (i *Config) InitTLS() {
|
||||
@@ -333,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) {
|
||||
@@ -354,10 +390,6 @@ func (i *Config) GetNotificationsEnabled() bool {
|
||||
return i.getBool(NotificationsEnabled)
|
||||
}
|
||||
|
||||
// func (i *Instance) GetConfigUpdatesChannel() chan int {
|
||||
// return i.configUpdates
|
||||
// }
|
||||
|
||||
// GetShowOneTimeMovedNotification shows whether a small notification to inform the user that Stash
|
||||
// will no longer show a terminal window, and instead will be available in the tray, should be shown.
|
||||
// It is true when an existing system is started after upgrading, and set to false forever after it is shown.
|
||||
@@ -365,34 +397,93 @@ func (i *Config) GetShowOneTimeMovedNotification() bool {
|
||||
return i.getBool(ShowOneTimeMovedNotification)
|
||||
}
|
||||
|
||||
func (i *Config) Set(key string, value interface{}) {
|
||||
// if key == MenuItems {
|
||||
// i.configUpdates <- 0
|
||||
// }
|
||||
// these methods are intended to ensure type safety (ie no primitive pointers)
|
||||
func (i *Config) SetBool(key string, value bool) {
|
||||
i.SetInterface(key, value)
|
||||
}
|
||||
|
||||
func (i *Config) SetString(key string, value string) {
|
||||
i.SetInterface(key, value)
|
||||
}
|
||||
|
||||
func (i *Config) SetInt(key string, value int) {
|
||||
i.SetInterface(key, value)
|
||||
}
|
||||
|
||||
func (i *Config) SetFloat(key string, value float64) {
|
||||
i.SetInterface(key, value)
|
||||
}
|
||||
|
||||
func (i *Config) SetInterface(key string, value interface{}) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
i.main.Set(key, value)
|
||||
|
||||
i.set(key, value)
|
||||
}
|
||||
|
||||
func (i *Config) set(key string, value interface{}) {
|
||||
// assumes lock held
|
||||
|
||||
// default behaviour for Set is to merge the value
|
||||
// we want to replace it
|
||||
i.main.Delete(key)
|
||||
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// test for nil interface as well
|
||||
refVal := reflect.ValueOf(value)
|
||||
if refVal.Kind() == reflect.Ptr && refVal.IsNil() {
|
||||
return
|
||||
}
|
||||
|
||||
_ = i.main.Set(key, value)
|
||||
}
|
||||
|
||||
func (i *Config) SetDefault(key string, value interface{}) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
i.main.SetDefault(key, value)
|
||||
|
||||
i.setDefault(key, value)
|
||||
}
|
||||
|
||||
func (i *Config) setDefault(key string, value interface{}) {
|
||||
if !i.main.Exists(key) {
|
||||
i.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Config) SetPassword(value string) {
|
||||
// if blank, don't bother hashing; we want it to be blank
|
||||
if value == "" {
|
||||
i.Set(Password, "")
|
||||
i.SetString(Password, "")
|
||||
} else {
|
||||
i.Set(Password, hashPassword(value))
|
||||
i.SetString(Password, hashPassword(value))
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Config) Write() error {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
return i.main.WriteConfig()
|
||||
|
||||
data, err := i.marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(i.filePath, data, 0640)
|
||||
}
|
||||
|
||||
func (i *Config) Marshal() ([]byte, error) {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
return i.marshal()
|
||||
}
|
||||
|
||||
func (i *Config) marshal() ([]byte, error) {
|
||||
return i.main.Marshal(yaml.Parser())
|
||||
}
|
||||
|
||||
// FileEnvSet returns true if the configuration file environment parameter
|
||||
@@ -405,7 +496,7 @@ func FileEnvSet() bool {
|
||||
func (i *Config) GetConfigFile() string {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
return i.main.ConfigFileUsed()
|
||||
return i.filePath
|
||||
}
|
||||
|
||||
// GetConfigPath returns the path of the directory containing the used
|
||||
@@ -414,18 +505,32 @@ func (i *Config) GetConfigPath() string {
|
||||
return filepath.Dir(i.GetConfigFile())
|
||||
}
|
||||
|
||||
// GetConfigPathAbs returns the path of the directory containing the used
|
||||
// configuration file, resolved to an absolute path. Returns the return value
|
||||
// of GetConfigPath if the path cannot be made into an absolute path.
|
||||
func (i *Config) GetConfigPathAbs() string {
|
||||
p := filepath.Dir(i.GetConfigFile())
|
||||
|
||||
ret, _ := filepath.Abs(p)
|
||||
if ret == "" {
|
||||
return p
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetDefaultDatabaseFilePath returns the default database filename,
|
||||
// which is located in the same directory as the config file.
|
||||
func (i *Config) GetDefaultDatabaseFilePath() string {
|
||||
return filepath.Join(i.GetConfigPath(), "stash-go.sqlite")
|
||||
}
|
||||
|
||||
// viper returns the viper instance that should be used to get the provided
|
||||
// forKey returns the Koanf instance that should be used to get the provided
|
||||
// key. Returns the overrides instance if the key exists there, otherwise it
|
||||
// returns the main instance. Assumes read lock held.
|
||||
func (i *Config) viper(key string) *viper.Viper {
|
||||
func (i *Config) forKey(key string) *koanf.Koanf {
|
||||
v := i.main
|
||||
if i.overrides.IsSet(key) {
|
||||
if i.overrides.Exists(key) {
|
||||
v = i.overrides
|
||||
}
|
||||
|
||||
@@ -434,10 +539,10 @@ func (i *Config) viper(key string) *viper.Viper {
|
||||
|
||||
// viper returns the viper instance that has the key set. Returns nil
|
||||
// if no instance has the key. Assumes read lock held.
|
||||
func (i *Config) viperWith(key string) *viper.Viper {
|
||||
v := i.viper(key)
|
||||
func (i *Config) with(key string) *koanf.Koanf {
|
||||
v := i.forKey(key)
|
||||
|
||||
if v.IsSet(key) {
|
||||
if v.Exists(key) {
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -448,7 +553,7 @@ func (i *Config) HasOverride(key string) bool {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
return i.overrides.IsSet(key)
|
||||
return i.overrides.Exists(key)
|
||||
}
|
||||
|
||||
// These functions wrap the equivalent viper functions, checking the override
|
||||
@@ -458,28 +563,28 @@ func (i *Config) unmarshalKey(key string, rawVal interface{}) error {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
return i.viper(key).UnmarshalKey(key, rawVal)
|
||||
return i.forKey(key).Unmarshal(key, rawVal)
|
||||
}
|
||||
|
||||
func (i *Config) getStringSlice(key string) []string {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
return i.viper(key).GetStringSlice(key)
|
||||
return i.forKey(key).Strings(key)
|
||||
}
|
||||
|
||||
func (i *Config) getString(key string) string {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
return i.viper(key).GetString(key)
|
||||
return i.forKey(key).String(key)
|
||||
}
|
||||
|
||||
func (i *Config) getBool(key string) bool {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
return i.viper(key).GetBool(key)
|
||||
return i.forKey(key).Bool(key)
|
||||
}
|
||||
|
||||
func (i *Config) getBoolDefault(key string, def bool) bool {
|
||||
@@ -487,9 +592,9 @@ func (i *Config) getBoolDefault(key string, def bool) bool {
|
||||
defer i.RUnlock()
|
||||
|
||||
ret := def
|
||||
v := i.viper(key)
|
||||
if v.IsSet(key) {
|
||||
ret = v.GetBool(key)
|
||||
v := i.forKey(key)
|
||||
if v.Exists(key) {
|
||||
ret = v.Bool(key)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -498,21 +603,21 @@ func (i *Config) getInt(key string) int {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
return i.viper(key).GetInt(key)
|
||||
return i.forKey(key).Int(key)
|
||||
}
|
||||
|
||||
func (i *Config) getFloat64(key string) float64 {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
return i.viper(key).GetFloat64(key)
|
||||
return i.forKey(key).Float64(key)
|
||||
}
|
||||
|
||||
func (i *Config) getStringMapString(key string) map[string]string {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
ret := i.viper(key).GetStringMapString(key)
|
||||
ret := i.forKey(key).StringMap(key)
|
||||
|
||||
// GetStringMapString returns an empty map regardless of whether the
|
||||
// key exists or not.
|
||||
@@ -533,13 +638,13 @@ func (i *Config) GetStashPaths() StashConfigs {
|
||||
var ret StashConfigs
|
||||
|
||||
v := i.main
|
||||
if !v.IsSet(Stash) {
|
||||
if !v.Exists(Stash) {
|
||||
v = i.overrides
|
||||
}
|
||||
|
||||
if err := v.UnmarshalKey(Stash, &ret); err != nil || len(ret) == 0 {
|
||||
if err := v.Unmarshal(Stash, &ret); err != nil || len(ret) == 0 {
|
||||
// fallback to legacy format
|
||||
ss := v.GetStringSlice(Stash)
|
||||
ss := v.Strings(Stash)
|
||||
ret = nil
|
||||
for _, path := range ss {
|
||||
toAdd := &StashConfig{
|
||||
@@ -597,12 +702,25 @@ 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
|
||||
}
|
||||
|
||||
// GetFFMpegPath returns the path to the FFMpeg executable.
|
||||
// If empty, stash will attempt to resolve it from the path.
|
||||
func (i *Config) GetFFMpegPath() string {
|
||||
return i.getString(FFMpegPath)
|
||||
}
|
||||
|
||||
// GetFFProbePath returns the path to the FFProbe executable.
|
||||
// If empty, stash will attempt to resolve it from the path.
|
||||
func (i *Config) GetFFProbePath() string {
|
||||
return i.getString(FFProbePath)
|
||||
}
|
||||
|
||||
func (i *Config) GetJWTSignKey() []byte {
|
||||
return []byte(i.getString(JWTSignKey))
|
||||
}
|
||||
@@ -628,7 +746,7 @@ func (i *Config) GetImageExcludes() []string {
|
||||
|
||||
func (i *Config) GetVideoExtensions() []string {
|
||||
ret := i.getStringSlice(VideoExtensions)
|
||||
if ret == nil {
|
||||
if len(ret) == 0 {
|
||||
ret = defaultVideoExtensions
|
||||
}
|
||||
return ret
|
||||
@@ -636,7 +754,7 @@ func (i *Config) GetVideoExtensions() []string {
|
||||
|
||||
func (i *Config) GetImageExtensions() []string {
|
||||
ret := i.getStringSlice(ImageExtensions)
|
||||
if ret == nil {
|
||||
if len(ret) == 0 {
|
||||
ret = defaultImageExtensions
|
||||
}
|
||||
return ret
|
||||
@@ -644,7 +762,7 @@ func (i *Config) GetImageExtensions() []string {
|
||||
|
||||
func (i *Config) GetGalleryExtensions() []string {
|
||||
ret := i.getStringSlice(GalleryExtensions)
|
||||
if ret == nil {
|
||||
if len(ret) == 0 {
|
||||
ret = defaultGalleryExtensions
|
||||
}
|
||||
return ret
|
||||
@@ -750,16 +868,15 @@ func (i *Config) GetAllPluginConfiguration() map[string]map[string]interface{} {
|
||||
|
||||
ret := make(map[string]map[string]interface{})
|
||||
|
||||
sub := i.viper(PluginsSetting).GetStringMap(PluginsSetting)
|
||||
v := i.forKey(PluginsSetting)
|
||||
|
||||
sub := v.Cut(PluginsSetting)
|
||||
if sub == nil {
|
||||
return ret
|
||||
}
|
||||
|
||||
for plugin := range sub {
|
||||
// HACK: viper changes map keys to case insensitive values, so the workaround is to
|
||||
// convert map keys to snake case for storage
|
||||
name := fromSnakeCase(plugin)
|
||||
ret[name] = fromSnakeCaseMap(i.viper(PluginsSetting).GetStringMap(PluginsSettingPrefix + plugin))
|
||||
for plugin := range sub.Raw() {
|
||||
ret[plugin] = sub.Cut(plugin).Raw()
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -769,26 +886,20 @@ func (i *Config) GetPluginConfiguration(pluginID string) map[string]interface{}
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
key := PluginsSettingPrefix + toSnakeCase(pluginID)
|
||||
key := PluginsSettingPrefix + pluginID
|
||||
|
||||
// HACK: viper changes map keys to case insensitive values, so the workaround is to
|
||||
// convert map keys to snake case for storage
|
||||
v := i.viper(key).GetStringMap(key)
|
||||
|
||||
return fromSnakeCaseMap(v)
|
||||
return i.forKey(key).Cut(key).Raw()
|
||||
}
|
||||
|
||||
// SetPluginConfiguration sets the configuration for a plugin.
|
||||
// It will overwrite any existing configuration.
|
||||
func (i *Config) SetPluginConfiguration(pluginID string, v map[string]interface{}) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
pluginID = toSnakeCase(pluginID)
|
||||
|
||||
key := PluginsSettingPrefix + pluginID
|
||||
|
||||
// HACK: viper changes map keys to case insensitive values, so the workaround is to
|
||||
// convert map keys to snake case for storage
|
||||
i.viper(key).Set(key, toSnakeCaseMap(v))
|
||||
i.set(key, v)
|
||||
}
|
||||
|
||||
func (i *Config) GetDisabledPlugins() []string {
|
||||
@@ -988,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"`
|
||||
@@ -1009,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"}
|
||||
}
|
||||
|
||||
@@ -1028,9 +1142,9 @@ func (i *Config) GetMaxSessionAge() int {
|
||||
defer i.RUnlock()
|
||||
|
||||
ret := DefaultMaxSessionAge
|
||||
v := i.viper(MaxSessionAge)
|
||||
if v.IsSet(MaxSessionAge) {
|
||||
ret = v.GetInt(MaxSessionAge)
|
||||
v := i.forKey(MaxSessionAge)
|
||||
if v.Exists(MaxSessionAge) {
|
||||
ret = v.Int(MaxSessionAge)
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -1042,17 +1156,21 @@ func (i *Config) GetCustomServedFolders() utils.URLMap {
|
||||
return i.getStringMapString(CustomServedFolders)
|
||||
}
|
||||
|
||||
func (i *Config) GetCustomUILocation() string {
|
||||
return i.getString(CustomUILocation)
|
||||
func (i *Config) GetUILocation() string {
|
||||
if ret := i.getString(UILocation); ret != "" {
|
||||
return ret
|
||||
}
|
||||
|
||||
return i.getString(LegacyCustomUILocation)
|
||||
}
|
||||
|
||||
// Interface options
|
||||
func (i *Config) GetMenuItems() []string {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
v := i.viper(MenuItems)
|
||||
if v.IsSet(MenuItems) {
|
||||
return v.GetStringSlice(MenuItems)
|
||||
v := i.forKey(MenuItems)
|
||||
if v.Exists(MenuItems) {
|
||||
return v.Strings(MenuItems)
|
||||
}
|
||||
return defaultMenuItems
|
||||
}
|
||||
@@ -1066,9 +1184,9 @@ func (i *Config) GetWallShowTitle() bool {
|
||||
defer i.RUnlock()
|
||||
|
||||
ret := defaultWallShowTitle
|
||||
v := i.viper(WallShowTitle)
|
||||
if v.IsSet(WallShowTitle) {
|
||||
ret = v.GetBool(WallShowTitle)
|
||||
v := i.forKey(WallShowTitle)
|
||||
if v.Exists(WallShowTitle) {
|
||||
ret = v.Bool(WallShowTitle)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -1082,9 +1200,9 @@ func (i *Config) GetWallPlayback() string {
|
||||
defer i.RUnlock()
|
||||
|
||||
ret := defaultWallPlayback
|
||||
v := i.viper(WallPlayback)
|
||||
if v.IsSet(WallPlayback) {
|
||||
ret = v.GetString(WallPlayback)
|
||||
v := i.forKey(WallPlayback)
|
||||
if v.Exists(WallPlayback) {
|
||||
ret = v.String(WallPlayback)
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -1118,14 +1236,14 @@ func (i *Config) getSlideshowDelay() int {
|
||||
// assume have lock
|
||||
|
||||
ret := defaultImageLightboxSlideshowDelay
|
||||
v := i.viper(ImageLightboxSlideshowDelay)
|
||||
if v.IsSet(ImageLightboxSlideshowDelay) {
|
||||
ret = v.GetInt(ImageLightboxSlideshowDelay)
|
||||
v := i.forKey(ImageLightboxSlideshowDelay)
|
||||
if v.Exists(ImageLightboxSlideshowDelay) {
|
||||
ret = v.Int(ImageLightboxSlideshowDelay)
|
||||
} else {
|
||||
// fallback to old location
|
||||
v := i.viper(legacyImageLightboxSlideshowDelay)
|
||||
if v.IsSet(legacyImageLightboxSlideshowDelay) {
|
||||
ret = v.GetInt(legacyImageLightboxSlideshowDelay)
|
||||
v := i.forKey(legacyImageLightboxSlideshowDelay)
|
||||
if v.Exists(legacyImageLightboxSlideshowDelay) {
|
||||
ret = v.Int(legacyImageLightboxSlideshowDelay)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1142,24 +1260,24 @@ func (i *Config) GetImageLightboxOptions() ConfigImageLightboxResult {
|
||||
SlideshowDelay: &delay,
|
||||
}
|
||||
|
||||
if v := i.viperWith(ImageLightboxDisplayModeKey); v != nil {
|
||||
mode := ImageLightboxDisplayMode(v.GetString(ImageLightboxDisplayModeKey))
|
||||
if v := i.with(ImageLightboxDisplayModeKey); v != nil {
|
||||
mode := ImageLightboxDisplayMode(v.String(ImageLightboxDisplayModeKey))
|
||||
ret.DisplayMode = &mode
|
||||
}
|
||||
if v := i.viperWith(ImageLightboxScaleUp); v != nil {
|
||||
value := v.GetBool(ImageLightboxScaleUp)
|
||||
if v := i.with(ImageLightboxScaleUp); v != nil {
|
||||
value := v.Bool(ImageLightboxScaleUp)
|
||||
ret.ScaleUp = &value
|
||||
}
|
||||
if v := i.viperWith(ImageLightboxResetZoomOnNav); v != nil {
|
||||
value := v.GetBool(ImageLightboxResetZoomOnNav)
|
||||
if v := i.with(ImageLightboxResetZoomOnNav); v != nil {
|
||||
value := v.Bool(ImageLightboxResetZoomOnNav)
|
||||
ret.ResetZoomOnNav = &value
|
||||
}
|
||||
if v := i.viperWith(ImageLightboxScrollModeKey); v != nil {
|
||||
mode := ImageLightboxScrollMode(v.GetString(ImageLightboxScrollModeKey))
|
||||
if v := i.with(ImageLightboxScrollModeKey); v != nil {
|
||||
mode := ImageLightboxScrollMode(v.String(ImageLightboxScrollModeKey))
|
||||
ret.ScrollMode = &mode
|
||||
}
|
||||
if v := i.viperWith(ImageLightboxScrollAttemptsBeforeChange); v != nil {
|
||||
ret.ScrollAttemptsBeforeChange = v.GetInt(ImageLightboxScrollAttemptsBeforeChange)
|
||||
if v := i.with(ImageLightboxScrollAttemptsBeforeChange); v != nil {
|
||||
ret.ScrollAttemptsBeforeChange = v.Int(ImageLightboxScrollAttemptsBeforeChange)
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -1178,20 +1296,14 @@ func (i *Config) GetUIConfiguration() map[string]interface{} {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
// HACK: viper changes map keys to case insensitive values, so the workaround is to
|
||||
// convert map keys to snake case for storage
|
||||
v := i.viper(UI).GetStringMap(UI)
|
||||
|
||||
return fromSnakeCaseMap(v)
|
||||
return i.forKey(UI).Cut(UI).Raw()
|
||||
}
|
||||
|
||||
func (i *Config) SetUIConfiguration(v map[string]interface{}) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
// HACK: viper changes map keys to case insensitive values, so the workaround is to
|
||||
// convert map keys to snake case for storage
|
||||
i.viper(UI).Set(UI, toSnakeCaseMap(v))
|
||||
i.set(UI, v)
|
||||
}
|
||||
|
||||
func (i *Config) GetCSSPath() string {
|
||||
@@ -1349,11 +1461,12 @@ func (i *Config) GetDeleteGeneratedDefault() bool {
|
||||
func (i *Config) GetDefaultIdentifySettings() *identify.Options {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
v := i.viper(DefaultIdentifySettings)
|
||||
v := i.forKey(DefaultIdentifySettings)
|
||||
|
||||
if v.IsSet(DefaultIdentifySettings) {
|
||||
if v.Exists(DefaultIdentifySettings) && v.Get(DefaultIdentifySettings) != nil {
|
||||
var ret identify.Options
|
||||
if err := v.UnmarshalKey(DefaultIdentifySettings, &ret); err != nil {
|
||||
|
||||
if err := v.Unmarshal(DefaultIdentifySettings, &ret); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &ret
|
||||
@@ -1368,11 +1481,11 @@ func (i *Config) GetDefaultIdentifySettings() *identify.Options {
|
||||
func (i *Config) GetDefaultScanSettings() *ScanMetadataOptions {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
v := i.viper(DefaultScanSettings)
|
||||
v := i.forKey(DefaultScanSettings)
|
||||
|
||||
if v.IsSet(DefaultScanSettings) {
|
||||
if v.Exists(DefaultScanSettings) && v.Get(DefaultScanSettings) != nil {
|
||||
var ret ScanMetadataOptions
|
||||
if err := v.UnmarshalKey(DefaultScanSettings, &ret); err != nil {
|
||||
if err := v.Unmarshal(DefaultScanSettings, &ret); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &ret
|
||||
@@ -1387,11 +1500,11 @@ func (i *Config) GetDefaultScanSettings() *ScanMetadataOptions {
|
||||
func (i *Config) GetDefaultAutoTagSettings() *AutoTagMetadataOptions {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
v := i.viper(DefaultAutoTagSettings)
|
||||
v := i.forKey(DefaultAutoTagSettings)
|
||||
|
||||
if v.IsSet(DefaultAutoTagSettings) {
|
||||
if v.Exists(DefaultAutoTagSettings) {
|
||||
var ret AutoTagMetadataOptions
|
||||
if err := v.UnmarshalKey(DefaultAutoTagSettings, &ret); err != nil {
|
||||
if err := v.Unmarshal(DefaultAutoTagSettings, &ret); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &ret
|
||||
@@ -1406,11 +1519,11 @@ func (i *Config) GetDefaultAutoTagSettings() *AutoTagMetadataOptions {
|
||||
func (i *Config) GetDefaultGenerateSettings() *models.GenerateMetadataOptions {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
v := i.viper(DefaultGenerateSettings)
|
||||
v := i.forKey(DefaultGenerateSettings)
|
||||
|
||||
if v.IsSet(DefaultGenerateSettings) {
|
||||
if v.Exists(DefaultGenerateSettings) {
|
||||
var ret models.GenerateMetadataOptions
|
||||
if err := v.UnmarshalKey(DefaultGenerateSettings, &ret); err != nil {
|
||||
if err := v.Unmarshal(DefaultGenerateSettings, &ret); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &ret
|
||||
@@ -1455,6 +1568,21 @@ func (i *Config) GetDLNAInterfaces() []string {
|
||||
return i.getStringSlice(DLNAInterfaces)
|
||||
}
|
||||
|
||||
// GetDLNAPort returns the port to run the DLNA server on. If empty, 1338
|
||||
// will be used.
|
||||
func (i *Config) GetDLNAPort() int {
|
||||
ret := i.getInt(DLNAPort)
|
||||
if ret == 0 {
|
||||
ret = DLNAPortDefault
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetDLNAPortAsString returns the port to run the DLNA server on as a string.
|
||||
func (i *Config) GetDLNAPortAsString() string {
|
||||
return ":" + strconv.Itoa(i.GetDLNAPort())
|
||||
}
|
||||
|
||||
// GetVideoSortOrder returns the sort order to display videos. If
|
||||
// empty, videos will be sorted by titles.
|
||||
func (i *Config) GetVideoSortOrder() string {
|
||||
@@ -1502,9 +1630,9 @@ func (i *Config) GetMaxUploadSize() int64 {
|
||||
defer i.RUnlock()
|
||||
ret := int64(1024)
|
||||
|
||||
v := i.viper(MaxUploadSize)
|
||||
if v.IsSet(MaxUploadSize) {
|
||||
ret = v.GetInt64(MaxUploadSize)
|
||||
v := i.forKey(MaxUploadSize)
|
||||
if v.Exists(MaxUploadSize) {
|
||||
ret = v.Int64(MaxUploadSize)
|
||||
}
|
||||
return ret << 20
|
||||
}
|
||||
@@ -1534,7 +1662,7 @@ func (i *Config) GetNoProxy() string {
|
||||
// config field to the provided IP address to indicate that stash has been accessed
|
||||
// from this public IP without authentication.
|
||||
func (i *Config) ActivatePublicAccessTripwire(requestIP string) error {
|
||||
i.Set(SecurityTripwireAccessedFromPublicInternet, requestIP)
|
||||
i.SetString(SecurityTripwireAccessedFromPublicInternet, requestIP)
|
||||
return i.Write()
|
||||
}
|
||||
|
||||
@@ -1604,7 +1732,7 @@ func (i *Config) Validate() error {
|
||||
var missingFields []string
|
||||
|
||||
for _, p := range mandatoryPaths {
|
||||
if !i.viper(p).IsSet(p) || i.viper(p).GetString(p) == "" {
|
||||
if !i.forKey(p).Exists(p) || i.forKey(p).String(p) == "" {
|
||||
missingFields = append(missingFields, p)
|
||||
}
|
||||
}
|
||||
@@ -1615,7 +1743,7 @@ func (i *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if i.GetBlobsStorage() == BlobStorageTypeFilesystem && i.viper(BlobsPath).GetString(BlobsPath) == "" {
|
||||
if i.GetBlobsStorage() == BlobStorageTypeFilesystem && i.forKey(BlobsPath).String(BlobsPath) == "" {
|
||||
return MissingConfigError{
|
||||
missingFields: []string{BlobsPath},
|
||||
}
|
||||
@@ -1635,52 +1763,52 @@ func (i *Config) setDefaultValues() {
|
||||
|
||||
// set the default host and port so that these are written to the config
|
||||
// file
|
||||
i.main.SetDefault(Host, hostDefault)
|
||||
i.main.SetDefault(Port, portDefault)
|
||||
i.setDefault(Host, hostDefault)
|
||||
i.setDefault(Port, portDefault)
|
||||
|
||||
i.main.SetDefault(ParallelTasks, parallelTasksDefault)
|
||||
i.main.SetDefault(SequentialScanning, SequentialScanningDefault)
|
||||
i.main.SetDefault(PreviewSegmentDuration, previewSegmentDurationDefault)
|
||||
i.main.SetDefault(PreviewSegments, previewSegmentsDefault)
|
||||
i.main.SetDefault(PreviewExcludeStart, previewExcludeStartDefault)
|
||||
i.main.SetDefault(PreviewExcludeEnd, previewExcludeEndDefault)
|
||||
i.main.SetDefault(PreviewAudio, previewAudioDefault)
|
||||
i.main.SetDefault(SoundOnPreview, false)
|
||||
i.setDefault(ParallelTasks, parallelTasksDefault)
|
||||
i.setDefault(SequentialScanning, SequentialScanningDefault)
|
||||
i.setDefault(PreviewSegmentDuration, previewSegmentDurationDefault)
|
||||
i.setDefault(PreviewSegments, previewSegmentsDefault)
|
||||
i.setDefault(PreviewExcludeStart, previewExcludeStartDefault)
|
||||
i.setDefault(PreviewExcludeEnd, previewExcludeEndDefault)
|
||||
i.setDefault(PreviewAudio, previewAudioDefault)
|
||||
i.setDefault(SoundOnPreview, false)
|
||||
|
||||
i.main.SetDefault(ThemeColor, DefaultThemeColor)
|
||||
i.setDefault(ThemeColor, DefaultThemeColor)
|
||||
|
||||
i.main.SetDefault(WriteImageThumbnails, writeImageThumbnailsDefault)
|
||||
i.main.SetDefault(CreateImageClipsFromVideos, createImageClipsFromVideosDefault)
|
||||
i.setDefault(WriteImageThumbnails, writeImageThumbnailsDefault)
|
||||
i.setDefault(CreateImageClipsFromVideos, createImageClipsFromVideosDefault)
|
||||
|
||||
i.main.SetDefault(Database, defaultDatabaseFilePath)
|
||||
i.setDefault(Database, defaultDatabaseFilePath)
|
||||
|
||||
i.main.SetDefault(dangerousAllowPublicWithoutAuth, dangerousAllowPublicWithoutAuthDefault)
|
||||
i.main.SetDefault(SecurityTripwireAccessedFromPublicInternet, securityTripwireAccessedFromPublicInternetDefault)
|
||||
i.setDefault(dangerousAllowPublicWithoutAuth, dangerousAllowPublicWithoutAuthDefault)
|
||||
i.setDefault(SecurityTripwireAccessedFromPublicInternet, securityTripwireAccessedFromPublicInternetDefault)
|
||||
|
||||
// Set generated to the metadata path for backwards compat
|
||||
i.main.SetDefault(Generated, i.main.GetString(Metadata))
|
||||
i.setDefault(Generated, i.main.String(Metadata))
|
||||
|
||||
i.main.SetDefault(NoBrowser, NoBrowserDefault)
|
||||
i.main.SetDefault(NotificationsEnabled, NotificationsEnabledDefault)
|
||||
i.main.SetDefault(ShowOneTimeMovedNotification, ShowOneTimeMovedNotificationDefault)
|
||||
i.setDefault(NoBrowser, NoBrowserDefault)
|
||||
i.setDefault(NotificationsEnabled, NotificationsEnabledDefault)
|
||||
i.setDefault(ShowOneTimeMovedNotification, ShowOneTimeMovedNotificationDefault)
|
||||
|
||||
// Set default scrapers and plugins paths
|
||||
i.main.SetDefault(ScrapersPath, defaultScrapersPath)
|
||||
i.main.SetDefault(PluginsPath, defaultPluginsPath)
|
||||
i.setDefault(ScrapersPath, defaultScrapersPath)
|
||||
i.setDefault(PluginsPath, defaultPluginsPath)
|
||||
|
||||
// Set default gallery cover regex
|
||||
i.main.SetDefault(GalleryCoverRegex, galleryCoverRegexDefault)
|
||||
i.setDefault(GalleryCoverRegex, galleryCoverRegexDefault)
|
||||
|
||||
// Set NoProxy default
|
||||
i.main.SetDefault(NoProxy, noProxyDefault)
|
||||
i.setDefault(NoProxy, noProxyDefault)
|
||||
|
||||
// set default package sources
|
||||
i.main.SetDefault(PluginPackageSources, []map[string]string{{
|
||||
i.setDefault(PluginPackageSources, []map[string]string{{
|
||||
"name": sourceDefaultName,
|
||||
"url": pluginPackageSourcesDefault,
|
||||
"localpath": sourceDefaultPath,
|
||||
}})
|
||||
i.main.SetDefault(ScraperPackageSources, []map[string]string{{
|
||||
i.setDefault(ScraperPackageSources, []map[string]string{{
|
||||
"name": sourceDefaultName,
|
||||
"url": scraperPackageSourcesDefault,
|
||||
"localpath": sourceDefaultPath,
|
||||
@@ -1696,13 +1824,13 @@ func (i *Config) setExistingSystemDefaults() {
|
||||
if !i.isNewSystem {
|
||||
// Existing systems as of the introduction of auto-browser open should retain existing
|
||||
// behavior and not start the browser automatically.
|
||||
if !i.main.InConfig(NoBrowser) {
|
||||
i.main.Set(NoBrowser, true)
|
||||
if !i.main.Exists(NoBrowser) {
|
||||
i.set(NoBrowser, true)
|
||||
}
|
||||
|
||||
// Existing systems as of the introduction of the taskbar should inform users.
|
||||
if !i.main.InConfig(ShowOneTimeMovedNotification) {
|
||||
i.main.Set(ShowOneTimeMovedNotification, true)
|
||||
if !i.main.Exists(ShowOneTimeMovedNotification) {
|
||||
i.set(ShowOneTimeMovedNotification, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1717,7 +1845,7 @@ func (i *Config) SetInitialConfig() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating JWTSignKey: %w", err)
|
||||
}
|
||||
i.Set(JWTSignKey, signKey)
|
||||
i.SetString(JWTSignKey, signKey)
|
||||
}
|
||||
|
||||
if string(i.GetSessionStoreKey()) == "" {
|
||||
@@ -1725,7 +1853,7 @@ func (i *Config) SetInitialConfig() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating session store key: %w", err)
|
||||
}
|
||||
i.Set(SessionStoreKey, sessionStoreKey)
|
||||
i.SetString(SessionStoreKey, sessionStoreKey)
|
||||
}
|
||||
|
||||
i.setDefaultValues()
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// should be run with -race
|
||||
@@ -16,6 +17,7 @@ func TestConcurrentConfigAccess(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func(wk int) {
|
||||
for l := 0; l < loops; l++ {
|
||||
start := time.Now()
|
||||
if err := i.SetInitialConfig(); err != nil {
|
||||
t.Errorf("Failure setting initial configuration in worker %v iteration %v: %v", wk, l, err)
|
||||
}
|
||||
@@ -25,96 +27,102 @@ func TestConcurrentConfigAccess(t *testing.T) {
|
||||
i.GetConfigFile()
|
||||
i.GetConfigPath()
|
||||
i.GetDefaultDatabaseFilePath()
|
||||
i.Set(BackupDirectoryPath, i.GetBackupDirectoryPath())
|
||||
i.SetInterface(BackupDirectoryPath, i.GetBackupDirectoryPath())
|
||||
i.GetStashPaths()
|
||||
_ = i.ValidateStashBoxes(nil)
|
||||
_ = i.Validate()
|
||||
_ = i.ActivatePublicAccessTripwire("")
|
||||
i.Set(Cache, i.GetCachePath())
|
||||
i.Set(Generated, i.GetGeneratedPath())
|
||||
i.Set(Metadata, i.GetMetadataPath())
|
||||
i.Set(Database, i.GetDatabasePath())
|
||||
i.Set(JWTSignKey, i.GetJWTSignKey())
|
||||
i.Set(SessionStoreKey, i.GetSessionStoreKey())
|
||||
i.SetInterface(Cache, i.GetCachePath())
|
||||
i.SetInterface(Generated, i.GetGeneratedPath())
|
||||
i.SetInterface(Metadata, i.GetMetadataPath())
|
||||
i.SetInterface(Database, i.GetDatabasePath())
|
||||
|
||||
// these must be set as strings since the original values are also strings
|
||||
// setting them as []byte will cause the returned string to be corrupted
|
||||
i.SetInterface(JWTSignKey, string(i.GetJWTSignKey()))
|
||||
i.SetInterface(SessionStoreKey, string(i.GetSessionStoreKey()))
|
||||
|
||||
i.GetDefaultScrapersPath()
|
||||
i.Set(Exclude, i.GetExcludes())
|
||||
i.Set(ImageExclude, i.GetImageExcludes())
|
||||
i.Set(VideoExtensions, i.GetVideoExtensions())
|
||||
i.Set(ImageExtensions, i.GetImageExtensions())
|
||||
i.Set(GalleryExtensions, i.GetGalleryExtensions())
|
||||
i.Set(CreateGalleriesFromFolders, i.GetCreateGalleriesFromFolders())
|
||||
i.Set(Language, i.GetLanguage())
|
||||
i.Set(VideoFileNamingAlgorithm, i.GetVideoFileNamingAlgorithm())
|
||||
i.Set(ScrapersPath, i.GetScrapersPath())
|
||||
i.Set(ScraperUserAgent, i.GetScraperUserAgent())
|
||||
i.Set(ScraperCDPPath, i.GetScraperCDPPath())
|
||||
i.Set(ScraperCertCheck, i.GetScraperCertCheck())
|
||||
i.Set(ScraperExcludeTagPatterns, i.GetScraperExcludeTagPatterns())
|
||||
i.Set(StashBoxes, i.GetStashBoxes())
|
||||
i.SetInterface(Exclude, i.GetExcludes())
|
||||
i.SetInterface(ImageExclude, i.GetImageExcludes())
|
||||
i.SetInterface(VideoExtensions, i.GetVideoExtensions())
|
||||
i.SetInterface(ImageExtensions, i.GetImageExtensions())
|
||||
i.SetInterface(GalleryExtensions, i.GetGalleryExtensions())
|
||||
i.SetInterface(CreateGalleriesFromFolders, i.GetCreateGalleriesFromFolders())
|
||||
i.SetInterface(Language, i.GetLanguage())
|
||||
i.SetInterface(VideoFileNamingAlgorithm, i.GetVideoFileNamingAlgorithm())
|
||||
i.SetInterface(ScrapersPath, i.GetScrapersPath())
|
||||
i.SetInterface(ScraperUserAgent, i.GetScraperUserAgent())
|
||||
i.SetInterface(ScraperCDPPath, i.GetScraperCDPPath())
|
||||
i.SetInterface(ScraperCertCheck, i.GetScraperCertCheck())
|
||||
i.SetInterface(ScraperExcludeTagPatterns, i.GetScraperExcludeTagPatterns())
|
||||
i.SetInterface(StashBoxes, i.GetStashBoxes())
|
||||
i.GetDefaultPluginsPath()
|
||||
i.Set(PluginsPath, i.GetPluginsPath())
|
||||
i.Set(Host, i.GetHost())
|
||||
i.Set(Port, i.GetPort())
|
||||
i.Set(ExternalHost, i.GetExternalHost())
|
||||
i.Set(PreviewSegmentDuration, i.GetPreviewSegmentDuration())
|
||||
i.Set(ParallelTasks, i.GetParallelTasks())
|
||||
i.Set(ParallelTasks, i.GetParallelTasksWithAutoDetection())
|
||||
i.Set(PreviewAudio, i.GetPreviewAudio())
|
||||
i.Set(PreviewSegments, i.GetPreviewSegments())
|
||||
i.Set(PreviewExcludeStart, i.GetPreviewExcludeStart())
|
||||
i.Set(PreviewExcludeEnd, i.GetPreviewExcludeEnd())
|
||||
i.Set(PreviewPreset, i.GetPreviewPreset())
|
||||
i.Set(MaxTranscodeSize, i.GetMaxTranscodeSize())
|
||||
i.Set(MaxStreamingTranscodeSize, i.GetMaxStreamingTranscodeSize())
|
||||
i.Set(ApiKey, i.GetAPIKey())
|
||||
i.Set(Username, i.GetUsername())
|
||||
i.Set(Password, i.GetPasswordHash())
|
||||
i.SetInterface(PluginsPath, i.GetPluginsPath())
|
||||
i.SetInterface(Host, i.GetHost())
|
||||
i.SetInterface(Port, i.GetPort())
|
||||
i.SetInterface(ExternalHost, i.GetExternalHost())
|
||||
i.SetInterface(PreviewSegmentDuration, i.GetPreviewSegmentDuration())
|
||||
i.SetInterface(ParallelTasks, i.GetParallelTasks())
|
||||
i.SetInterface(ParallelTasks, i.GetParallelTasksWithAutoDetection())
|
||||
i.SetInterface(PreviewAudio, i.GetPreviewAudio())
|
||||
i.SetInterface(PreviewSegments, i.GetPreviewSegments())
|
||||
i.SetInterface(PreviewExcludeStart, i.GetPreviewExcludeStart())
|
||||
i.SetInterface(PreviewExcludeEnd, i.GetPreviewExcludeEnd())
|
||||
i.SetInterface(PreviewPreset, i.GetPreviewPreset())
|
||||
i.SetInterface(MaxTranscodeSize, i.GetMaxTranscodeSize())
|
||||
i.SetInterface(MaxStreamingTranscodeSize, i.GetMaxStreamingTranscodeSize())
|
||||
i.SetInterface(ApiKey, i.GetAPIKey())
|
||||
i.SetInterface(Username, i.GetUsername())
|
||||
i.SetInterface(Password, i.GetPasswordHash())
|
||||
i.GetCredentials()
|
||||
i.Set(MaxSessionAge, i.GetMaxSessionAge())
|
||||
i.Set(CustomServedFolders, i.GetCustomServedFolders())
|
||||
i.Set(CustomUILocation, i.GetCustomUILocation())
|
||||
i.Set(MenuItems, i.GetMenuItems())
|
||||
i.Set(SoundOnPreview, i.GetSoundOnPreview())
|
||||
i.Set(WallShowTitle, i.GetWallShowTitle())
|
||||
i.Set(CustomPerformerImageLocation, i.GetCustomPerformerImageLocation())
|
||||
i.Set(WallPlayback, i.GetWallPlayback())
|
||||
i.Set(MaximumLoopDuration, i.GetMaximumLoopDuration())
|
||||
i.Set(AutostartVideo, i.GetAutostartVideo())
|
||||
i.Set(ShowStudioAsText, i.GetShowStudioAsText())
|
||||
i.Set(legacyImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay)
|
||||
i.Set(ImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay)
|
||||
i.SetInterface(MaxSessionAge, i.GetMaxSessionAge())
|
||||
i.SetInterface(CustomServedFolders, i.GetCustomServedFolders())
|
||||
i.SetInterface(LegacyCustomUILocation, i.GetUILocation())
|
||||
i.SetInterface(MenuItems, i.GetMenuItems())
|
||||
i.SetInterface(SoundOnPreview, i.GetSoundOnPreview())
|
||||
i.SetInterface(WallShowTitle, i.GetWallShowTitle())
|
||||
i.SetInterface(CustomPerformerImageLocation, i.GetCustomPerformerImageLocation())
|
||||
i.SetInterface(WallPlayback, i.GetWallPlayback())
|
||||
i.SetInterface(MaximumLoopDuration, i.GetMaximumLoopDuration())
|
||||
i.SetInterface(AutostartVideo, i.GetAutostartVideo())
|
||||
i.SetInterface(ShowStudioAsText, i.GetShowStudioAsText())
|
||||
i.SetInterface(legacyImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay)
|
||||
i.SetInterface(ImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay)
|
||||
i.GetCSSPath()
|
||||
i.GetCSS()
|
||||
i.GetJavascriptPath()
|
||||
i.GetJavascript()
|
||||
i.GetCustomLocalesPath()
|
||||
i.GetCustomLocales()
|
||||
i.Set(CSSEnabled, i.GetCSSEnabled())
|
||||
i.Set(CSSEnabled, i.GetCustomLocalesEnabled())
|
||||
i.Set(HandyKey, i.GetHandyKey())
|
||||
i.Set(UseStashHostedFunscript, i.GetUseStashHostedFunscript())
|
||||
i.Set(DLNAServerName, i.GetDLNAServerName())
|
||||
i.Set(DLNADefaultEnabled, i.GetDLNADefaultEnabled())
|
||||
i.Set(DLNADefaultIPWhitelist, i.GetDLNADefaultIPWhitelist())
|
||||
i.Set(DLNAInterfaces, i.GetDLNAInterfaces())
|
||||
i.Set(LogFile, i.GetLogFile())
|
||||
i.Set(LogOut, i.GetLogOut())
|
||||
i.Set(LogLevel, i.GetLogLevel())
|
||||
i.Set(LogAccess, i.GetLogAccess())
|
||||
i.Set(MaxUploadSize, i.GetMaxUploadSize())
|
||||
i.Set(FunscriptOffset, i.GetFunscriptOffset())
|
||||
i.Set(DefaultIdentifySettings, i.GetDefaultIdentifySettings())
|
||||
i.Set(DeleteGeneratedDefault, i.GetDeleteGeneratedDefault())
|
||||
i.Set(DeleteFileDefault, i.GetDeleteFileDefault())
|
||||
i.Set(dangerousAllowPublicWithoutAuth, i.GetDangerousAllowPublicWithoutAuth())
|
||||
i.Set(SecurityTripwireAccessedFromPublicInternet, i.GetSecurityTripwireAccessedFromPublicInternet())
|
||||
i.Set(DisableDropdownCreatePerformer, i.GetDisableDropdownCreate().Performer)
|
||||
i.Set(DisableDropdownCreateStudio, i.GetDisableDropdownCreate().Studio)
|
||||
i.Set(DisableDropdownCreateTag, i.GetDisableDropdownCreate().Tag)
|
||||
i.Set(DisableDropdownCreateMovie, i.GetDisableDropdownCreate().Movie)
|
||||
i.Set(AutostartVideoOnPlaySelected, i.GetAutostartVideoOnPlaySelected())
|
||||
i.Set(ContinuePlaylistDefault, i.GetContinuePlaylistDefault())
|
||||
i.Set(PythonPath, i.GetPythonPath())
|
||||
i.SetInterface(CSSEnabled, i.GetCSSEnabled())
|
||||
i.SetInterface(CSSEnabled, i.GetCustomLocalesEnabled())
|
||||
i.SetInterface(HandyKey, i.GetHandyKey())
|
||||
i.SetInterface(UseStashHostedFunscript, i.GetUseStashHostedFunscript())
|
||||
i.SetInterface(DLNAServerName, i.GetDLNAServerName())
|
||||
i.SetInterface(DLNADefaultEnabled, i.GetDLNADefaultEnabled())
|
||||
i.SetInterface(DLNADefaultIPWhitelist, i.GetDLNADefaultIPWhitelist())
|
||||
i.SetInterface(DLNAInterfaces, i.GetDLNAInterfaces())
|
||||
i.SetInterface(DLNAPort, i.GetDLNAPort())
|
||||
i.SetInterface(LogFile, i.GetLogFile())
|
||||
i.SetInterface(LogOut, i.GetLogOut())
|
||||
i.SetInterface(LogLevel, i.GetLogLevel())
|
||||
i.SetInterface(LogAccess, i.GetLogAccess())
|
||||
i.SetInterface(MaxUploadSize, i.GetMaxUploadSize())
|
||||
i.SetInterface(FunscriptOffset, i.GetFunscriptOffset())
|
||||
i.SetInterface(DefaultIdentifySettings, i.GetDefaultIdentifySettings())
|
||||
i.SetInterface(DeleteGeneratedDefault, i.GetDeleteGeneratedDefault())
|
||||
i.SetInterface(DeleteFileDefault, i.GetDeleteFileDefault())
|
||||
i.SetInterface(dangerousAllowPublicWithoutAuth, i.GetDangerousAllowPublicWithoutAuth())
|
||||
i.SetInterface(SecurityTripwireAccessedFromPublicInternet, i.GetSecurityTripwireAccessedFromPublicInternet())
|
||||
i.SetInterface(DisableDropdownCreatePerformer, i.GetDisableDropdownCreate().Performer)
|
||||
i.SetInterface(DisableDropdownCreateStudio, i.GetDisableDropdownCreate().Studio)
|
||||
i.SetInterface(DisableDropdownCreateTag, i.GetDisableDropdownCreate().Tag)
|
||||
i.SetInterface(DisableDropdownCreateMovie, i.GetDisableDropdownCreate().Movie)
|
||||
i.SetInterface(AutostartVideoOnPlaySelected, i.GetAutostartVideoOnPlaySelected())
|
||||
i.SetInterface(ContinuePlaylistDefault, i.GetContinuePlaylistDefault())
|
||||
i.SetInterface(PythonPath, i.GetPythonPath())
|
||||
t.Logf("Worker %v iteration %v took %v", wk, l, time.Since(start))
|
||||
}
|
||||
wg.Done()
|
||||
}(k)
|
||||
|
||||
34
internal/manager/config/config_test.go
Normal file
34
internal/manager/config/config_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfig_GetAllPluginConfiguration(t *testing.T) {
|
||||
i := InitializeEmpty()
|
||||
|
||||
assert.Equal(t, i.GetAllPluginConfiguration(), map[string]map[string]interface{}{})
|
||||
|
||||
i.SetPluginConfiguration("plugin1", map[string]interface{}{"key1": "value1"})
|
||||
|
||||
assert.Equal(t, map[string]map[string]interface{}{
|
||||
"plugin1": {"key1": "value1"},
|
||||
}, i.GetAllPluginConfiguration())
|
||||
|
||||
i.SetPluginConfiguration("plugin2", map[string]interface{}{"key2": "value2"})
|
||||
|
||||
assert.Equal(t, map[string]map[string]interface{}{
|
||||
"plugin1": {"key1": "value1"},
|
||||
"plugin2": {"key2": "value2"},
|
||||
}, i.GetAllPluginConfiguration())
|
||||
|
||||
// ensure SetPluginConfiguration overwrites existing configuration
|
||||
i.SetPluginConfiguration("plugin2", map[string]interface{}{"key3": "value3"})
|
||||
|
||||
assert.Equal(t, map[string]map[string]interface{}{
|
||||
"plugin1": {"key1": "value1"},
|
||||
"plugin2": {"key3": "value3"},
|
||||
}, i.GetAllPluginConfiguration())
|
||||
}
|
||||
@@ -6,9 +6,12 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/providers/env"
|
||||
"github.com/knadh/koanf/providers/posflag"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
@@ -19,20 +22,45 @@ type flagStruct struct {
|
||||
nobrowser bool
|
||||
}
|
||||
|
||||
var flags flagStruct
|
||||
var (
|
||||
flags flagStruct
|
||||
|
||||
homeDir, _ = os.UserHomeDir()
|
||||
|
||||
defaultConfigLocations = []string{
|
||||
"config.yml",
|
||||
filepath.Join(homeDir, ".stash", "config.yml"),
|
||||
}
|
||||
|
||||
// map of env vars to config keys
|
||||
envBinds = map[string]string{
|
||||
"host": Host,
|
||||
"port": Port,
|
||||
"external_host": ExternalHost,
|
||||
"generated": Generated,
|
||||
"metadata": Metadata,
|
||||
"blobs": BlobsPath,
|
||||
"cache": Cache,
|
||||
"stash": Stash,
|
||||
"ui": UILocation,
|
||||
}
|
||||
)
|
||||
|
||||
var errConfigNotFound = errors.New("config file not found")
|
||||
|
||||
func init() {
|
||||
pflag.IP("host", net.IPv4(0, 0, 0, 0), "ip address for the host")
|
||||
pflag.Int("port", 9999, "port to serve from")
|
||||
pflag.StringVarP(&flags.configFilePath, "config", "c", "", "config file to use")
|
||||
pflag.BoolVar(&flags.nobrowser, "nobrowser", false, "Don't open a browser window after launch")
|
||||
pflag.StringP("ui-location", "u", "", "path to the webui")
|
||||
}
|
||||
|
||||
// Called at startup
|
||||
func Initialize() (*Config, error) {
|
||||
cfg := &Config{
|
||||
main: viper.New(),
|
||||
overrides: viper.New(),
|
||||
main: koanf.New("."),
|
||||
overrides: koanf.New("."),
|
||||
}
|
||||
|
||||
cfg.initOverrides()
|
||||
@@ -75,45 +103,49 @@ func Initialize() (*Config, error) {
|
||||
// Called by tests to initialize an empty config
|
||||
func InitializeEmpty() *Config {
|
||||
cfg := &Config{
|
||||
main: viper.New(),
|
||||
overrides: viper.New(),
|
||||
main: koanf.New("."),
|
||||
overrides: koanf.New("."),
|
||||
}
|
||||
instance = cfg
|
||||
return instance
|
||||
}
|
||||
|
||||
func bindEnv(v *viper.Viper, key string) {
|
||||
if err := v.BindEnv(key); err != nil {
|
||||
panic(fmt.Sprintf("unable to set environment key (%v): %v", key, err))
|
||||
func (i *Config) loadFromCommandLine() {
|
||||
v := i.overrides
|
||||
|
||||
if err := v.Load(posflag.ProviderWithFlag(pflag.CommandLine, ".", v, func(f *pflag.Flag) (string, interface{}) {
|
||||
// ignore flags that have not been changed
|
||||
if !f.Changed {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return f.Name, posflag.FlagVal(pflag.CommandLine, f)
|
||||
}), nil); err != nil {
|
||||
logger.Errorf("failed to load flags: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Config) loadFromEnv() {
|
||||
v := i.overrides
|
||||
|
||||
if err := v.Load(env.ProviderWithValue("STASH_", ".", func(key, value string) (string, interface{}) {
|
||||
key = strings.ToLower(strings.TrimPrefix(key, "STASH_"))
|
||||
if newKey, ok := envBinds[key]; ok {
|
||||
return newKey, value
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}), nil); err != nil {
|
||||
logger.Errorf("failed to load envs: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Config) initOverrides() {
|
||||
v := i.overrides
|
||||
|
||||
if err := v.BindPFlags(pflag.CommandLine); err != nil {
|
||||
logger.Infof("failed to bind flags: %v", err)
|
||||
}
|
||||
|
||||
v.SetEnvPrefix("stash") // will be uppercased automatically
|
||||
bindEnv(v, "host") // STASH_HOST
|
||||
bindEnv(v, "port") // STASH_PORT
|
||||
bindEnv(v, "external_host") // STASH_EXTERNAL_HOST
|
||||
bindEnv(v, "generated") // STASH_GENERATED
|
||||
bindEnv(v, "metadata") // STASH_METADATA
|
||||
bindEnv(v, "cache") // STASH_CACHE
|
||||
bindEnv(v, "stash") // STASH_STASH
|
||||
i.loadFromCommandLine()
|
||||
i.loadFromEnv()
|
||||
}
|
||||
|
||||
func (i *Config) initConfig() error {
|
||||
v := i.main
|
||||
|
||||
// The config file is called config. Leave off the file extension.
|
||||
v.SetConfigName("config")
|
||||
|
||||
v.AddConfigPath(".") // Look for config in the working directory
|
||||
v.AddConfigPath(filepath.FromSlash("$HOME/.stash")) // Look for the config in the home directory
|
||||
|
||||
configFile := ""
|
||||
envConfigFile := os.Getenv("STASH_CONFIG_FILE")
|
||||
|
||||
@@ -124,11 +156,10 @@ func (i *Config) initConfig() error {
|
||||
}
|
||||
|
||||
if configFile != "" {
|
||||
v.SetConfigFile(configFile)
|
||||
|
||||
// if file does not exist, assume it is a new system
|
||||
if exists, _ := fsutil.FileExists(configFile); !exists {
|
||||
i.isNewSystem = true
|
||||
i.SetConfigFile(configFile)
|
||||
|
||||
// ensure we can write to the file
|
||||
if err := fsutil.Touch(configFile); err != nil {
|
||||
@@ -139,18 +170,33 @@ func (i *Config) initConfig() error {
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
// load from provided config file
|
||||
if err := i.loadFirstFromFiles([]string{configFile}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// load from default locations
|
||||
if err := i.loadFirstFromFiles(defaultConfigLocations); err != nil {
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
i.isNewSystem = true
|
||||
return nil
|
||||
}
|
||||
|
||||
err := v.ReadInConfig() // Find and read the config file
|
||||
// if not found, assume its a new system
|
||||
var notFoundErr viper.ConfigFileNotFoundError
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
i.isNewSystem = true
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Config) loadFirstFromFiles(f []string) error {
|
||||
for _, ff := range f {
|
||||
if exists, _ := fsutil.FileExists(ff); exists {
|
||||
return i.load(ff)
|
||||
}
|
||||
}
|
||||
|
||||
return errConfigNotFound
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// HACK: viper changes map keys to case insensitive values, so the workaround is to
|
||||
// convert the map to use snake-case keys
|
||||
|
||||
// toSnakeCase converts a string from camelCase to snake_case
|
||||
// NOTE: a double capital will be converted in a way that will yield a different result
|
||||
// when converted back to camel case.
|
||||
// For example: someIDs => some_ids => someIds
|
||||
func toSnakeCase(v string) string {
|
||||
var buf bytes.Buffer
|
||||
underscored := false
|
||||
for i, c := range v {
|
||||
if !underscored && unicode.IsUpper(c) && i > 0 {
|
||||
buf.WriteByte('_')
|
||||
underscored = true
|
||||
} else {
|
||||
underscored = false
|
||||
}
|
||||
|
||||
buf.WriteRune(unicode.ToLower(c))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// fromSnakeCase converts a string from snake_case to camelCase
|
||||
func fromSnakeCase(v string) string {
|
||||
var buf bytes.Buffer
|
||||
leadingUnderscore := true
|
||||
capvar := false
|
||||
for i, c := range v {
|
||||
switch {
|
||||
case c == '_' && !leadingUnderscore && i > 0:
|
||||
capvar = true
|
||||
case c == '_' && leadingUnderscore:
|
||||
buf.WriteRune(c)
|
||||
case capvar:
|
||||
buf.WriteRune(unicode.ToUpper(c))
|
||||
capvar = false
|
||||
default:
|
||||
leadingUnderscore = false
|
||||
buf.WriteRune(c)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// fromSnakeCaseMap recursively converts a map using snake_case keys to camelCase keys
|
||||
func fromSnakeCaseMap(m map[string]interface{}) map[string]interface{} {
|
||||
return fromSnakeCaseValue(m).(map[string]interface{})
|
||||
}
|
||||
|
||||
func fromSnakeCaseValue(val interface{}) interface{} {
|
||||
switch v := val.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
ret := cast.ToStringMap(v)
|
||||
for k, vv := range ret {
|
||||
adjKey := fromSnakeCase(k)
|
||||
ret[adjKey] = fromSnakeCaseValue(vv)
|
||||
}
|
||||
return ret
|
||||
case map[string]interface{}:
|
||||
ret := make(map[string]interface{})
|
||||
for k, vv := range v {
|
||||
adjKey := fromSnakeCase(k)
|
||||
ret[adjKey] = fromSnakeCaseValue(vv)
|
||||
}
|
||||
return ret
|
||||
case []interface{}:
|
||||
ret := make([]interface{}, len(v))
|
||||
for i, vv := range v {
|
||||
ret[i] = fromSnakeCaseValue(vv)
|
||||
}
|
||||
return ret
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// toSnakeCaseMap recursively converts a map using camelCase keys to snake_case keys
|
||||
func toSnakeCaseMap(m map[string]interface{}) map[string]interface{} {
|
||||
return toSnakeCaseValue(m).(map[string]interface{})
|
||||
}
|
||||
|
||||
func toSnakeCaseValue(val interface{}) interface{} {
|
||||
switch v := val.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
ret := cast.ToStringMap(v)
|
||||
for k, vv := range ret {
|
||||
adjKey := toSnakeCase(k)
|
||||
ret[adjKey] = toSnakeCaseValue(vv)
|
||||
}
|
||||
return ret
|
||||
case map[string]interface{}:
|
||||
ret := make(map[string]interface{})
|
||||
for k, vv := range v {
|
||||
adjKey := toSnakeCase(k)
|
||||
ret[adjKey] = toSnakeCaseValue(vv)
|
||||
}
|
||||
return ret
|
||||
case []interface{}:
|
||||
ret := make([]interface{}, len(v))
|
||||
for i, vv := range v {
|
||||
ret[i] = toSnakeCaseValue(vv)
|
||||
}
|
||||
return ret
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_toSnakeCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
v string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"basic",
|
||||
"basic",
|
||||
"basic",
|
||||
},
|
||||
{
|
||||
"two words",
|
||||
"twoWords",
|
||||
"two_words",
|
||||
},
|
||||
{
|
||||
"three word value",
|
||||
"threeWordValue",
|
||||
"three_word_value",
|
||||
},
|
||||
{
|
||||
"snake case",
|
||||
"snake_case",
|
||||
"snake_case",
|
||||
},
|
||||
{
|
||||
"double capital",
|
||||
"doubleCApital",
|
||||
"double_capital",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := toSnakeCase(tt.v); got != tt.want {
|
||||
t.Errorf("toSnakeCase() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fromSnakeCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
v string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"basic",
|
||||
"basic",
|
||||
"basic",
|
||||
},
|
||||
{
|
||||
"two words",
|
||||
"two_words",
|
||||
"twoWords",
|
||||
},
|
||||
{
|
||||
"three word value",
|
||||
"three_word_value",
|
||||
"threeWordValue",
|
||||
},
|
||||
{
|
||||
"camel case",
|
||||
"camelCase",
|
||||
"camelCase",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := fromSnakeCase(tt.v); got != tt.want {
|
||||
t.Errorf("fromSnakeCase() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package config
|
||||
|
||||
type ScanMetadataOptions struct {
|
||||
// Forces a rescan on files even if they have not changed
|
||||
Rescan bool `json:"rescan"`
|
||||
// Generate scene covers during scan
|
||||
ScanGenerateCovers bool `json:"scanGenerateCovers"`
|
||||
// Generate previews during scan
|
||||
|
||||
@@ -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{},
|
||||
}
|
||||
@@ -192,7 +198,6 @@ func (s *Manager) postInit(ctx context.Context) error {
|
||||
s.RefreshScraperCache()
|
||||
s.RefreshScraperSourceManager()
|
||||
|
||||
s.RefreshStreamManager()
|
||||
s.RefreshDLNA()
|
||||
|
||||
s.SetBlobStoreOptions()
|
||||
@@ -239,9 +244,8 @@ func (s *Manager) postInit(ctx context.Context) error {
|
||||
logger.Info("Using HTTP proxy")
|
||||
}
|
||||
|
||||
if err := s.initFFmpeg(ctx); err != nil {
|
||||
return fmt.Errorf("error initializing FFmpeg subsystem: %v", err)
|
||||
}
|
||||
s.RefreshFFMpeg(ctx)
|
||||
s.RefreshStreamManager()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -260,41 +264,55 @@ func (s *Manager) writeStashIcon() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Manager) initFFmpeg(ctx context.Context) error {
|
||||
func (s *Manager) RefreshFFMpeg(ctx context.Context) {
|
||||
// use same directory as config path
|
||||
configDirectory := s.Config.GetConfigPath()
|
||||
paths := []string{
|
||||
configDirectory,
|
||||
paths.GetStashHomeDirectory(),
|
||||
}
|
||||
ffmpegPath, ffprobePath := ffmpeg.GetPaths(paths)
|
||||
// executing binaries requires directory to be included
|
||||
// https://pkg.go.dev/os/exec#hdr-Executables_in_the_current_directory
|
||||
configDirectory := s.Config.GetConfigPathAbs()
|
||||
stashHomeDir := paths.GetStashHomeDirectory()
|
||||
|
||||
if ffmpegPath == "" || ffprobePath == "" {
|
||||
logger.Infof("couldn't find FFmpeg, attempting to download it")
|
||||
if err := ffmpeg.Download(ctx, configDirectory); err != nil {
|
||||
path, absErr := filepath.Abs(configDirectory)
|
||||
if absErr != nil {
|
||||
path = configDirectory
|
||||
}
|
||||
msg := `Unable to automatically download FFmpeg
|
||||
// prefer the configured paths
|
||||
ffmpegPath := s.Config.GetFFMpegPath()
|
||||
ffprobePath := s.Config.GetFFProbePath()
|
||||
|
||||
Check the readme for download links.
|
||||
The ffmpeg and ffprobe binaries should be placed in %s.
|
||||
|
||||
`
|
||||
logger.Errorf(msg, path)
|
||||
return err
|
||||
} else {
|
||||
// After download get new paths for ffmpeg and ffprobe
|
||||
ffmpegPath, ffprobePath = ffmpeg.GetPaths(paths)
|
||||
// ensure the paths are valid
|
||||
if ffmpegPath != "" {
|
||||
// path was set explicitly
|
||||
if err := ffmpeg.ValidateFFMpeg(ffmpegPath); err != nil {
|
||||
logger.Errorf("invalid ffmpeg path: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ffmpeg.ValidateFFMpegCodecSupport(ffmpegPath); err != nil {
|
||||
logger.Warn(err)
|
||||
}
|
||||
} else {
|
||||
ffmpegPath = ffmpeg.ResolveFFMpeg(configDirectory, stashHomeDir)
|
||||
}
|
||||
|
||||
s.FFMpeg = ffmpeg.NewEncoder(ffmpegPath)
|
||||
s.FFProbe = ffmpeg.FFProbe(ffprobePath)
|
||||
if ffprobePath != "" {
|
||||
if err := ffmpeg.ValidateFFProbe(ffmpegPath); err != nil {
|
||||
logger.Errorf("invalid ffprobe path: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ffprobePath = ffmpeg.ResolveFFProbe(configDirectory, stashHomeDir)
|
||||
}
|
||||
|
||||
s.FFMpeg.InitHWSupport(ctx)
|
||||
s.RefreshStreamManager()
|
||||
if ffmpegPath == "" {
|
||||
logger.Warn("Couldn't find FFmpeg")
|
||||
}
|
||||
if ffprobePath == "" {
|
||||
logger.Warn("Couldn't find FFProbe")
|
||||
}
|
||||
|
||||
return nil
|
||||
if ffmpegPath != "" && ffprobePath != "" {
|
||||
logger.Debugf("using ffmpeg: %s", ffmpegPath)
|
||||
logger.Debugf("using ffprobe: %s", ffprobePath)
|
||||
|
||||
s.FFMpeg = ffmpeg.NewEncoder(ffmpegPath)
|
||||
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
|
||||
}
|
||||
@@ -245,7 +248,7 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
||||
}
|
||||
}
|
||||
|
||||
s.Config.Set(config.Generated, input.GeneratedLocation)
|
||||
s.Config.SetString(config.Generated, input.GeneratedLocation)
|
||||
}
|
||||
|
||||
// create the cache directory if it does not exist
|
||||
@@ -256,11 +259,11 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Set(config.Cache, input.CacheLocation)
|
||||
cfg.SetString(config.Cache, input.CacheLocation)
|
||||
}
|
||||
|
||||
if input.StoreBlobsInDatabase {
|
||||
cfg.Set(config.BlobsStorage, config.BlobStorageTypeDatabase)
|
||||
cfg.SetInterface(config.BlobsStorage, config.BlobStorageTypeDatabase)
|
||||
} else {
|
||||
if !cfg.HasOverride(config.BlobsPath) {
|
||||
if exists, _ := fsutil.DirExists(input.BlobsLocation); !exists {
|
||||
@@ -269,18 +272,18 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Set(config.BlobsPath, input.BlobsLocation)
|
||||
cfg.SetString(config.BlobsPath, input.BlobsLocation)
|
||||
}
|
||||
|
||||
cfg.Set(config.BlobsStorage, config.BlobStorageTypeFilesystem)
|
||||
cfg.SetInterface(config.BlobsStorage, config.BlobStorageTypeFilesystem)
|
||||
}
|
||||
|
||||
// set the configuration
|
||||
if !cfg.HasOverride(config.Database) {
|
||||
cfg.Set(config.Database, input.DatabaseFile)
|
||||
cfg.SetString(config.Database, input.DatabaseFile)
|
||||
}
|
||||
|
||||
cfg.Set(config.Stash, input.Stashes)
|
||||
cfg.SetInterface(config.Stash, input.Stashes)
|
||||
|
||||
if err := cfg.Write(); err != nil {
|
||||
return fmt.Errorf("error writing configuration file: %v", err)
|
||||
@@ -297,58 +300,12 @@ 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
|
||||
}
|
||||
|
||||
func (s *Manager) Migrate(ctx context.Context, input MigrateInput) error {
|
||||
database := s.Database
|
||||
|
||||
// always backup so that we can roll back to the previous version if
|
||||
// migration fails
|
||||
backupPath := input.BackupPath
|
||||
if backupPath == "" {
|
||||
backupPath = database.DatabaseBackupPath(s.Config.GetBackupDirectoryPath())
|
||||
} else {
|
||||
// check if backup path is a filename or path
|
||||
// filename goes into backup directory, path is kept as is
|
||||
filename := filepath.Base(backupPath)
|
||||
if backupPath == filename {
|
||||
backupPath = filepath.Join(s.Config.GetBackupDirectoryPathOrDefault(), filename)
|
||||
}
|
||||
}
|
||||
|
||||
// perform database backup
|
||||
if err := database.Backup(backupPath); err != nil {
|
||||
return fmt.Errorf("error backing up database: %s", err)
|
||||
}
|
||||
|
||||
if err := database.RunMigrations(); err != nil {
|
||||
errStr := fmt.Sprintf("error performing migration: %s", err)
|
||||
|
||||
// roll back to the backed up version
|
||||
restoreErr := database.RestoreFromBackup(backupPath)
|
||||
if restoreErr != nil {
|
||||
errStr = fmt.Sprintf("ERROR: unable to restore database from backup after migration failure: %s\n%s", restoreErr.Error(), errStr)
|
||||
} else {
|
||||
errStr = "An error occurred migrating the database to the latest schema version. The backup database file was automatically renamed to restore the database.\n" + errStr
|
||||
}
|
||||
|
||||
return errors.New(errStr)
|
||||
}
|
||||
|
||||
// if no backup path was provided, then delete the created backup
|
||||
if input.BackupPath == "" {
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
logger.Warnf("error removing unwanted database backup (%s): %s", backupPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Manager) BackupDatabase(download bool) (string, string, error) {
|
||||
var backupPath string
|
||||
var backupName string
|
||||
@@ -437,6 +394,16 @@ func (s *Manager) GetSystemStatus() *SystemStatus {
|
||||
|
||||
configFile := s.Config.GetConfigFile()
|
||||
|
||||
ffmpegPath := ""
|
||||
if s.FFMpeg != nil {
|
||||
ffmpegPath = s.FFMpeg.Path()
|
||||
}
|
||||
|
||||
ffprobePath := ""
|
||||
if s.FFProbe != nil {
|
||||
ffprobePath = s.FFProbe.Path()
|
||||
}
|
||||
|
||||
return &SystemStatus{
|
||||
Os: runtime.GOOS,
|
||||
WorkingDir: workingDir,
|
||||
@@ -446,6 +413,8 @@ func (s *Manager) GetSystemStatus() *SystemStatus {
|
||||
AppSchema: appSchema,
|
||||
Status: status,
|
||||
ConfigPath: &configFile,
|
||||
FfmpegPath: &ffmpegPath,
|
||||
FfprobePath: &ffprobePath,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ func (s *Manager) Import(ctx context.Context) (int, error) {
|
||||
return 0, errors.New("metadata path must be set in config")
|
||||
}
|
||||
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
task := ImportTask{
|
||||
repository: s.Repository,
|
||||
resetter: s.Database,
|
||||
@@ -147,6 +147,9 @@ func (s *Manager) Import(ctx context.Context) (int, error) {
|
||||
fileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
}
|
||||
task.Start(ctx)
|
||||
|
||||
// TODO - return error from task
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, "Importing...", j), nil
|
||||
@@ -159,7 +162,7 @@ func (s *Manager) Export(ctx context.Context) (int, error) {
|
||||
return 0, errors.New("metadata path must be set in config")
|
||||
}
|
||||
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
task := ExportTask{
|
||||
@@ -168,6 +171,8 @@ func (s *Manager) Export(ctx context.Context) (int, error) {
|
||||
fileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
}
|
||||
task.Start(ctx, &wg)
|
||||
// TODO - return error from task
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, "Exporting...", j), nil
|
||||
@@ -177,9 +182,11 @@ func (s *Manager) RunSingleTask(ctx context.Context, t Task) int {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
t.Start(ctx)
|
||||
wg.Done()
|
||||
defer wg.Done()
|
||||
// TODO - return error from task
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, t.GetDescription(), j)
|
||||
@@ -215,11 +222,10 @@ func (s *Manager) generateScreenshot(ctx context.Context, sceneId string, at *fl
|
||||
logger.Warnf("failure generating screenshot: %v", err)
|
||||
}
|
||||
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
sceneIdInt, err := strconv.Atoi(sceneId)
|
||||
if err != nil {
|
||||
logger.Errorf("Error parsing scene id %s: %v", sceneId, err)
|
||||
return
|
||||
return fmt.Errorf("error parsing scene id %s: %w", sceneId, err)
|
||||
}
|
||||
|
||||
var scene *models.Scene
|
||||
@@ -234,8 +240,7 @@ func (s *Manager) generateScreenshot(ctx context.Context, sceneId string, at *fl
|
||||
|
||||
return scene.LoadPrimaryFile(ctx, s.Repository.File)
|
||||
}); err != nil {
|
||||
logger.Errorf("error finding scene for screenshot generation: %v", err)
|
||||
return
|
||||
return fmt.Errorf("error finding scene for screenshot generation: %w", err)
|
||||
}
|
||||
|
||||
task := GenerateCoverTask{
|
||||
@@ -248,6 +253,9 @@ func (s *Manager) generateScreenshot(ctx context.Context, sceneId string, at *fl
|
||||
task.Start(ctx)
|
||||
|
||||
logger.Infof("Generate screenshot finished")
|
||||
|
||||
// TODO - return error from task
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, fmt.Sprintf("Generating screenshot for scene id %s", sceneId), j)
|
||||
@@ -309,7 +317,7 @@ func (s *Manager) OptimiseDatabase(ctx context.Context) int {
|
||||
}
|
||||
|
||||
func (s *Manager) MigrateHash(ctx context.Context) int {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm()
|
||||
logger.Infof("Migrating generated files for %s naming hash", fileNamingAlgo.String())
|
||||
|
||||
@@ -319,8 +327,7 @@ func (s *Manager) MigrateHash(ctx context.Context) int {
|
||||
scenes, err = s.Repository.Scene.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
logger.Errorf("failed to fetch list of scenes for migration: %s", err.Error())
|
||||
return
|
||||
return fmt.Errorf("failed to fetch list of scenes for migration: %w", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -331,7 +338,7 @@ func (s *Manager) MigrateHash(ctx context.Context) int {
|
||||
progress.Increment()
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if scene == nil {
|
||||
@@ -351,6 +358,7 @@ func (s *Manager) MigrateHash(ctx context.Context) int {
|
||||
}
|
||||
|
||||
logger.Info("Finished migrating")
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, "Migrating scene hashes...", j)
|
||||
@@ -358,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
|
||||
@@ -380,17 +389,10 @@ type StashBoxBatchTagInput struct {
|
||||
PerformerNames []string `json:"performer_names"`
|
||||
}
|
||||
|
||||
func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxBatchTagInput) int {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
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) {
|
||||
logger.Error(fmt.Errorf("invalid stash_box_index %d", input.Endpoint))
|
||||
return
|
||||
}
|
||||
box := boxes[input.Endpoint]
|
||||
|
||||
var tasks []StashBoxBatchTagTask
|
||||
|
||||
// The gocritic linter wants to turn this ifElseChain into a switch.
|
||||
@@ -435,7 +437,7 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxB
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
} else if len(input.Names) > 0 || len(input.PerformerNames) > 0 {
|
||||
// The user is batch adding performers
|
||||
@@ -493,13 +495,12 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxB
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Error(err.Error())
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
progress.SetTotal(len(tasks))
|
||||
@@ -513,22 +514,17 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxB
|
||||
|
||||
progress.Increment()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, "Batch stash-box performer tag...", j)
|
||||
}
|
||||
|
||||
func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, input StashBoxBatchTagInput) int {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
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) {
|
||||
logger.Error(fmt.Errorf("invalid stash_box_index %d", input.Endpoint))
|
||||
return
|
||||
}
|
||||
box := boxes[input.Endpoint]
|
||||
|
||||
var tasks []StashBoxBatchTagTask
|
||||
|
||||
// The gocritic linter wants to turn this ifElseChain into a switch.
|
||||
@@ -620,13 +616,12 @@ func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, input StashBoxBatc
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Error(err.Error())
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
progress.SetTotal(len(tasks))
|
||||
@@ -640,6 +635,8 @@ func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, input StashBoxBatc
|
||||
|
||||
progress.Increment()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, "Batch stash-box studio tag...", j)
|
||||
|
||||
@@ -13,6 +13,8 @@ type SystemStatus struct {
|
||||
Os string `json:"os"`
|
||||
WorkingDir string `json:"working_dir"`
|
||||
HomeDir string `json:"home_dir"`
|
||||
FfmpegPath *string `json:"ffmpegPath"`
|
||||
FfprobePath *string `json:"ffprobePath"`
|
||||
}
|
||||
|
||||
type SetupInput struct {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user