mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
218 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a373a25ca | ||
|
|
e116775d60 | ||
|
|
c5bafeb15c | ||
|
|
205b24499b | ||
|
|
48035061ec | ||
|
|
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 | ||
|
|
7733a214d3 | ||
|
|
cd2f0922ab | ||
|
|
f1f6e84aa0 | ||
|
|
22986097c4 | ||
|
|
409a200ebc | ||
|
|
20ac388f77 | ||
|
|
0626a7aea1 | ||
|
|
2ca9e0f43a | ||
|
|
b4823bec8a | ||
|
|
945188a0ba | ||
|
|
b59afd2dcd | ||
|
|
9202787be0 | ||
|
|
4999e85fae | ||
|
|
2bdf0d9e62 | ||
|
|
2e00cb6c5a | ||
|
|
33857122b8 | ||
|
|
768f74a0b3 | ||
|
|
98c428ba4e | ||
|
|
fcf249e5f6 | ||
|
|
3a56dd98db | ||
|
|
48c287ed76 | ||
|
|
9c6fbfc16f | ||
|
|
6a9175c954 | ||
|
|
56896d7c7d | ||
|
|
2e35221003 | ||
|
|
ba1ebba6c0 | ||
|
|
4a3ce8b6ec | ||
|
|
4b84ec0d85 | ||
|
|
a302fc78ea | ||
|
|
f2bc3d5567 | ||
|
|
a303446bb7 | ||
|
|
0c2a2190e5 | ||
|
|
a8c909e0c9 | ||
|
|
c4a91d15a6 | ||
|
|
61bd9233b2 | ||
|
|
37acd6b79b | ||
|
|
5bb9bf902c | ||
|
|
76e5598876 | ||
|
|
8b1d4ccc97 | ||
|
|
cff068f519 | ||
|
|
276bc5a8cb | ||
|
|
b4a6cc43d1 | ||
|
|
777fb44ac6 | ||
|
|
f5a42ede2d | ||
|
|
7bb38ae6dc | ||
|
|
7d56f1a093 | ||
|
|
afd7f02644 | ||
|
|
93b851eae6 | ||
|
|
1dfb960a87 | ||
|
|
e231812203 | ||
|
|
e7f610ce18 | ||
|
|
6e9718a600 | ||
|
|
6fb1c41ae9 | ||
|
|
5aba3c1a98 | ||
|
|
440c261f5b | ||
|
|
8fc997dfe9 | ||
|
|
5b9bdadaec | ||
|
|
706b61233f | ||
|
|
aaf3114194 | ||
|
|
15aac68a14 | ||
|
|
dad4ab6a6f | ||
|
|
e9703e9a6e | ||
|
|
46eb01198a | ||
|
|
235c9c90c2 | ||
|
|
a4bbdcfbae | ||
|
|
8c410a9a14 | ||
|
|
9981574e82 | ||
|
|
79e72ff3bc | ||
|
|
a16f3da33e | ||
|
|
8770e81ec5 | ||
|
|
9284ede0fb | ||
|
|
2d73912f15 | ||
|
|
9ac6505241 | ||
|
|
a402ee5fa7 | ||
|
|
a8df95c3a4 | ||
|
|
330581283a | ||
|
|
892d74c98b | ||
|
|
de2b28d3f9 | ||
|
|
217c02f181 | ||
|
|
3ea31aeb76 | ||
|
|
cf8efa9035 | ||
|
|
1d0fa27c71 | ||
|
|
0b82dbf666 | ||
|
|
11cafe933a | ||
|
|
d82c526ada | ||
|
|
1588d1cb4e | ||
|
|
64f2071d8c | ||
|
|
3573795cf7 | ||
|
|
723211a620 | ||
|
|
dd8da7f339 | ||
|
|
e7311a60d2 | ||
|
|
29677696fd | ||
|
|
403f7c54ef | ||
|
|
75099b38a8 | ||
|
|
45e2e12594 | ||
|
|
ec547e8d30 | ||
|
|
e470dc5f52 | ||
|
|
14bde44597 | ||
|
|
aeb68a5851 | ||
|
|
5cf28cf8af | ||
|
|
08b73581a6 | ||
|
|
95a2c8d13f | ||
|
|
0b131f76df | ||
|
|
6271f18979 | ||
|
|
ca976a0994 | ||
|
|
9859ec61fb | ||
|
|
a998497004 | ||
|
|
f5e3fe77b7 | ||
|
|
743ab9a52c | ||
|
|
d23cecfc18 | ||
|
|
d8990e655d | ||
|
|
5b9a96b843 | ||
|
|
b968aa3f31 | ||
|
|
910c7025dc | ||
|
|
ea503833c5 | ||
|
|
6848dec5f4 | ||
|
|
bd7d4ac7ff | ||
|
|
5a6504b4ba | ||
|
|
f8a93789bb | ||
|
|
82cbeff9b5 | ||
|
|
f32d60f208 |
@@ -17,7 +17,7 @@
|
||||
|
||||
# GraphQL generated output
|
||||
pkg/models/generated_*.go
|
||||
ui/v2.5/src/core/generated-*.tsx
|
||||
ui/v2.5/src/core/generated-graphql.ts
|
||||
|
||||
####
|
||||
# Jetbrains
|
||||
|
||||
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']
|
||||
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
@@ -92,13 +92,16 @@ 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
|
||||
|
||||
@@ -126,6 +129,14 @@ jobs:
|
||||
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@v2
|
||||
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 +158,7 @@ jobs:
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-linux-arm32v6
|
||||
dist/stash-freebsd
|
||||
dist/stash-ui.zip
|
||||
CHECKSUMS_SHA1
|
||||
|
||||
- name: Master release
|
||||
@@ -166,6 +178,7 @@ jobs:
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-linux-arm32v6
|
||||
dist/stash-freebsd
|
||||
dist/stash-ui.zip
|
||||
CHECKSUMS_SHA1
|
||||
gzip: false
|
||||
|
||||
|
||||
23
Makefile
23
Makefile
@@ -48,6 +48,11 @@ GO_BUILD_TAGS += sqlite_stat4 sqlite_math_functions
|
||||
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
# define COMPILER_IMAGE for cross-compilation docker container
|
||||
ifndef COMPILER_IMAGE
|
||||
COMPILER_IMAGE := stashapp/compiler:latest
|
||||
endif
|
||||
|
||||
.PHONY: release
|
||||
release: pre-ui generate ui build-release
|
||||
|
||||
@@ -348,6 +353,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
|
||||
@@ -378,3 +388,16 @@ docker-build: build-info
|
||||
.PHONY: docker-cuda-build
|
||||
docker-cuda-build: build-info
|
||||
docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/cuda-build -f docker/build/x86_64/Dockerfile-CUDA .
|
||||
|
||||
# start the build container - for cross compilation
|
||||
# this is adapted from the github actions build.yml file
|
||||
.PHONY: start-compiler-container
|
||||
start-compiler-container:
|
||||
docker run -d --name build --mount type=bind,source="$(PWD)",target=/stash,consistency=delegated $(EXTRA_CONTAINER_ARGS) -w /stash $(COMPILER_IMAGE) tail -f /dev/null
|
||||
|
||||
# run the cross-compilation using
|
||||
# docker exec -t build /bin/bash -c "make build-cc-<platform>"
|
||||
|
||||
.PHONY: remove-compiler-container
|
||||
remove-compiler-container:
|
||||
docker rm -f -v build
|
||||
14
README.md
14
README.md
@@ -24,9 +24,11 @@ For further information you can consult the [documentation](https://docs.stashap
|
||||
|
||||
# Installing Stash
|
||||
|
||||
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> MacOS| <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
|
||||
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS | <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
|
||||
:---:|:---:|:---:|:---:
|
||||
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release (Apple Silicon)](https://github.com/stashapp/stash/releases/latest/download/stash-macos-applesilicon) <br />[Latest Release (Intel)](https://github.com/stashapp/stash/releases/latest/download/stash-macos-intel) <br /><sup><sub>[Development Preview (Universal)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-macos)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub> [Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
|
||||
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release](https://github.com/stashapp/stash/releases/latest/download/Stash.app.zip) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/Stash.app.zip)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub>[Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
|
||||
|
||||
Download links for other platforms and architectures are available on the [Releases page](https://github.com/stashapp/stash/releases).
|
||||
|
||||
## First Run
|
||||
|
||||
@@ -46,9 +48,11 @@ 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>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
@@ -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,7 +77,7 @@ 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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -11,9 +11,14 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-linux-arm32v6; \
|
||||
|
||||
FROM --platform=$TARGETPLATFORM alpine:latest AS app
|
||||
COPY --from=binary /stash /usr/bin/
|
||||
|
||||
# vips version 8.15.0-r0 breaks thumbnail generation on arm32v6
|
||||
# need to use 8.14.3-r0 from alpine 3.18 instead
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps gcc python3-dev musl-dev \
|
||||
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg vips-tools ruby tzdata \
|
||||
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx \
|
||||
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg ruby tzdata \
|
||||
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.18/community vips=8.14.3-r0 vips-tools=8.14.3-r0 \
|
||||
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx stashapp-tools \
|
||||
&& gem install faraday \
|
||||
&& apk del .build-deps
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
@@ -9,11 +9,11 @@ https://docs.docker.com/engine/install/
|
||||
|
||||
### Get the docker-compose.yml file
|
||||
|
||||
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml) in the repository, or if you have curl, you can make your Linux console do it for you:
|
||||
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/develop/docker/production/docker-compose.yml) in the repository, or if you have curl, you can make your Linux console do it for you:
|
||||
|
||||
```
|
||||
mkdir stashapp && cd stashapp
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/develop/docker/production/docker-compose.yml
|
||||
```
|
||||
|
||||
Once you have that file where you want it, modify the settings as you please, and then run:
|
||||
|
||||
23
go.mod
23
go.mod
@@ -8,11 +8,12 @@ require (
|
||||
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/cors v1.2.1
|
||||
@@ -30,17 +31,16 @@ require (
|
||||
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/natefinch/pie v0.0.0-20170715172608-9a0d72014007
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cast v1.5.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.16.0
|
||||
github.com/vearutop/statigz v1.4.0
|
||||
@@ -49,12 +49,12 @@ require (
|
||||
github.com/vektra/mockery/v2 v2.10.0
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
|
||||
github.com/zencoder/go-dash/v3 v3.0.2
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/image v0.12.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/term v0.13.0
|
||||
golang.org/x/text v0.13.0
|
||||
golang.org/x/net v0.23.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/term v0.18.0
|
||||
golang.org/x/text v0.14.0
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
@@ -67,11 +67,14 @@ require (
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // 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/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
@@ -83,8 +86,10 @@ require (
|
||||
github.com/matryer/moq v0.2.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mitchellh/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
|
||||
@@ -97,6 +102,7 @@ require (
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cobra v1.7.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.16.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
@@ -107,6 +113,5 @@ require (
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
142
go.sum
142
go.sum
@@ -70,6 +70,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=
|
||||
@@ -94,10 +95,20 @@ 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=
|
||||
@@ -118,8 +129,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=
|
||||
@@ -142,6 +156,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
|
||||
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/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 +165,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 +192,9 @@ 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/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
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=
|
||||
@@ -187,11 +213,17 @@ 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-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-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=
|
||||
@@ -244,6 +276,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,6 +293,7 @@ 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.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
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=
|
||||
@@ -281,6 +315,8 @@ 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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
@@ -296,9 +332,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 +344,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,12 +355,17 @@ 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=
|
||||
@@ -336,21 +381,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/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/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 +416,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,15 +424,21 @@ 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/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=
|
||||
@@ -414,16 +479,25 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
|
||||
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 +507,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,21 +543,27 @@ 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/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
@@ -490,6 +576,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
||||
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/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=
|
||||
@@ -501,6 +589,7 @@ github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5K
|
||||
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=
|
||||
@@ -582,8 +671,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 +706,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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/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=
|
||||
@@ -711,8 +803,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
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.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
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=
|
||||
@@ -749,9 +841,11 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
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 +857,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 +877,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 +886,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 +899,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 +917,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=
|
||||
@@ -827,27 +928,30 @@ 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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/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.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/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 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
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/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=
|
||||
@@ -962,6 +1066,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=
|
||||
@@ -1025,9 +1130,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=
|
||||
@@ -1068,10 +1175,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=
|
||||
@@ -1079,14 +1188,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=
|
||||
@@ -1103,3 +1212,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=
|
||||
|
||||
@@ -36,6 +36,8 @@ models:
|
||||
model: github.com/stashapp/stash/internal/api.Timestamp
|
||||
BoolMap:
|
||||
model: github.com/stashapp/stash/internal/api.BoolMap
|
||||
PluginConfigMap:
|
||||
model: github.com/stashapp/stash/internal/api.PluginConfigMap
|
||||
# define to force resolvers
|
||||
Image:
|
||||
model: github.com/stashapp/stash/pkg/models.Image
|
||||
@@ -68,6 +70,8 @@ models:
|
||||
model: github.com/stashapp/stash/internal/manager/config.ConfigDisableDropdownCreate
|
||||
ScanMetadataOptions:
|
||||
model: github.com/stashapp/stash/internal/manager/config.ScanMetadataOptions
|
||||
CleanGeneratedInput:
|
||||
model: github.com/stashapp/stash/internal/manager/task.CleanGeneratedOptions
|
||||
AutoTagMetadataOptions:
|
||||
model: github.com/stashapp/stash/internal/manager/config.AutoTagMetadataOptions
|
||||
SystemStatus:
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
fragment SlimMovieData on Movie {
|
||||
id
|
||||
name
|
||||
front_image_path
|
||||
rating100
|
||||
}
|
||||
@@ -12,7 +12,8 @@ type Query {
|
||||
"A function which queries Scene objects"
|
||||
findScenes(
|
||||
scene_filter: SceneFilterType
|
||||
scene_ids: [Int!]
|
||||
scene_ids: [Int!] @deprecated(reason: "use ids")
|
||||
ids: [ID!]
|
||||
filter: FindFilterType
|
||||
): FindScenesResultType!
|
||||
|
||||
@@ -50,7 +51,8 @@ type Query {
|
||||
"A function which queries Scene objects"
|
||||
findImages(
|
||||
image_filter: ImageFilterType
|
||||
image_ids: [Int!]
|
||||
image_ids: [Int!] @deprecated(reason: "use ids")
|
||||
ids: [ID!]
|
||||
filter: FindFilterType
|
||||
): FindImagesResultType!
|
||||
|
||||
@@ -60,7 +62,8 @@ type Query {
|
||||
findPerformers(
|
||||
performer_filter: PerformerFilterType
|
||||
filter: FindFilterType
|
||||
performer_ids: [Int!]
|
||||
performer_ids: [Int!] @deprecated(reason: "use ids")
|
||||
ids: [ID!]
|
||||
): FindPerformersResultType!
|
||||
|
||||
"Find a studio by ID"
|
||||
@@ -69,6 +72,7 @@ type Query {
|
||||
findStudios(
|
||||
studio_filter: StudioFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindStudiosResultType!
|
||||
|
||||
"Find a movie by ID"
|
||||
@@ -77,18 +81,21 @@ type Query {
|
||||
findMovies(
|
||||
movie_filter: MovieFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindMoviesResultType!
|
||||
|
||||
findGallery(id: ID!): Gallery
|
||||
findGalleries(
|
||||
gallery_filter: GalleryFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindGalleriesResultType!
|
||||
|
||||
findTag(id: ID!): Tag
|
||||
findTags(
|
||||
tag_filter: TagFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindTagsResultType!
|
||||
|
||||
"Retrieve random scene markers for the wall"
|
||||
@@ -197,15 +204,16 @@ type Query {
|
||||
|
||||
# Get everything
|
||||
|
||||
allScenes: [Scene!]!
|
||||
allScenes: [Scene!]! @deprecated(reason: "Use findScenes instead")
|
||||
allSceneMarkers: [SceneMarker!]!
|
||||
allImages: [Image!]!
|
||||
allGalleries: [Gallery!]!
|
||||
allStudios: [Studio!]!
|
||||
allMovies: [Movie!]!
|
||||
allTags: [Tag!]!
|
||||
@deprecated(reason: "Use findSceneMarkers instead")
|
||||
allImages: [Image!]! @deprecated(reason: "Use findImages instead")
|
||||
allGalleries: [Gallery!]! @deprecated(reason: "Use findGalleries instead")
|
||||
|
||||
allPerformers: [Performer!]! @deprecated(reason: "Use findPerformers instead")
|
||||
allPerformers: [Performer!]!
|
||||
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
|
||||
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
|
||||
allMovies: [Movie!]! @deprecated(reason: "Use findMovies instead")
|
||||
|
||||
# Get everything with minimal metadata
|
||||
|
||||
@@ -218,7 +226,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
|
||||
@@ -229,9 +242,15 @@ type Mutation {
|
||||
scenesUpdate(input: [SceneUpdateInput!]!): [Scene]
|
||||
|
||||
"Increments the o-counter for a scene. Returns the new value"
|
||||
sceneIncrementO(id: ID!): Int!
|
||||
sceneIncrementO(id: ID!): Int! @deprecated(reason: "Use sceneAddO instead")
|
||||
"Decrements the o-counter for a scene. Returns the new value"
|
||||
sceneDecrementO(id: ID!): Int!
|
||||
sceneDecrementO(id: ID!): Int! @deprecated(reason: "Use sceneRemoveO instead")
|
||||
|
||||
"Increments the o-counter for a scene. Uses the current time if none provided."
|
||||
sceneAddO(id: ID!, times: [Timestamp!]): HistoryMutationResult!
|
||||
"Decrements the o-counter for a scene, removing the last recorded time if specific time not provided. Returns the new value"
|
||||
sceneDeleteO(id: ID!, times: [Timestamp!]): HistoryMutationResult!
|
||||
|
||||
"Resets the o-counter for a scene to 0. Returns the new value"
|
||||
sceneResetO(id: ID!): Int!
|
||||
|
||||
@@ -240,6 +259,14 @@ type Mutation {
|
||||
|
||||
"Increments the play count for the scene. Returns the new play count value."
|
||||
sceneIncrementPlayCount(id: ID!): Int!
|
||||
@deprecated(reason: "Use sceneAddPlay instead")
|
||||
|
||||
"Increments the play count for the scene. Uses the current time if none provided."
|
||||
sceneAddPlay(id: ID!, times: [Timestamp!]): HistoryMutationResult!
|
||||
"Decrements the play count for the scene, removing the specific times or the last recorded time if not provided."
|
||||
sceneDeletePlay(id: ID!, times: [Timestamp!]): HistoryMutationResult!
|
||||
"Resets the play count for a scene to 0. Returns the new play count value."
|
||||
sceneResetPlayCount(id: ID!): Int!
|
||||
|
||||
"Generates screenshot at specified time in seconds. Leave empty to generate default screenshot"
|
||||
sceneGenerateScreenshot(id: ID!, at: Float): String!
|
||||
@@ -327,12 +354,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"
|
||||
@@ -356,6 +390,8 @@ type Mutation {
|
||||
metadataAutoTag(input: AutoTagMetadataInput!): ID!
|
||||
"Clean metadata. Returns the job ID"
|
||||
metadataClean(input: CleanMetadataInput!): ID!
|
||||
"Clean generated files. Returns the job ID"
|
||||
metadataCleanGenerated(input: CleanGeneratedInput!): ID!
|
||||
"Identifies scenes using scrapers. Returns the job ID"
|
||||
metadataIdentify(input: IdentifyMetadataInput!): ID!
|
||||
|
||||
@@ -381,12 +417,29 @@ type Mutation {
|
||||
"""
|
||||
setPluginsEnabled(enabledMap: BoolMap!): Boolean!
|
||||
|
||||
"Run plugin task. Returns the job ID"
|
||||
"""
|
||||
Run a plugin task.
|
||||
If task_name is provided, then the task must exist in the plugin config and the tasks configuration
|
||||
will be used to run the plugin.
|
||||
If no task_name is provided, then the plugin will be executed with the arguments provided only.
|
||||
Returns the job ID
|
||||
"""
|
||||
runPluginTask(
|
||||
plugin_id: ID!
|
||||
task_name: String!
|
||||
args: [PluginArgInput!]
|
||||
"if provided, then the default args will be applied"
|
||||
task_name: String
|
||||
"displayed in the task queue"
|
||||
description: String
|
||||
args: [PluginArgInput!] @deprecated(reason: "Use args_map instead")
|
||||
args_map: Map
|
||||
): ID!
|
||||
|
||||
"""
|
||||
Runs a plugin operation. The operation is run immediately and does not use the job queue.
|
||||
Returns a map of the result.
|
||||
"""
|
||||
runPluginOperation(plugin_id: ID!, args: Map): Any
|
||||
|
||||
reloadPlugins: Boolean!
|
||||
|
||||
"""
|
||||
|
||||
@@ -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"
|
||||
@@ -535,7 +547,7 @@ type ConfigResult {
|
||||
scraping: ConfigScrapingResult!
|
||||
defaults: ConfigDefaultSettingsResult!
|
||||
ui: Map!
|
||||
plugins(include: [String!]): Map!
|
||||
plugins(include: [ID!]): PluginConfigMap!
|
||||
}
|
||||
|
||||
"Directory structure of a path"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -61,6 +62,19 @@ input ResolutionCriterionInput {
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
enum OrientationEnum {
|
||||
"Landscape"
|
||||
LANDSCAPE
|
||||
"Portrait"
|
||||
PORTRAIT
|
||||
"Square"
|
||||
SQUARE
|
||||
}
|
||||
|
||||
input OrientationCriterionInput {
|
||||
value: [OrientationEnum!]!
|
||||
}
|
||||
|
||||
input PHashDuplicationCriterionInput {
|
||||
duplicated: Boolean
|
||||
"Currently unimplemented"
|
||||
@@ -130,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"
|
||||
@@ -212,8 +228,12 @@ input SceneFilterType {
|
||||
duplicated: PHashDuplicationCriterionInput
|
||||
"Filter by resolution"
|
||||
resolution: ResolutionCriterionInput
|
||||
"Filter by orientation"
|
||||
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"
|
||||
@@ -228,6 +248,8 @@ input SceneFilterType {
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"Filter to only include scenes with this movie"
|
||||
movies: MultiCriterionInput
|
||||
"Filter to only include scenes with this gallery"
|
||||
galleries: MultiCriterionInput
|
||||
"Filter to only include scenes with these tags"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter by tag count"
|
||||
@@ -258,6 +280,8 @@ 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"
|
||||
@@ -306,6 +330,8 @@ input StudioFilterType {
|
||||
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"
|
||||
@@ -316,6 +342,8 @@ input StudioFilterType {
|
||||
url: StringCriterionInput
|
||||
"Filter by studio aliases"
|
||||
aliases: StringCriterionInput
|
||||
"Filter by subsidiary studio count"
|
||||
child_count: IntCriterionInput
|
||||
"Filter by autotag ignore value"
|
||||
ignore_auto_tag: Boolean
|
||||
"Filter by creation time"
|
||||
@@ -351,6 +379,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"
|
||||
@@ -394,6 +424,9 @@ input TagFilterType {
|
||||
"Filter by tag aliases"
|
||||
aliases: StringCriterionInput
|
||||
|
||||
"Filter by favorite"
|
||||
favorite: Boolean
|
||||
|
||||
"Filter by tag description"
|
||||
description: StringCriterionInput
|
||||
|
||||
@@ -465,6 +498,8 @@ input ImageFilterType {
|
||||
o_counter: IntCriterionInput
|
||||
"Filter by resolution"
|
||||
resolution: ResolutionCriterionInput
|
||||
"Filter by orientation"
|
||||
orientation: OrientationCriterionInput
|
||||
"Filter to only include images missing this property"
|
||||
is_missing: String
|
||||
"Filter to only include images with this studio"
|
||||
@@ -481,6 +516,8 @@ input ImageFilterType {
|
||||
performer_count: IntCriterionInput
|
||||
"Filter images that have performers that have been favorited"
|
||||
performer_favorite: Boolean
|
||||
"Filter images by performer age at time of image"
|
||||
performer_age: IntCriterionInput
|
||||
"Filter to only include images with these galleries"
|
||||
galleries: MultiCriterionInput
|
||||
"Filter by creation time"
|
||||
@@ -545,6 +582,7 @@ input MultiCriterionInput {
|
||||
|
||||
input GenderCriterionInput {
|
||||
value: GenderEnum
|
||||
value_list: [GenderEnum!]
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
"Log entries"
|
||||
scalar Time
|
||||
|
||||
enum LogLevel {
|
||||
Trace
|
||||
Debug
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
scalar Upload
|
||||
|
||||
input GenerateMetadataInput {
|
||||
covers: Boolean
|
||||
sprites: Boolean
|
||||
@@ -14,6 +12,7 @@ input GenerateMetadataInput {
|
||||
forceTranscodes: Boolean
|
||||
phashes: Boolean
|
||||
interactiveHeatmapsSpeeds: Boolean
|
||||
imageThumbnails: Boolean
|
||||
clipPreviews: Boolean
|
||||
|
||||
"scene ids to generate for"
|
||||
@@ -50,6 +49,7 @@ type GenerateMetadataOptions {
|
||||
transcodes: Boolean
|
||||
phashes: Boolean
|
||||
interactiveHeatmapsSpeeds: Boolean
|
||||
imageThumbnails: Boolean
|
||||
clipPreviews: Boolean
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -118,6 +122,26 @@ input CleanMetadataInput {
|
||||
dryRun: Boolean!
|
||||
}
|
||||
|
||||
input CleanGeneratedInput {
|
||||
"Clean blob files without blob entries"
|
||||
blobFiles: Boolean
|
||||
"Clean sprite and vtt files without scene entries"
|
||||
sprites: Boolean
|
||||
"Clean preview files without scene entries"
|
||||
screenshots: Boolean
|
||||
"Clean scene transcodes without scene entries"
|
||||
transcodes: Boolean
|
||||
|
||||
"Clean marker files without marker entries"
|
||||
markers: Boolean
|
||||
|
||||
"Clean image thumbnails/clips without image entries"
|
||||
imageThumbnails: Boolean
|
||||
|
||||
"Do a dry run. Don't delete any files"
|
||||
dryRun: Boolean
|
||||
}
|
||||
|
||||
input AutoTagMetadataInput {
|
||||
"Paths to tag, null for all files"
|
||||
paths: [String!]
|
||||
@@ -306,6 +330,8 @@ type SystemStatus {
|
||||
os: String!
|
||||
workingDir: String!
|
||||
homeDir: String!
|
||||
ffmpegPath: String
|
||||
ffprobePath: String
|
||||
}
|
||||
|
||||
input MigrateInput {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"An RFC3339 timestamp"
|
||||
scalar Time
|
||||
|
||||
"""
|
||||
Timestamp is a point in time. It is always output as RFC3339-compatible time points.
|
||||
It can be input as a RFC3339 string, or as "<4h" for "4 hours in the past" or ">5m"
|
||||
@@ -5,12 +8,18 @@ for "5 minutes in the future"
|
||||
"""
|
||||
scalar Timestamp
|
||||
|
||||
# generic JSON object
|
||||
"A String -> Any map"
|
||||
scalar Map
|
||||
|
||||
# string, boolean map
|
||||
"A String -> Boolean map"
|
||||
scalar BoolMap
|
||||
|
||||
"A plugin ID -> Map (String -> Any map) map"
|
||||
scalar PluginConfigMap
|
||||
|
||||
scalar Any
|
||||
|
||||
scalar Int64
|
||||
|
||||
"A multipart file upload"
|
||||
scalar Upload
|
||||
|
||||
@@ -58,6 +58,11 @@ type Scene {
|
||||
"The number ot times a scene has been played"
|
||||
play_count: Int
|
||||
|
||||
"Times a scene was played"
|
||||
play_history: [Time!]!
|
||||
"Times the o counter was incremented"
|
||||
o_history: [Time!]!
|
||||
|
||||
files: [VideoFile!]!
|
||||
paths: ScenePathsType! # Resolver
|
||||
scene_markers: [SceneMarker!]!
|
||||
@@ -118,6 +123,7 @@ input SceneUpdateInput {
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
o_counter: Int
|
||||
@deprecated(reason: "Unsupported - Use sceneIncrementO/sceneDecrementO")
|
||||
organized: Boolean
|
||||
studio_id: ID
|
||||
gallery_ids: [ID!]
|
||||
@@ -134,6 +140,9 @@ input SceneUpdateInput {
|
||||
play_duration: Float
|
||||
"The number ot times a scene has been played"
|
||||
play_count: Int
|
||||
@deprecated(
|
||||
reason: "Unsupported - Use sceneIncrementPlayCount/sceneDecrementPlayCount"
|
||||
)
|
||||
|
||||
primary_file_id: ID
|
||||
}
|
||||
@@ -251,4 +260,13 @@ input SceneMergeInput {
|
||||
destination: ID!
|
||||
# values defined here will override values in the destination
|
||||
values: SceneUpdateInput
|
||||
|
||||
# if true, the source history will be combined with the destination
|
||||
play_history: Boolean
|
||||
o_history: Boolean
|
||||
}
|
||||
|
||||
type HistoryMutationResult {
|
||||
count: Int!
|
||||
history: [Time!]!
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ type Studio {
|
||||
stash_ids: [StashID!]!
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
favorite: Boolean!
|
||||
details: String
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
@@ -31,6 +32,7 @@ input StudioCreateInput {
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
favorite: Boolean
|
||||
details: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
@@ -46,6 +48,7 @@ input StudioUpdateInput {
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
favorite: Boolean
|
||||
details: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
@@ -6,7 +6,7 @@ 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
|
||||
@@ -25,7 +25,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 +39,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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
//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
|
||||
//go:generate go run github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
|
||||
//go:generate go run github.com/vektah/dataloaden SceneOCountLoader int int
|
||||
//go:generate go run github.com/vektah/dataloaden ScenePlayCountLoader int int
|
||||
//go:generate go run github.com/vektah/dataloaden SceneOHistoryLoader int []time.Time
|
||||
//go:generate go run github.com/vektah/dataloaden ScenePlayHistoryLoader int []time.Time
|
||||
//go:generate go run github.com/vektah/dataloaden SceneLastPlayedLoader int *time.Time
|
||||
package loaders
|
||||
|
||||
import (
|
||||
@@ -32,8 +36,14 @@ const (
|
||||
)
|
||||
|
||||
type Loaders struct {
|
||||
SceneByID *SceneLoader
|
||||
SceneFiles *SceneFileIDsLoader
|
||||
SceneByID *SceneLoader
|
||||
SceneFiles *SceneFileIDsLoader
|
||||
ScenePlayCount *ScenePlayCountLoader
|
||||
SceneOCount *SceneOCountLoader
|
||||
ScenePlayHistory *ScenePlayHistoryLoader
|
||||
SceneOHistory *SceneOHistoryLoader
|
||||
SceneLastPlayed *SceneLastPlayedLoader
|
||||
|
||||
ImageFiles *ImageFileIDsLoader
|
||||
GalleryFiles *GalleryFileIDsLoader
|
||||
|
||||
@@ -109,6 +119,31 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGalleriesFileIDs(ctx),
|
||||
},
|
||||
ScenePlayCount: &ScenePlayCountLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesPlayCount(ctx),
|
||||
},
|
||||
SceneOCount: &SceneOCountLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesOCount(ctx),
|
||||
},
|
||||
ScenePlayHistory: &ScenePlayHistoryLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesPlayHistory(ctx),
|
||||
},
|
||||
SceneLastPlayed: &SceneLastPlayedLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesLastPlayed(ctx),
|
||||
},
|
||||
SceneOHistory: &SceneOHistoryLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesOHistory(ctx),
|
||||
},
|
||||
}
|
||||
|
||||
newCtx := context.WithValue(r.Context(), loadersCtxKey, ldrs)
|
||||
@@ -251,3 +286,58 @@ func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int)
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesOCount(ctx context.Context) func(keys []int) ([]int, []error) {
|
||||
return func(keys []int) (ret []int, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyOCount(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesPlayCount(ctx context.Context) func(keys []int) ([]int, []error) {
|
||||
return func(keys []int) (ret []int, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyViewCount(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesOHistory(ctx context.Context) func(keys []int) ([][]time.Time, []error) {
|
||||
return func(keys []int) (ret [][]time.Time, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyODates(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesPlayHistory(ctx context.Context) func(keys []int) ([][]time.Time, []error) {
|
||||
return func(keys []int) (ret [][]time.Time, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyViewDates(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesLastPlayed(ctx context.Context) func(keys []int) ([]*time.Time, []error) {
|
||||
return func(keys []int) (ret []*time.Time, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyLastViewed(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
222
internal/api/loaders/scenelastplayedloader_gen.go
Normal file
222
internal/api/loaders/scenelastplayedloader_gen.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SceneLastPlayedLoaderConfig captures the config to create a new SceneLastPlayedLoader
|
||||
type SceneLastPlayedLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*time.Time, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewSceneLastPlayedLoader creates a new SceneLastPlayedLoader given a fetch, wait, and maxBatch
|
||||
func NewSceneLastPlayedLoader(config SceneLastPlayedLoaderConfig) *SceneLastPlayedLoader {
|
||||
return &SceneLastPlayedLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// SceneLastPlayedLoader batches and caches requests
|
||||
type SceneLastPlayedLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]*time.Time, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]*time.Time
|
||||
|
||||
// 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 *sceneLastPlayedLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type sceneLastPlayedLoaderBatch struct {
|
||||
keys []int
|
||||
data []*time.Time
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Time by key, batching and caching will be applied automatically
|
||||
func (l *SceneLastPlayedLoader) Load(key int) (*time.Time, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Time.
|
||||
// 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 *SceneLastPlayedLoader) LoadThunk(key int) func() (*time.Time, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (*time.Time, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &sceneLastPlayedLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (*time.Time, error) {
|
||||
<-batch.done
|
||||
|
||||
var data *time.Time
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *SceneLastPlayedLoader) LoadAll(keys []int) ([]*time.Time, []error) {
|
||||
results := make([]func() (*time.Time, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
times := make([]*time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return times, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Times.
|
||||
// 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 *SceneLastPlayedLoader) LoadAllThunk(keys []int) func() ([]*time.Time, []error) {
|
||||
results := make([]func() (*time.Time, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]*time.Time, []error) {
|
||||
times := make([]*time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return times, 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 *SceneLastPlayedLoader) Prime(key int, value *time.Time) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := *value
|
||||
l.unsafeSet(key, &cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *SceneLastPlayedLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *SceneLastPlayedLoader) unsafeSet(key int, value *time.Time) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]*time.Time{}
|
||||
}
|
||||
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 *sceneLastPlayedLoaderBatch) keyIndex(l *SceneLastPlayedLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *sceneLastPlayedLoaderBatch) startTimer(l *SceneLastPlayedLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *sceneLastPlayedLoaderBatch) end(l *SceneLastPlayedLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
219
internal/api/loaders/sceneocountloader_gen.go
Normal file
219
internal/api/loaders/sceneocountloader_gen.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SceneOCountLoaderConfig captures the config to create a new SceneOCountLoader
|
||||
type SceneOCountLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]int, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewSceneOCountLoader creates a new SceneOCountLoader given a fetch, wait, and maxBatch
|
||||
func NewSceneOCountLoader(config SceneOCountLoaderConfig) *SceneOCountLoader {
|
||||
return &SceneOCountLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// SceneOCountLoader batches and caches requests
|
||||
type SceneOCountLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]int, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]int
|
||||
|
||||
// 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 *sceneOCountLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type sceneOCountLoaderBatch struct {
|
||||
keys []int
|
||||
data []int
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a int by key, batching and caching will be applied automatically
|
||||
func (l *SceneOCountLoader) Load(key int) (int, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a int.
|
||||
// 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 *SceneOCountLoader) LoadThunk(key int) func() (int, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (int, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &sceneOCountLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (int, error) {
|
||||
<-batch.done
|
||||
|
||||
var data int
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *SceneOCountLoader) LoadAll(keys []int) ([]int, []error) {
|
||||
results := make([]func() (int, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
ints := make([]int, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
ints[i], errors[i] = thunk()
|
||||
}
|
||||
return ints, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a ints.
|
||||
// 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 *SceneOCountLoader) LoadAllThunk(keys []int) func() ([]int, []error) {
|
||||
results := make([]func() (int, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]int, []error) {
|
||||
ints := make([]int, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
ints[i], errors[i] = thunk()
|
||||
}
|
||||
return ints, 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 *SceneOCountLoader) Prime(key int, value int) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
l.unsafeSet(key, value)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *SceneOCountLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *SceneOCountLoader) unsafeSet(key int, value int) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]int{}
|
||||
}
|
||||
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 *sceneOCountLoaderBatch) keyIndex(l *SceneOCountLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *sceneOCountLoaderBatch) startTimer(l *SceneOCountLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *sceneOCountLoaderBatch) end(l *SceneOCountLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
223
internal/api/loaders/sceneohistoryloader_gen.go
Normal file
223
internal/api/loaders/sceneohistoryloader_gen.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SceneOHistoryLoaderConfig captures the config to create a new SceneOHistoryLoader
|
||||
type SceneOHistoryLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([][]time.Time, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewSceneOHistoryLoader creates a new SceneOHistoryLoader given a fetch, wait, and maxBatch
|
||||
func NewSceneOHistoryLoader(config SceneOHistoryLoaderConfig) *SceneOHistoryLoader {
|
||||
return &SceneOHistoryLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// SceneOHistoryLoader batches and caches requests
|
||||
type SceneOHistoryLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([][]time.Time, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int][]time.Time
|
||||
|
||||
// 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 *sceneOHistoryLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type sceneOHistoryLoaderBatch struct {
|
||||
keys []int
|
||||
data [][]time.Time
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Time by key, batching and caching will be applied automatically
|
||||
func (l *SceneOHistoryLoader) Load(key int) ([]time.Time, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Time.
|
||||
// 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 *SceneOHistoryLoader) LoadThunk(key int) func() ([]time.Time, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]time.Time, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &sceneOHistoryLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]time.Time, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []time.Time
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *SceneOHistoryLoader) LoadAll(keys []int) ([][]time.Time, []error) {
|
||||
results := make([]func() ([]time.Time, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
times := make([][]time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return times, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Times.
|
||||
// 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 *SceneOHistoryLoader) LoadAllThunk(keys []int) func() ([][]time.Time, []error) {
|
||||
results := make([]func() ([]time.Time, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]time.Time, []error) {
|
||||
times := make([][]time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return times, 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 *SceneOHistoryLoader) Prime(key int, value []time.Time) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]time.Time, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *SceneOHistoryLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *SceneOHistoryLoader) unsafeSet(key int, value []time.Time) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int][]time.Time{}
|
||||
}
|
||||
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 *sceneOHistoryLoaderBatch) keyIndex(l *SceneOHistoryLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *sceneOHistoryLoaderBatch) startTimer(l *SceneOHistoryLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *sceneOHistoryLoaderBatch) end(l *SceneOHistoryLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
219
internal/api/loaders/sceneplaycountloader_gen.go
Normal file
219
internal/api/loaders/sceneplaycountloader_gen.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScenePlayCountLoaderConfig captures the config to create a new ScenePlayCountLoader
|
||||
type ScenePlayCountLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]int, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewScenePlayCountLoader creates a new ScenePlayCountLoader given a fetch, wait, and maxBatch
|
||||
func NewScenePlayCountLoader(config ScenePlayCountLoaderConfig) *ScenePlayCountLoader {
|
||||
return &ScenePlayCountLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// ScenePlayCountLoader batches and caches requests
|
||||
type ScenePlayCountLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]int, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]int
|
||||
|
||||
// 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 *scenePlayCountLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type scenePlayCountLoaderBatch struct {
|
||||
keys []int
|
||||
data []int
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a int by key, batching and caching will be applied automatically
|
||||
func (l *ScenePlayCountLoader) Load(key int) (int, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a int.
|
||||
// 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 *ScenePlayCountLoader) LoadThunk(key int) func() (int, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (int, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &scenePlayCountLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (int, error) {
|
||||
<-batch.done
|
||||
|
||||
var data int
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *ScenePlayCountLoader) LoadAll(keys []int) ([]int, []error) {
|
||||
results := make([]func() (int, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
ints := make([]int, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
ints[i], errors[i] = thunk()
|
||||
}
|
||||
return ints, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a ints.
|
||||
// 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 *ScenePlayCountLoader) LoadAllThunk(keys []int) func() ([]int, []error) {
|
||||
results := make([]func() (int, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]int, []error) {
|
||||
ints := make([]int, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
ints[i], errors[i] = thunk()
|
||||
}
|
||||
return ints, 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 *ScenePlayCountLoader) Prime(key int, value int) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
l.unsafeSet(key, value)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *ScenePlayCountLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *ScenePlayCountLoader) unsafeSet(key int, value int) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]int{}
|
||||
}
|
||||
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 *scenePlayCountLoaderBatch) keyIndex(l *ScenePlayCountLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *scenePlayCountLoaderBatch) startTimer(l *ScenePlayCountLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *scenePlayCountLoaderBatch) end(l *ScenePlayCountLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
223
internal/api/loaders/sceneplayhistoryloader_gen.go
Normal file
223
internal/api/loaders/sceneplayhistoryloader_gen.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScenePlayHistoryLoaderConfig captures the config to create a new ScenePlayHistoryLoader
|
||||
type ScenePlayHistoryLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([][]time.Time, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewScenePlayHistoryLoader creates a new ScenePlayHistoryLoader given a fetch, wait, and maxBatch
|
||||
func NewScenePlayHistoryLoader(config ScenePlayHistoryLoaderConfig) *ScenePlayHistoryLoader {
|
||||
return &ScenePlayHistoryLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// ScenePlayHistoryLoader batches and caches requests
|
||||
type ScenePlayHistoryLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([][]time.Time, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int][]time.Time
|
||||
|
||||
// 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 *scenePlayHistoryLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type scenePlayHistoryLoaderBatch struct {
|
||||
keys []int
|
||||
data [][]time.Time
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Time by key, batching and caching will be applied automatically
|
||||
func (l *ScenePlayHistoryLoader) Load(key int) ([]time.Time, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Time.
|
||||
// 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 *ScenePlayHistoryLoader) LoadThunk(key int) func() ([]time.Time, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]time.Time, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &scenePlayHistoryLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]time.Time, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []time.Time
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *ScenePlayHistoryLoader) LoadAll(keys []int) ([][]time.Time, []error) {
|
||||
results := make([]func() ([]time.Time, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
times := make([][]time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return times, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Times.
|
||||
// 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 *ScenePlayHistoryLoader) LoadAllThunk(keys []int) func() ([][]time.Time, []error) {
|
||||
results := make([]func() ([]time.Time, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]time.Time, []error) {
|
||||
times := make([][]time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return times, 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 *ScenePlayHistoryLoader) Prime(key int, value []time.Time) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]time.Time, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *ScenePlayHistoryLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *ScenePlayHistoryLoader) unsafeSet(key int, value []time.Time) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int][]time.Time{}
|
||||
}
|
||||
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 *scenePlayHistoryLoaderBatch) keyIndex(l *ScenePlayHistoryLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *scenePlayHistoryLoaderBatch) startTimer(l *ScenePlayHistoryLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *scenePlayHistoryLoaderBatch) end(l *ScenePlayHistoryLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
37
internal/api/plugin_map.go
Normal file
37
internal/api/plugin_map.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
)
|
||||
|
||||
func MarshalPluginConfigMap(val map[string]map[string]interface{}) graphql.Marshaler {
|
||||
return graphql.WriterFunc(func(w io.Writer) {
|
||||
err := json.NewEncoder(w).Encode(val)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func UnmarshalPluginConfigMap(v interface{}) (map[string]map[string]interface{}, error) {
|
||||
m, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T is not a plugin config map", v)
|
||||
}
|
||||
|
||||
result := make(map[string]map[string]interface{})
|
||||
for k, v := range m {
|
||||
val, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("key %s (%T) is not a map", k, v)
|
||||
}
|
||||
|
||||
result[k] = val
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
)
|
||||
@@ -29,7 +29,7 @@ var (
|
||||
)
|
||||
|
||||
type hookExecutor interface {
|
||||
ExecutePostHooks(ctx context.Context, id int, hookType plugin.HookTriggerEnum, input interface{}, inputFields []string)
|
||||
ExecutePostHooks(ctx context.Context, id int, hookType hook.TriggerEnum, input interface{}, inputFields []string)
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
@@ -228,7 +228,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
scenesTotalOCount, err := sceneQB.OCount(ctx)
|
||||
scenesTotalOCount, err := sceneQB.GetAllOCount(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -243,12 +243,12 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
totalPlayCount, err := sceneQB.PlayCount(ctx)
|
||||
totalPlayCount, err := sceneQB.CountAllViews(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uniqueScenePlayCount, err := sceneQB.UniqueScenePlayCount(ctx)
|
||||
uniqueScenePlayCount, err := sceneQB.CountUniqueViews(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
)
|
||||
|
||||
func (r *configResultResolver) Plugins(ctx context.Context, obj *ConfigResult, include []string) (map[string]interface{}, error) {
|
||||
func (r *configResultResolver) Plugins(ctx context.Context, obj *ConfigResult, include []string) (map[string]map[string]interface{}, error) {
|
||||
if len(include) == 0 {
|
||||
ret := config.GetInstance().GetAllPluginConfiguration()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
ret := make(map[string]interface{})
|
||||
ret := make(map[string]map[string]interface{})
|
||||
|
||||
for _, plugin := range include {
|
||||
c := config.GetInstance().GetPluginConfiguration(plugin)
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
@@ -319,3 +320,62 @@ func (r *sceneResolver) Urls(ctx context.Context, obj *models.Scene) ([]string,
|
||||
|
||||
return obj.URLs.List(), nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) OCounter(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
ret, err := loaders.From(ctx).SceneOCount.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) LastPlayedAt(ctx context.Context, obj *models.Scene) (*time.Time, error) {
|
||||
ret, err := loaders.From(ctx).SceneLastPlayed.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) PlayCount(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
ret, err := loaders.From(ctx).ScenePlayCount.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) PlayHistory(ctx context.Context, obj *models.Scene) ([]*time.Time, error) {
|
||||
ret, err := loaders.From(ctx).ScenePlayHistory.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert to pointer slice
|
||||
ptrRet := make([]*time.Time, len(ret))
|
||||
for i, t := range ret {
|
||||
tt := t
|
||||
ptrRet[i] = &tt
|
||||
}
|
||||
|
||||
return ptrRet, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) OHistory(ctx context.Context, obj *models.Scene) ([]*time.Time, error) {
|
||||
ret, err := loaders.From(ctx).SceneOHistory.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert to pointer slice
|
||||
ptrRet := make([]*time.Time, len(ret))
|
||||
for i, t := range ret {
|
||||
tt := t
|
||||
ptrRet[i] = &tt
|
||||
}
|
||||
|
||||
return ptrRet, nil
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
@@ -90,7 +91,7 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGallery.ID, plugin.GalleryCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGallery.ID, hook.GalleryCreatePost, input, nil)
|
||||
return r.getGallery(ctx, newGallery.ID)
|
||||
}
|
||||
|
||||
@@ -108,7 +109,7 @@ func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.Galle
|
||||
}
|
||||
|
||||
// execute post hooks outside txn
|
||||
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.GalleryUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, hook.GalleryUpdatePost, input, translator.getFields())
|
||||
return r.getGallery(ctx, ret.ID)
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, hook.GalleryUpdatePost, input, translator.getFields())
|
||||
|
||||
gallery, err = r.getGallery(ctx, gallery.ID)
|
||||
if err != nil {
|
||||
@@ -313,7 +314,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
|
||||
// execute post hooks outside of txn
|
||||
var newRet []*models.Gallery
|
||||
for _, gallery := range ret {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, hook.GalleryUpdatePost, input, translator.getFields())
|
||||
|
||||
gallery, err := r.getGallery(ctx, gallery.ID)
|
||||
if err != nil {
|
||||
@@ -386,9 +387,9 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
}
|
||||
}
|
||||
|
||||
// call post hook after performing the other actions
|
||||
// call post hook after performing the other actionsa
|
||||
for _, gallery := range galleries {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, hook.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
GalleryDestroyInput: input,
|
||||
Checksum: gallery.PrimaryChecksum(),
|
||||
Path: gallery.Path,
|
||||
@@ -397,7 +398,7 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
|
||||
// call image destroy post hook as well
|
||||
for _, img := range imgsDestroyed {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, img.ID, plugin.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
r.hookExecutor.ExecutePostHooks(ctx, img.ID, hook.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
Checksum: img.Checksum,
|
||||
Path: img.Path,
|
||||
}, nil)
|
||||
@@ -518,7 +519,7 @@ func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input Galle
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newChapter.ID, plugin.GalleryChapterCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newChapter.ID, hook.GalleryChapterCreatePost, input, nil)
|
||||
return r.getGalleryChapter(ctx, newChapter.ID)
|
||||
}
|
||||
|
||||
@@ -584,7 +585,7 @@ func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input Galle
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, chapterID, plugin.GalleryChapterUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, chapterID, hook.GalleryChapterUpdatePost, input, translator.getFields())
|
||||
return r.getGalleryChapter(ctx, chapterID)
|
||||
}
|
||||
|
||||
@@ -612,7 +613,7 @@ func (r *mutationResolver) GalleryChapterDestroy(ctx context.Context, id string)
|
||||
return false, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, chapterID, plugin.GalleryChapterDestroyPost, id, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, chapterID, hook.GalleryChapterDestroyPost, id, nil)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -41,7 +42,7 @@ func (r *mutationResolver) ImageUpdate(ctx context.Context, input ImageUpdateInp
|
||||
}
|
||||
|
||||
// execute post hooks outside txn
|
||||
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.ImageUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, hook.ImageUpdatePost, input, translator.getFields())
|
||||
return r.getImage(ctx, ret.ID)
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdat
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, plugin.ImageUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, hook.ImageUpdatePost, input, translator.getFields())
|
||||
|
||||
image, err = r.getImage(ctx, image.ID)
|
||||
if err != nil {
|
||||
@@ -288,7 +289,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
// execute post hooks outside of txn
|
||||
var newRet []*models.Image
|
||||
for _, image := range ret {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, plugin.ImageUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, hook.ImageUpdatePost, input, translator.getFields())
|
||||
|
||||
image, err = r.getImage(ctx, image.ID)
|
||||
if err != nil {
|
||||
@@ -332,7 +333,7 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD
|
||||
fileDeleter.Commit()
|
||||
|
||||
// call post hook after performing the other actions
|
||||
r.hookExecutor.ExecutePostHooks(ctx, i.ID, plugin.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
r.hookExecutor.ExecutePostHooks(ctx, i.ID, hook.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
ImageDestroyInput: input,
|
||||
Checksum: i.Checksum,
|
||||
Path: i.Path,
|
||||
@@ -383,7 +384,7 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image
|
||||
|
||||
for _, image := range images {
|
||||
// call post hook after performing the other actions
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, plugin.ImageDestroyPost, plugin.ImagesDestroyInput{
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, hook.ImageDestroyPost, plugin.ImagesDestroyInput{
|
||||
ImagesDestroyInput: input,
|
||||
Checksum: image.Checksum,
|
||||
Path: image.Path,
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stashapp/stash/internal/identify"
|
||||
"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/logger"
|
||||
)
|
||||
|
||||
@@ -98,6 +99,21 @@ func (r *mutationResolver) MetadataClean(ctx context.Context, input manager.Clea
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataCleanGenerated(ctx context.Context, input task.CleanGeneratedOptions) (string, error) {
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.CleanGeneratedJob{
|
||||
Options: input,
|
||||
Paths: mgr.Paths,
|
||||
BlobsStorageType: mgr.Config.GetBlobsStorage(),
|
||||
VideoFileNamingAlgorithm: mgr.Config.GetVideoFileNamingAlgorithm(),
|
||||
Repository: mgr.Repository,
|
||||
BlobCleaner: mgr.Repository.Blob,
|
||||
}
|
||||
jobID := mgr.JobManager.Add(ctx, "Cleaning generated files...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MigrateHashNaming(ctx context.Context) (string, error) {
|
||||
jobID := manager.GetInstance().MigrateHash(ctx)
|
||||
return strconv.Itoa(jobID), 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
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
@@ -102,7 +102,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, plugin.MovieCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, hook.MovieCreatePost, input, nil)
|
||||
return r.getMovie(ctx, newMovie.ID)
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, plugin.MovieUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||
return r.getMovie(ctx, movie.ID)
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
|
||||
|
||||
var newRet []*models.Movie
|
||||
for _, movie := range ret {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, plugin.MovieUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||
|
||||
movie, err = r.getMovie(ctx, movie.ID)
|
||||
if err != nil {
|
||||
@@ -252,7 +252,7 @@ func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyI
|
||||
return false, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.MovieDestroyPost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -277,7 +277,7 @@ func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string)
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.MovieDestroyPost, movieIDs, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, movieIDs, nil)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
@@ -108,7 +108,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newPerformer.ID, plugin.PerformerCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newPerformer.ID, hook.PerformerCreatePost, input, nil)
|
||||
return r.getPerformer(ctx, newPerformer.ID)
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, performerID, plugin.PerformerUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, performerID, hook.PerformerUpdatePost, input, translator.getFields())
|
||||
return r.getPerformer(ctx, performerID)
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
// execute post hooks outside of txn
|
||||
var newRet []*models.Performer
|
||||
for _, performer := range ret {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, performer.ID, plugin.PerformerUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, performer.ID, hook.PerformerUpdatePost, input, translator.getFields())
|
||||
|
||||
performer, err = r.getPerformer(ctx, performer.ID)
|
||||
if err != nil {
|
||||
@@ -322,7 +322,7 @@ func (r *mutationResolver) PerformerDestroy(ctx context.Context, input Performer
|
||||
return false, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.PerformerDestroyPost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.PerformerDestroyPost, input, nil)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -347,7 +347,7 @@ func (r *mutationResolver) PerformersDestroy(ctx context.Context, performerIDs [
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.PerformerDestroyPost, performerIDs, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.PerformerDestroyPost, performerIDs, nil)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
@@ -9,10 +10,72 @@ import (
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, taskName string, args []*plugin.PluginArgInput) (string, error) {
|
||||
func toPluginArgs(args []*plugin.PluginArgInput) plugin.OperationInput {
|
||||
ret := make(plugin.OperationInput)
|
||||
for _, a := range args {
|
||||
ret[a.Key] = toPluginArgValue(a.Value)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func toPluginArgValue(arg *plugin.PluginValueInput) interface{} {
|
||||
if arg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case arg.Str != nil:
|
||||
return *arg.Str
|
||||
case arg.I != nil:
|
||||
return *arg.I
|
||||
case arg.B != nil:
|
||||
return *arg.B
|
||||
case arg.F != nil:
|
||||
return *arg.F
|
||||
case arg.O != nil:
|
||||
return toPluginArgs(arg.O)
|
||||
case arg.A != nil:
|
||||
var ret []interface{}
|
||||
for _, v := range arg.A {
|
||||
ret = append(ret, toPluginArgValue(v))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RunPluginTask(
|
||||
ctx context.Context,
|
||||
pluginID string,
|
||||
taskName *string,
|
||||
description *string,
|
||||
args []*plugin.PluginArgInput,
|
||||
argsMap map[string]interface{},
|
||||
) (string, error) {
|
||||
if argsMap == nil {
|
||||
// convert args to map
|
||||
// otherwise ignore args in favour of args map
|
||||
argsMap = toPluginArgs(args)
|
||||
}
|
||||
|
||||
m := manager.GetInstance()
|
||||
m.RunPluginTask(ctx, pluginID, taskName, args)
|
||||
return "todo", nil
|
||||
jobID := m.RunPluginTask(ctx, pluginID, taskName, description, argsMap)
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RunPluginOperation(
|
||||
ctx context.Context,
|
||||
pluginID string,
|
||||
args map[string]interface{},
|
||||
) (interface{}, error) {
|
||||
if args == nil {
|
||||
args = make(map[string]interface{})
|
||||
}
|
||||
|
||||
m := manager.GetInstance()
|
||||
return m.PluginCache.RunPlugin(ctx, pluginID, args)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) {
|
||||
@@ -40,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
|
||||
|
||||
@@ -5,11 +5,14 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
@@ -114,7 +117,7 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.SceneUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, hook.SceneUpdatePost, input, translator.getFields())
|
||||
return r.getScene(ctx, ret.ID)
|
||||
}
|
||||
|
||||
@@ -148,7 +151,7 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, plugin.SceneUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, hook.SceneUpdatePost, input, translator.getFields())
|
||||
|
||||
scene, err = r.getScene(ctx, scene.ID)
|
||||
if err != nil {
|
||||
@@ -169,8 +172,15 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
|
||||
updatedScene.Details = translator.optionalString(input.Details, "details")
|
||||
updatedScene.Director = translator.optionalString(input.Director, "director")
|
||||
updatedScene.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedScene.OCounter = translator.optionalInt(input.OCounter, "o_counter")
|
||||
updatedScene.PlayCount = translator.optionalInt(input.PlayCount, "play_count")
|
||||
|
||||
if input.OCounter != nil {
|
||||
logger.Warnf("o_counter is deprecated and no longer supported, use sceneIncrementO/sceneDecrementO instead")
|
||||
}
|
||||
|
||||
if input.PlayCount != nil {
|
||||
logger.Warnf("play_count is deprecated and no longer supported, use sceneIncrementPlayCount/sceneDecrementPlayCount instead")
|
||||
}
|
||||
|
||||
updatedScene.PlayDuration = translator.optionalFloat64(input.PlayDuration, "play_duration")
|
||||
updatedScene.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
updatedScene.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
|
||||
@@ -376,7 +386,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
|
||||
// execute post hooks outside of txn
|
||||
var newRet []*models.Scene
|
||||
for _, scene := range ret {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, plugin.SceneUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, hook.SceneUpdatePost, input, translator.getFields())
|
||||
|
||||
scene, err = r.getScene(ctx, scene.ID)
|
||||
if err != nil {
|
||||
@@ -432,7 +442,7 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
|
||||
fileDeleter.Commit()
|
||||
|
||||
// call post hook after performing the other actions
|
||||
r.hookExecutor.ExecutePostHooks(ctx, s.ID, plugin.SceneDestroyPost, plugin.SceneDestroyInput{
|
||||
r.hookExecutor.ExecutePostHooks(ctx, s.ID, hook.SceneDestroyPost, plugin.SceneDestroyInput{
|
||||
SceneDestroyInput: input,
|
||||
Checksum: s.Checksum,
|
||||
OSHash: s.OSHash,
|
||||
@@ -493,7 +503,7 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
|
||||
|
||||
for _, scene := range scenes {
|
||||
// call post hook after performing the other actions
|
||||
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, plugin.SceneDestroyPost, plugin.ScenesDestroyInput{
|
||||
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, hook.SceneDestroyPost, plugin.ScenesDestroyInput{
|
||||
ScenesDestroyInput: input,
|
||||
Checksum: scene.Checksum,
|
||||
OSHash: scene.OSHash,
|
||||
@@ -560,9 +570,20 @@ func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput
|
||||
values = &v
|
||||
}
|
||||
|
||||
mgr := manager.GetInstance()
|
||||
fileDeleter := &scene.FileDeleter{
|
||||
Deleter: file.NewDeleter(),
|
||||
FileNamingAlgo: mgr.Config.GetVideoFileNamingAlgorithm(),
|
||||
Paths: mgr.Paths,
|
||||
}
|
||||
|
||||
var ret *models.Scene
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := r.Resolver.sceneService.Merge(ctx, srcIDs, destID, *values); err != nil {
|
||||
if err := r.Resolver.sceneService.Merge(ctx, srcIDs, destID, fileDeleter, scene.MergeOptions{
|
||||
ScenePartial: *values,
|
||||
IncludePlayHistory: utils.IsTrue(input.PlayHistory),
|
||||
IncludeOHistory: utils.IsTrue(input.OHistory),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -633,7 +654,7 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newMarker.ID, plugin.SceneMarkerCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newMarker.ID, hook.SceneMarkerCreatePost, input, nil)
|
||||
return r.getSceneMarker(ctx, newMarker.ID)
|
||||
}
|
||||
|
||||
@@ -731,7 +752,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
|
||||
// perform the post-commit actions
|
||||
fileDeleter.Commit()
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, markerID, plugin.SceneMarkerUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerUpdatePost, input, translator.getFields())
|
||||
return r.getSceneMarker(ctx, markerID)
|
||||
}
|
||||
|
||||
@@ -781,7 +802,7 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
|
||||
// perform the post-commit actions
|
||||
fileDeleter.Commit()
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, markerID, plugin.SceneMarkerDestroyPost, id, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerDestroyPost, id, nil)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -804,16 +825,96 @@ func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, res
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
var updatedTimes []time.Time
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.IncrementWatchCount(ctx, sceneID)
|
||||
updatedTimes, err = qb.AddViews(ctx, sceneID, nil)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(updatedTimes), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneAddPlay(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
var times []time.Time
|
||||
|
||||
// convert time to local time, so that sorting is consistent
|
||||
for _, tt := range t {
|
||||
times = append(times, tt.Local())
|
||||
}
|
||||
|
||||
var updatedTimes []time.Time
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
updatedTimes, err = qb.AddViews(ctx, sceneID, times)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HistoryMutationResult{
|
||||
Count: len(updatedTimes),
|
||||
History: sliceutil.ValuesToPtrs(updatedTimes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneDeletePlay(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var times []time.Time
|
||||
|
||||
for _, tt := range t {
|
||||
times = append(times, *tt)
|
||||
}
|
||||
|
||||
var updatedTimes []time.Time
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
updatedTimes, err = qb.DeleteViews(ctx, sceneID, times)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HistoryMutationResult{
|
||||
Count: len(updatedTimes),
|
||||
History: sliceutil.ValuesToPtrs(updatedTimes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneResetPlayCount(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.DeleteAllViews(ctx, sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -822,40 +923,46 @@ func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id strin
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
var updatedTimes []time.Time
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.IncrementOCounter(ctx, sceneID)
|
||||
updatedTimes, err = qb.AddO(ctx, sceneID, nil)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return len(updatedTimes), nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
var updatedTimes []time.Time
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.DecrementOCounter(ctx, sceneID)
|
||||
updatedTimes, err = qb.DeleteO(ctx, sceneID, nil)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return len(updatedTimes), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int, err error) {
|
||||
@@ -867,7 +974,7 @@ func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int,
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
ret, err = qb.ResetOCounter(ctx, sceneID)
|
||||
ret, err = qb.ResetO(ctx, sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -876,6 +983,65 @@ func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int,
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneAddO(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
var times []time.Time
|
||||
|
||||
// convert time to local time, so that sorting is consistent
|
||||
for _, tt := range t {
|
||||
times = append(times, tt.Local())
|
||||
}
|
||||
|
||||
var updatedTimes []time.Time
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
updatedTimes, err = qb.AddO(ctx, sceneID, times)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HistoryMutationResult{
|
||||
Count: len(updatedTimes),
|
||||
History: sliceutil.ValuesToPtrs(updatedTimes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneDeleteO(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
var times []time.Time
|
||||
|
||||
for _, tt := range t {
|
||||
times = append(times, *tt)
|
||||
}
|
||||
|
||||
var updatedTimes []time.Time
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
updatedTimes, err = qb.DeleteO(ctx, sceneID, times)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HistoryMutationResult{
|
||||
Count: len(updatedTimes),
|
||||
History: sliceutil.ValuesToPtrs(updatedTimes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneGenerateScreenshot(ctx context.Context, id string, at *float64) (string, error) {
|
||||
if at != nil {
|
||||
manager.GetInstance().GenerateScreenshot(ctx, id, *at)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/studio"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -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)
|
||||
@@ -61,16 +62,10 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Studio
|
||||
|
||||
if err := studio.EnsureStudioNameUnique(ctx, 0, newStudio.Name, qb); err != nil {
|
||||
if err := studio.ValidateCreate(ctx, newStudio, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(input.Aliases) > 0 {
|
||||
if err := studio.EnsureAliasesUnique(ctx, 0, input.Aliases, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = qb.Create(ctx, &newStudio)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -87,7 +82,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newStudio.ID, plugin.StudioCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newStudio.ID, hook.StudioCreatePost, input, nil)
|
||||
return r.getStudio(ctx, newStudio.ID)
|
||||
}
|
||||
|
||||
@@ -109,6 +104,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")
|
||||
@@ -153,7 +149,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, studioID, plugin.StudioUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, studioID, hook.StudioUpdatePost, input, translator.getFields())
|
||||
return r.getStudio(ctx, studioID)
|
||||
}
|
||||
|
||||
@@ -169,7 +165,7 @@ func (r *mutationResolver) StudioDestroy(ctx context.Context, input StudioDestro
|
||||
return false, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.StudioDestroyPost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.StudioDestroyPost, input, nil)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -194,7 +190,7 @@ func (r *mutationResolver) StudiosDestroy(ctx context.Context, studioIDs []strin
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.StudioDestroyPost, studioIDs, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.StudioDestroyPost, studioIDs, nil)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/tag"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -33,6 +33,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||
newTag := models.NewTag()
|
||||
|
||||
newTag.Name = input.Name
|
||||
newTag.Favorite = translator.bool(input.Favorite)
|
||||
newTag.Description = translator.string(input.Description)
|
||||
newTag.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
|
||||
@@ -119,7 +120,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newTag.ID, plugin.TagCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newTag.ID, hook.TagCreatePost, input, nil)
|
||||
return r.getTag(ctx, newTag.ID)
|
||||
}
|
||||
|
||||
@@ -136,6 +137,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||
// Populate tag from the input
|
||||
updatedTag := models.NewTagPartial()
|
||||
|
||||
updatedTag.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedTag.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
updatedTag.Description = translator.optionalString(input.Description, "description")
|
||||
|
||||
@@ -235,7 +237,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, t.ID, plugin.TagUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, t.ID, hook.TagUpdatePost, input, translator.getFields())
|
||||
return r.getTag(ctx, t.ID)
|
||||
}
|
||||
|
||||
@@ -251,7 +253,7 @@ func (r *mutationResolver) TagDestroy(ctx context.Context, input TagDestroyInput
|
||||
return false, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, tagID, plugin.TagDestroyPost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, tagID, hook.TagDestroyPost, input, nil)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -276,7 +278,7 @@ func (r *mutationResolver) TagsDestroy(ctx context.Context, tagIDs []string) (bo
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.TagDestroyPost, tagIDs, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.TagDestroyPost, tagIDs, nil)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
@@ -340,7 +342,7 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, t.ID, plugin.TagMergePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, t.ID, hook.TagMergePost, input, nil)
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -35,7 +35,7 @@ var testCtx = context.Background()
|
||||
|
||||
type mockHookExecutor struct{}
|
||||
|
||||
func (*mockHookExecutor) ExecutePostHooks(ctx context.Context, id int, hookType plugin.HookTriggerEnum, input interface{}, inputFields []string) {
|
||||
func (*mockHookExecutor) ExecutePostHooks(ctx context.Context, id int, hookType hook.TriggerEnum, input interface{}, inputFields []string) {
|
||||
}
|
||||
|
||||
func TestTagCreate(t *testing.T) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models.Gallery, err error) {
|
||||
@@ -23,9 +24,24 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *FindGalleriesResultType, err error) {
|
||||
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType, ids []string) (ret *FindGalleriesResultType, err error) {
|
||||
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
galleries, total, err := r.repository.Gallery.Query(ctx, galleryFilter, filter)
|
||||
var galleries []*models.Gallery
|
||||
var err error
|
||||
var total int
|
||||
|
||||
if len(idInts) > 0 {
|
||||
galleries, err = r.repository.Gallery.FindMany(ctx, idInts)
|
||||
total = len(galleries)
|
||||
} else {
|
||||
galleries, total, err = r.repository.Gallery.Query(ctx, galleryFilter, filter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
|
||||
@@ -46,26 +47,65 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *FindImagesResultType, err error) {
|
||||
func (r *queryResolver) FindImages(
|
||||
ctx context.Context,
|
||||
imageFilter *models.ImageFilterType,
|
||||
imageIds []int,
|
||||
ids []string,
|
||||
filter *models.FindFilterType,
|
||||
) (ret *FindImagesResultType, err error) {
|
||||
if len(ids) > 0 {
|
||||
imageIds, err = stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Image
|
||||
|
||||
var images []*models.Image
|
||||
fields := graphql.CollectAllFields(ctx)
|
||||
result := &models.ImageQueryResult{}
|
||||
|
||||
result, err := qb.Query(ctx, models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
},
|
||||
ImageFilter: imageFilter,
|
||||
Megapixels: sliceutil.Contains(fields, "megapixels"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
if len(imageIds) > 0 {
|
||||
images, err = r.repository.Image.FindMany(ctx, imageIds)
|
||||
if err == nil {
|
||||
result.Count = len(images)
|
||||
for _, s := range images {
|
||||
if err = s.LoadPrimaryFile(ctx, r.repository.File); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
f := s.Files.Primary()
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
imageFile, ok := f.(*models.ImageFile)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
result.Megapixels += float64(imageFile.Width*imageFile.Height) / float64(1000000)
|
||||
result.TotalSize += float64(f.Base().Size)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result, err = qb.Query(ctx, models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
},
|
||||
ImageFilter: imageFilter,
|
||||
Megapixels: sliceutil.Contains(fields, "megapixels"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
})
|
||||
if err == nil {
|
||||
images, err = result.Resolve(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
images, err := result.Resolve(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Movie, err error) {
|
||||
@@ -23,9 +24,24 @@ 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) (ret *FindMoviesResultType, err error) {
|
||||
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, 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 {
|
||||
movies, total, err := r.repository.Movie.Query(ctx, movieFilter, filter)
|
||||
var movies []*models.Movie
|
||||
var err error
|
||||
var total int
|
||||
|
||||
if len(idInts) > 0 {
|
||||
movies, err = r.repository.Movie.FindMany(ctx, idInts)
|
||||
total = len(movies)
|
||||
} else {
|
||||
movies, total, err = r.repository.Movie.Query(ctx, movieFilter, filter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *models.Performer, err error) {
|
||||
@@ -23,7 +24,14 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType, performerIDs []int) (ret *FindPerformersResultType, err error) {
|
||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType, performerIDs []int, ids []string) (ret *FindPerformersResultType, err error) {
|
||||
if len(ids) > 0 {
|
||||
performerIDs, err = stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var performers []*models.Performer
|
||||
var err error
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
|
||||
@@ -74,7 +75,20 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInpu
|
||||
return scene, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
|
||||
func (r *queryResolver) FindScenes(
|
||||
ctx context.Context,
|
||||
sceneFilter *models.SceneFilterType,
|
||||
sceneIDs []int,
|
||||
ids []string,
|
||||
filter *models.FindFilterType,
|
||||
) (ret *FindScenesResultType, err error) {
|
||||
if len(ids) > 0 {
|
||||
sceneIDs, err = stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var scenes []*models.Scene
|
||||
var err error
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.Studio, err error) {
|
||||
@@ -24,9 +25,23 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (ret *FindStudiosResultType, err error) {
|
||||
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType, ids []string) (ret *FindStudiosResultType, err error) {
|
||||
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
studios, total, err := r.repository.Studio.Query(ctx, studioFilter, filter)
|
||||
var studios []*models.Studio
|
||||
var err error
|
||||
var total int
|
||||
|
||||
if len(idInts) > 0 {
|
||||
studios, err = r.repository.Studio.FindMany(ctx, idInts)
|
||||
total = len(studios)
|
||||
} else {
|
||||
studios, total, err = r.repository.Studio.Query(ctx, studioFilter, filter)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag, err error) {
|
||||
@@ -23,9 +24,24 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType) (ret *FindTagsResultType, err error) {
|
||||
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType, ids []string) (ret *FindTagsResultType, err error) {
|
||||
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
tags, total, err := r.repository.Tag.Query(ctx, tagFilter, filter)
|
||||
var tags []*models.Tag
|
||||
var err error
|
||||
var total int
|
||||
|
||||
if len(idInts) > 0 {
|
||||
tags, err = r.repository.Tag.FindMany(ctx, idInts)
|
||||
total = len(tags)
|
||||
} else {
|
||||
tags, total, err = r.repository.Tag.Query(ctx, tagFilter, filter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -54,9 +54,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 +67,77 @@ 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 {
|
||||
@@ -123,7 +169,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) {
|
||||
@@ -264,39 +319,46 @@ 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 {
|
||||
case source.StashBoxIndex != nil:
|
||||
client, err := r.getStashBoxClient(*source.StashBoxIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []*stashbox.StashBoxPerformerQueryResult
|
||||
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,14 +367,16 @@ 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) {
|
||||
@@ -331,6 +395,8 @@ func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source scrape
|
||||
}
|
||||
|
||||
func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.Source, input ScrapeSingleGalleryInput) ([]*scraper.ScrapedGallery, error) {
|
||||
var ret []*scraper.ScrapedGallery
|
||||
|
||||
if source.StashBoxIndex != nil {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
@@ -351,16 +417,25 @@ 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) {
|
||||
|
||||
@@ -46,8 +46,9 @@ 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)
|
||||
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
|
||||
filepath := mgr.Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
|
||||
|
||||
// if the thumbnail doesn't exist, encode on the fly
|
||||
exists, _ := fsutil.FileExists(filepath)
|
||||
@@ -62,6 +63,11 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// use the image thumbnail generate wait group to limit the number of concurrent thumbnail generation tasks
|
||||
wg := &mgr.ImageThumbnailGenerateWaitGroup
|
||||
wg.Add()
|
||||
defer wg.Done()
|
||||
|
||||
clipPreviewOptions := image.ClipPreviewOptions{
|
||||
InputArgs: manager.GetInstance().Config.GetTranscodeInputArgs(),
|
||||
OutputArgs: manager.GetInstance().Config.GetTranscodeOutputArgs(),
|
||||
|
||||
@@ -50,6 +50,7 @@ func (rs pluginRoutes) Assets(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path, dir = p.UI.Assets.GetFilesystemLocation(r.URL.Path)
|
||||
if dir == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
dir = filepath.Join(pluginDir, filepath.FromSlash(dir))
|
||||
|
||||
@@ -53,6 +53,28 @@ type Server struct {
|
||||
manager *manager.Manager
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Called at startup
|
||||
func Initialize() (*Server, error) {
|
||||
mgr := manager.GetInstance()
|
||||
@@ -213,25 +235,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 +269,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)
|
||||
|
||||
@@ -2,14 +2,9 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID {
|
||||
ret := make([]*models.StashID, len(v))
|
||||
for i, vv := range v {
|
||||
c := vv
|
||||
ret[i] = &c
|
||||
}
|
||||
|
||||
return ret
|
||||
return sliceutil.ValuesToPtrs(v)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -542,7 +542,7 @@ func (me *Server) contentDirectoryEventSubHandler(w http.ResponseWriter, r *http
|
||||
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)
|
||||
_, _ = fmt.Sscanf(r.Header.Get("TIMEOUT"), "Second-%d", &timeout)
|
||||
sid, timeout, _ := service.Subscribe(urls, timeout)
|
||||
w.Header()["SID"] = []string{sid}
|
||||
w.Header()["TIMEOUT"] = []string{fmt.Sprintf("Second-%d", timeout)}
|
||||
@@ -595,6 +595,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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -252,7 +252,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
|
||||
@@ -392,7 +393,7 @@ func getScenePartial(scene *models.Scene, scraped *scraper.ScrapedScene, fieldOp
|
||||
switch getFieldStrategy(fieldOptions["url"]) {
|
||||
case FieldStrategyOverwrite:
|
||||
// only overwrite if not equal
|
||||
if len(sliceutil.Exclude(scene.URLs.List(), scraped.URLs)) != 0 {
|
||||
if len(sliceutil.Exclude(scraped.URLs, scene.URLs.List())) != 0 {
|
||||
partial.URLs = &models.UpdateStrings{
|
||||
Values: scraped.URLs,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sync"
|
||||
@@ -13,7 +15,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 +42,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 +163,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 +187,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
|
||||
@@ -231,13 +241,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 +264,7 @@ const (
|
||||
deleteGeneratedDefaultDefault = true
|
||||
|
||||
// Desktop Integration Options
|
||||
NoBrowser = "noBrowser"
|
||||
NoBrowser = "nobrowser"
|
||||
NoBrowserDefault = false
|
||||
NotificationsEnabled = "notifications_enabled"
|
||||
NotificationsEnabledDefault = true
|
||||
@@ -260,6 +273,9 @@ const (
|
||||
|
||||
// File upload options
|
||||
MaxUploadSize = "max_upload_size"
|
||||
|
||||
// Developer options
|
||||
ExtraBlobsPaths = "developer_options.extra_blob_paths"
|
||||
)
|
||||
|
||||
// slice default values
|
||||
@@ -290,12 +306,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
|
||||
@@ -313,6 +330,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
|
||||
}
|
||||
@@ -320,7 +346,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() {
|
||||
@@ -351,10 +377,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.
|
||||
@@ -362,34 +384,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
|
||||
@@ -402,7 +483,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
|
||||
@@ -411,18 +492,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
|
||||
}
|
||||
|
||||
@@ -431,10 +526,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
|
||||
}
|
||||
|
||||
@@ -445,7 +540,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
|
||||
@@ -455,28 +550,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 {
|
||||
@@ -484,9 +579,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
|
||||
}
|
||||
@@ -495,21 +590,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.
|
||||
@@ -530,13 +625,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{
|
||||
@@ -561,6 +656,12 @@ func (i *Config) GetBlobsPath() string {
|
||||
return i.getString(BlobsPath)
|
||||
}
|
||||
|
||||
// GetExtraBlobsPaths returns extra blobs paths.
|
||||
// For developer/advanced use only.
|
||||
func (i *Config) GetExtraBlobsPaths() []string {
|
||||
return i.getStringSlice(ExtraBlobsPaths)
|
||||
}
|
||||
|
||||
func (i *Config) GetBlobsStorage() BlobsStorageType {
|
||||
ret := BlobsStorageType(i.getString(BlobsStorage))
|
||||
|
||||
@@ -594,6 +695,18 @@ func (i *Config) GetBackupDirectoryPathOrDefault() string {
|
||||
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))
|
||||
}
|
||||
@@ -619,7 +732,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
|
||||
@@ -627,7 +740,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
|
||||
@@ -635,7 +748,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
|
||||
@@ -735,22 +848,21 @@ func (i *Config) GetPluginsPath() string {
|
||||
return i.getString(PluginsPath)
|
||||
}
|
||||
|
||||
func (i *Config) GetAllPluginConfiguration() map[string]interface{} {
|
||||
func (i *Config) GetAllPluginConfiguration() map[string]map[string]interface{} {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
ret := make(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
|
||||
@@ -760,26 +872,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 {
|
||||
@@ -1019,9 +1125,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
|
||||
@@ -1033,17 +1139,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
|
||||
}
|
||||
@@ -1057,9 +1167,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
|
||||
}
|
||||
@@ -1073,9 +1183,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
|
||||
@@ -1109,14 +1219,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,24 +1243,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
|
||||
@@ -1169,20 +1279,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 {
|
||||
@@ -1340,11 +1444,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
|
||||
@@ -1359,11 +1464,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
|
||||
@@ -1378,11 +1483,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
|
||||
@@ -1397,11 +1502,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
|
||||
@@ -1446,6 +1551,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 {
|
||||
@@ -1493,9 +1613,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
|
||||
}
|
||||
@@ -1525,7 +1645,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()
|
||||
}
|
||||
|
||||
@@ -1595,7 +1715,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)
|
||||
}
|
||||
}
|
||||
@@ -1606,7 +1726,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},
|
||||
}
|
||||
@@ -1626,52 +1746,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,
|
||||
@@ -1687,13 +1807,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1708,7 +1828,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()) == "" {
|
||||
@@ -1716,7 +1836,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,44 @@ 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,
|
||||
"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 +102,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 +155,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 +169,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
|
||||
|
||||
@@ -44,9 +44,7 @@ type Action struct {
|
||||
// Pos is the place in percent to move to.
|
||||
Pos int `json:"pos"`
|
||||
|
||||
Slope float64
|
||||
Intensity int64
|
||||
Speed float64
|
||||
Speed float64
|
||||
}
|
||||
|
||||
type GradientTable []struct {
|
||||
@@ -136,8 +134,7 @@ func (funscript *Script) UpdateIntensityAndSpeed() {
|
||||
|
||||
var t1, t2 int64
|
||||
var p1, p2 int
|
||||
var slope float64
|
||||
var intensity int64
|
||||
var intensity float64
|
||||
for i := range funscript.Actions {
|
||||
if i == 0 {
|
||||
continue
|
||||
@@ -147,13 +144,10 @@ func (funscript *Script) UpdateIntensityAndSpeed() {
|
||||
p1 = funscript.Actions[i].Pos
|
||||
p2 = funscript.Actions[i-1].Pos
|
||||
|
||||
slope = math.Min(math.Max(1/(2*float64(t1-t2)/1000), 0), 20)
|
||||
intensity = int64(slope * math.Abs((float64)(p1-p2)))
|
||||
speed := math.Abs(float64(p1-p2)) / float64(t1-t2) * 1000
|
||||
speed := math.Abs(float64(p1 - p2))
|
||||
intensity = float64(speed/float64(t1-t2)) * 1000
|
||||
|
||||
funscript.Actions[i].Slope = slope
|
||||
funscript.Actions[i].Intensity = intensity
|
||||
funscript.Actions[i].Speed = speed
|
||||
funscript.Actions[i].Speed = intensity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +288,7 @@ func (funscript Script) getGradientTable(numSegments int, sceneDurationMilli int
|
||||
}
|
||||
segments[segment].at = a.At
|
||||
segments[segment].count++
|
||||
segments[segment].intensity += int(a.Intensity)
|
||||
segments[segment].intensity += int(a.Speed)
|
||||
segments[segment].yRange[0] = averageTop
|
||||
segments[segment].yRange[1] = averageBottom
|
||||
}
|
||||
@@ -303,7 +297,7 @@ func (funscript Script) getGradientTable(numSegments int, sceneDurationMilli int
|
||||
|
||||
// Fill in gaps in segments
|
||||
for i := 0; i < numSegments; i++ {
|
||||
segmentTS := int64(float64(i) / float64(numSegments))
|
||||
segmentTS := (maxts / int64(numSegments)) * int64(i)
|
||||
|
||||
// Empty segment - fill it with the previous up to backfillThreshold ms
|
||||
if segments[i].count == 0 {
|
||||
@@ -340,12 +334,12 @@ func getSegmentColor(intensity float64) colorful.Color {
|
||||
colorBlack, _ := colorful.Hex("#0f001e")
|
||||
colorBackground, _ := colorful.Hex("#30404d") // Same as GridCard bg
|
||||
|
||||
var stepSize = 60.0
|
||||
var stepSize = 125.0
|
||||
var f float64
|
||||
var c colorful.Color
|
||||
|
||||
switch {
|
||||
case intensity <= 0.001:
|
||||
case intensity <= 25:
|
||||
c = colorBackground
|
||||
case intensity <= 1*stepSize:
|
||||
f = (intensity - 0*stepSize) / stepSize
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/remeh/sizedwaitgroup"
|
||||
"github.com/stashapp/stash/internal/desktop"
|
||||
"github.com/stashapp/stash/internal/dlna"
|
||||
"github.com/stashapp/stash/internal/log"
|
||||
@@ -80,6 +81,8 @@ func Initialize(cfg *config.Config, l *log.Logger) (*Manager, error) {
|
||||
|
||||
Paths: mgrPaths,
|
||||
|
||||
ImageThumbnailGenerateWaitGroup: sizedwaitgroup.New(1),
|
||||
|
||||
JobManager: initJobManager(cfg),
|
||||
ReadLockManager: fsutil.NewReadLockManager(),
|
||||
|
||||
@@ -189,7 +192,6 @@ func (s *Manager) postInit(ctx context.Context) error {
|
||||
s.RefreshScraperCache()
|
||||
s.RefreshScraperSourceManager()
|
||||
|
||||
s.RefreshStreamManager()
|
||||
s.RefreshDLNA()
|
||||
|
||||
s.SetBlobStoreOptions()
|
||||
@@ -236,9 +238,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
|
||||
}
|
||||
@@ -257,41 +258,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.FFProbe(ffprobePath)
|
||||
|
||||
s.FFMpeg.InitHWSupport(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/remeh/sizedwaitgroup"
|
||||
"github.com/stashapp/stash/internal/dlna"
|
||||
"github.com/stashapp/stash/internal/log"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
@@ -33,6 +34,10 @@ type Manager struct {
|
||||
Config *config.Config
|
||||
Logger *log.Logger
|
||||
|
||||
// ImageThumbnailGenerateWaitGroup is the global wait group image thumbnail generation
|
||||
// It uses the parallel tasks setting from the configuration.
|
||||
ImageThumbnailGenerateWaitGroup sizedwaitgroup.SizedWaitGroup
|
||||
|
||||
Paths *paths.Paths
|
||||
|
||||
FFMpeg *ffmpeg.FFMpeg
|
||||
@@ -75,11 +80,13 @@ func GetInstance() *Manager {
|
||||
func (s *Manager) SetBlobStoreOptions() {
|
||||
storageType := s.Config.GetBlobsStorage()
|
||||
blobsPath := s.Config.GetBlobsPath()
|
||||
extraBlobsPaths := s.Config.GetExtraBlobsPaths()
|
||||
|
||||
s.Database.SetBlobStoreOptions(sqlite.BlobStoreOptions{
|
||||
UseFilesystem: storageType == config.BlobStorageTypeFilesystem,
|
||||
UseDatabase: storageType == config.BlobStorageTypeDatabase,
|
||||
Path: blobsPath,
|
||||
UseFilesystem: storageType == config.BlobStorageTypeFilesystem,
|
||||
UseDatabase: storageType == config.BlobStorageTypeDatabase,
|
||||
Path: blobsPath,
|
||||
SupplementaryPaths: extraBlobsPaths,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,6 +112,8 @@ func (s *Manager) RefreshConfig() {
|
||||
if err := fsutil.EnsureDir(s.Paths.Generated.InteractiveHeatmap); err != nil {
|
||||
logger.Warnf("could not create interactive heatmaps directory: %v", err)
|
||||
}
|
||||
|
||||
s.ImageThumbnailGenerateWaitGroup.Size = cfg.GetParallelTasksWithAutoDetection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +245,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
|
||||
@@ -247,11 +256,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 {
|
||||
@@ -260,18 +269,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)
|
||||
@@ -294,52 +303,6 @@ func (s *Manager) validateFFmpeg() error {
|
||||
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
|
||||
@@ -428,6 +391,16 @@ func (s *Manager) GetSystemStatus() *SystemStatus {
|
||||
|
||||
configFile := s.Config.GetConfigFile()
|
||||
|
||||
ffmpegPath := ""
|
||||
if s.FFMpeg != nil {
|
||||
ffmpegPath = s.FFMpeg.Path()
|
||||
}
|
||||
|
||||
ffprobePath := ""
|
||||
if s.FFProbe != "" {
|
||||
ffprobePath = s.FFProbe.Path()
|
||||
}
|
||||
|
||||
return &SystemStatus{
|
||||
Os: runtime.GOOS,
|
||||
WorkingDir: workingDir,
|
||||
@@ -437,6 +410,8 @@ func (s *Manager) GetSystemStatus() *SystemStatus {
|
||||
AppSchema: appSchema,
|
||||
Status: status,
|
||||
ConfigPath: &configFile,
|
||||
FfmpegPath: &ffmpegPath,
|
||||
FfprobePath: &ffprobePath,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,14 +19,17 @@ import (
|
||||
)
|
||||
|
||||
func useAsVideo(pathname string) bool {
|
||||
if instance.Config.IsCreateImageClipsFromVideos() && config.StashConfigs.GetStashFromDirPath(instance.Config.GetStashPaths(), pathname).ExcludeVideo {
|
||||
stash := config.StashConfigs.GetStashFromDirPath(instance.Config.GetStashPaths(), pathname)
|
||||
|
||||
if instance.Config.IsCreateImageClipsFromVideos() && stash != nil && stash.ExcludeVideo {
|
||||
return false
|
||||
}
|
||||
return isVideo(pathname)
|
||||
}
|
||||
|
||||
func useAsImage(pathname string) bool {
|
||||
if instance.Config.IsCreateImageClipsFromVideos() && config.StashConfigs.GetStashFromDirPath(instance.Config.GetStashPaths(), pathname).ExcludeVideo {
|
||||
stash := config.StashConfigs.GetStashFromDirPath(instance.Config.GetStashPaths(), pathname)
|
||||
if instance.Config.IsCreateImageClipsFromVideos() && stash != nil && stash.ExcludeVideo {
|
||||
return isImage(pathname) || isVideo(pathname)
|
||||
}
|
||||
return isImage(pathname)
|
||||
@@ -133,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,
|
||||
@@ -144,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
|
||||
@@ -156,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{
|
||||
@@ -165,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
|
||||
@@ -174,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)
|
||||
@@ -212,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
|
||||
@@ -231,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{
|
||||
@@ -245,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)
|
||||
@@ -306,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())
|
||||
|
||||
@@ -316,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
|
||||
@@ -328,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 {
|
||||
@@ -348,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)
|
||||
@@ -378,13 +389,12 @@ type StashBoxBatchTagInput struct {
|
||||
}
|
||||
|
||||
func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxBatchTagInput) int {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
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
|
||||
return fmt.Errorf("invalid stash_box_index %d", input.Endpoint)
|
||||
}
|
||||
box := boxes[input.Endpoint]
|
||||
|
||||
@@ -432,7 +442,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
|
||||
@@ -490,13 +500,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))
|
||||
@@ -510,19 +519,20 @@ 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) {
|
||||
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
|
||||
return fmt.Errorf("invalid stash_box_index %d", input.Endpoint)
|
||||
}
|
||||
box := boxes[input.Endpoint]
|
||||
|
||||
@@ -617,13 +627,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))
|
||||
@@ -637,6 +646,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 {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
type SceneService interface {
|
||||
Create(ctx context.Context, input *models.Scene, fileIDs []models.FileID, coverImage []byte) (*models.Scene, error)
|
||||
AssignFile(ctx context.Context, sceneID int, fileID models.FileID) error
|
||||
Merge(ctx context.Context, sourceIDs []int, destinationID int, values models.ScenePartial) error
|
||||
Merge(ctx context.Context, sourceIDs []int, destinationID int, fileDeleter *scene.FileDeleter, options scene.MergeOptions) error
|
||||
Destroy(ctx context.Context, scene *models.Scene, fileDeleter *scene.FileDeleter, deleteGenerated, deleteFile bool) error
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,9 @@ func (s *SceneServer) StreamSceneDirect(scene *models.Scene, w http.ResponseWrit
|
||||
func (s *SceneServer) ServeScreenshot(scene *models.Scene, w http.ResponseWriter, r *http.Request) {
|
||||
var cover []byte
|
||||
readTxnErr := txn.WithReadTxn(r.Context(), s.TxnManager, func(ctx context.Context) error {
|
||||
cover, _ = s.SceneCoverGetter.GetCover(ctx, scene.ID)
|
||||
return nil
|
||||
var err error
|
||||
cover, err = s.SceneCoverGetter.GetCover(ctx, scene.ID)
|
||||
return err
|
||||
})
|
||||
if errors.Is(readTxnErr, context.Canceled) {
|
||||
return
|
||||
|
||||
730
internal/manager/task/clean_generated.go
Normal file
730
internal/manager/task/clean_generated.go
Normal file
@@ -0,0 +1,730 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
)
|
||||
|
||||
type CleanGeneratedOptions struct {
|
||||
BlobFiles bool `json:"blobs"`
|
||||
|
||||
Sprites bool `json:"sprites"`
|
||||
Screenshots bool `json:"screenshots"`
|
||||
Transcodes bool `json:"transcodes"`
|
||||
|
||||
Markers bool `json:"markers"`
|
||||
|
||||
ImageThumbnails bool `json:"imageThumbnails"`
|
||||
|
||||
DryRun bool `json:"dryRun"`
|
||||
}
|
||||
|
||||
type BlobCleaner interface {
|
||||
EntryExists(ctx context.Context, checksum string) (bool, error)
|
||||
}
|
||||
|
||||
type CleanGeneratedJob struct {
|
||||
Options CleanGeneratedOptions
|
||||
|
||||
Paths *paths.Paths
|
||||
BlobsStorageType config.BlobsStorageType
|
||||
VideoFileNamingAlgorithm models.HashAlgorithm
|
||||
|
||||
BlobCleaner BlobCleaner
|
||||
Repository models.Repository
|
||||
|
||||
dryRunPrefix string
|
||||
totalTasks int
|
||||
tasksComplete int
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) deleteFile(path string) {
|
||||
if j.Options.DryRun {
|
||||
logger.Debugf("would delete file: %s", path)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
logger.Errorf("error deleting file %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) deleteDir(path string) {
|
||||
if j.Options.DryRun {
|
||||
logger.Debugf("would delete file: %s", path)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
logger.Errorf("error deleting directory %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) countTasks() int {
|
||||
tasks := 0
|
||||
|
||||
if j.Options.BlobFiles {
|
||||
tasks++
|
||||
}
|
||||
if j.Options.Sprites {
|
||||
tasks++
|
||||
}
|
||||
if j.Options.Screenshots {
|
||||
tasks++
|
||||
}
|
||||
if j.Options.Transcodes {
|
||||
tasks++
|
||||
}
|
||||
if j.Options.Markers {
|
||||
tasks++
|
||||
}
|
||||
if j.Options.ImageThumbnails {
|
||||
tasks++
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) taskComplete(progress *job.Progress) {
|
||||
j.tasksComplete++
|
||||
progress.SetPercent(float64(j.tasksComplete) / float64(j.totalTasks))
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) logError(err error) {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
j.tasksComplete = 0
|
||||
|
||||
if !j.BlobsStorageType.IsValid() {
|
||||
return fmt.Errorf("invalid blobs storage type: %s", j.BlobsStorageType)
|
||||
}
|
||||
|
||||
if !j.VideoFileNamingAlgorithm.IsValid() {
|
||||
return fmt.Errorf("invalid video file naming algorithm: %s", j.VideoFileNamingAlgorithm)
|
||||
}
|
||||
|
||||
if j.Options.DryRun {
|
||||
j.dryRunPrefix = "[dry run] "
|
||||
}
|
||||
|
||||
logger.Infof("Cleaning generated files %s", j.dryRunPrefix)
|
||||
|
||||
j.totalTasks = j.countTasks()
|
||||
|
||||
if j.Options.BlobFiles {
|
||||
progress.ExecuteTask("Cleaning blob files", func() {
|
||||
if err := j.cleanBlobFiles(ctx, progress); err != nil {
|
||||
j.logError(fmt.Errorf("error cleaning blob files: %w", err))
|
||||
}
|
||||
})
|
||||
j.taskComplete(progress)
|
||||
}
|
||||
|
||||
if j.Options.Sprites {
|
||||
progress.ExecuteTask("Cleaning sprite files", func() {
|
||||
if err := j.cleanSpriteFiles(ctx, progress); err != nil {
|
||||
j.logError(fmt.Errorf("error cleaning sprite files: %w", err))
|
||||
}
|
||||
})
|
||||
j.taskComplete(progress)
|
||||
}
|
||||
|
||||
if j.Options.Screenshots {
|
||||
progress.ExecuteTask("Cleaning screenshot files", func() {
|
||||
if err := j.cleanScreenshotFiles(ctx, progress); err != nil {
|
||||
j.logError(fmt.Errorf("error cleaning screenshot files: %w", err))
|
||||
}
|
||||
})
|
||||
j.taskComplete(progress)
|
||||
}
|
||||
|
||||
if j.Options.Transcodes {
|
||||
progress.ExecuteTask("Cleaning transcode files", func() {
|
||||
if err := j.cleanTranscodeFiles(ctx, progress); err != nil {
|
||||
j.logError(fmt.Errorf("error cleaning transcode files: %w", err))
|
||||
}
|
||||
})
|
||||
j.taskComplete(progress)
|
||||
}
|
||||
|
||||
if j.Options.Markers {
|
||||
progress.ExecuteTask("Cleaning marker files", func() {
|
||||
if err := j.cleanMarkerFiles(ctx, progress); err != nil {
|
||||
j.logError(fmt.Errorf("error cleaning marker files: %w", err))
|
||||
}
|
||||
})
|
||||
j.taskComplete(progress)
|
||||
}
|
||||
|
||||
if j.Options.ImageThumbnails {
|
||||
progress.ExecuteTask("Cleaning thumbnail files", func() {
|
||||
if err := j.cleanThumbnailFiles(ctx, progress); err != nil {
|
||||
j.logError(fmt.Errorf("error cleaning thumbnail files: %w", err))
|
||||
}
|
||||
})
|
||||
j.taskComplete(progress)
|
||||
}
|
||||
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Finished cleaning generated files")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) setTaskProgress(taskProgress float64, progress *job.Progress) {
|
||||
progress.SetPercent((float64(j.tasksComplete) + taskProgress) / float64(j.totalTasks))
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) logDelete(format string, args ...interface{}) {
|
||||
logger.Infof(j.dryRunPrefix+format, args...)
|
||||
}
|
||||
|
||||
// estimates the progress by the hash prefix - first two characters
|
||||
// this is a rough estimate, but it's better than nothing
|
||||
// the prefix ranges from 00 to ff
|
||||
func (j *CleanGeneratedJob) estimateProgress(hashPrefix string) (float64, error) {
|
||||
toInt, err := strconv.ParseInt(hashPrefix, 16, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
const total = 256 // ff
|
||||
return float64(toInt) / total, nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) setProgressFromFilename(prefix string, progress *job.Progress) {
|
||||
p, err := j.estimateProgress(prefix)
|
||||
if err != nil {
|
||||
logger.Errorf("error estimating progress: %v", err)
|
||||
return
|
||||
}
|
||||
j.setTaskProgress(p, progress)
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getIntraFolderPrefix(basename string) (string, error) {
|
||||
var hash string
|
||||
_, err := fmt.Sscanf(basename, "%2x", &hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash), nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getBlobFileHash(basename string) (string, error) {
|
||||
var hash string
|
||||
_, err := fmt.Sscanf(basename, "%32x", &hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash), nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) cleanBlobFiles(ctx context.Context, progress *job.Progress) error {
|
||||
if job.IsCancelled(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if j.BlobsStorageType != config.BlobStorageTypeFilesystem {
|
||||
logger.Debugf("skipping blob file cleanup, storage type is not filesystem")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Cleaning blob files")
|
||||
|
||||
// walk through the blob directory
|
||||
if err := filepath.Walk(j.Paths.Blobs, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
if path == j.Paths.Blobs {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ignore any directory that isn't a two character hash prefix
|
||||
_, err := j.getIntraFolderPrefix(info.Name())
|
||||
if err != nil {
|
||||
logger.Warnf("Ignoring unknown directory: %s", path)
|
||||
return fs.SkipDir
|
||||
}
|
||||
|
||||
// estimate progress by the hash prefix
|
||||
if filepath.Dir(path) == j.Paths.Blobs {
|
||||
hashPrefix := filepath.Base(path)
|
||||
j.setProgressFromFilename(hashPrefix, progress)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
blobname := info.Name()
|
||||
|
||||
// ignore any files that aren't a 32 character hash
|
||||
_, err = j.getBlobFileHash(blobname)
|
||||
if err != nil {
|
||||
logger.Warnf("ignoring unknown blob file: %s", blobname)
|
||||
return nil
|
||||
}
|
||||
|
||||
// if blob entry does not exist, delete the file
|
||||
if err := j.Repository.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
exists, err := j.BlobCleaner.EntryExists(ctx, blobname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
j.logDelete("deleting unused blob file: %s", blobname)
|
||||
j.deleteFile(path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Errorf("error checking blob entry: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getScenesWithHash(ctx context.Context, hash string) ([]*models.Scene, error) {
|
||||
fp := models.Fingerprint{
|
||||
Fingerprint: hash,
|
||||
}
|
||||
|
||||
if j.VideoFileNamingAlgorithm == models.HashAlgorithmMd5 {
|
||||
fp.Type = models.FingerprintTypeMD5
|
||||
} else {
|
||||
fp.Type = models.FingerprintTypeOshash
|
||||
}
|
||||
|
||||
return j.Repository.Scene.FindByFingerprints(ctx, []models.Fingerprint{fp})
|
||||
}
|
||||
|
||||
const (
|
||||
md5Length = 32
|
||||
oshashLength = 16
|
||||
)
|
||||
|
||||
func (j *CleanGeneratedJob) hashPatternPrefix() string {
|
||||
hashLen := oshashLength
|
||||
if j.VideoFileNamingAlgorithm == models.HashAlgorithmMd5 {
|
||||
hashLen = md5Length
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%%%dx", hashLen)
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getSpriteFileHash(basename string) (string, error) {
|
||||
patternPrefix := j.hashPatternPrefix()
|
||||
spritePattern := patternPrefix + "_sprite.jpg"
|
||||
|
||||
var hash string
|
||||
_, err := fmt.Sscanf(basename, spritePattern, &hash)
|
||||
if err != nil {
|
||||
// also try thumbs
|
||||
thumbPattern := patternPrefix + "_thumbs.vtt"
|
||||
_, err = fmt.Sscanf(basename, thumbPattern, &hash)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash), nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) cleanSpriteFiles(ctx context.Context, progress *job.Progress) error {
|
||||
if job.IsCancelled(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Cleaning sprite files")
|
||||
|
||||
// walk through the sprite directory
|
||||
if err := filepath.Walk(j.Paths.Generated.Vtt, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
filename := info.Name()
|
||||
|
||||
hash, err := j.getSpriteFileHash(filename)
|
||||
if err != nil {
|
||||
logger.Warnf("Ignoring unknown sprite file: %s", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
j.setProgressFromFilename(hash[0:2], progress)
|
||||
|
||||
var exists []*models.Scene
|
||||
|
||||
if err := j.Repository.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
exists, err = j.getScenesWithHash(ctx, hash)
|
||||
return err
|
||||
}); err != nil {
|
||||
logger.Errorf("error checking scene entry for sprite: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(exists) == 0 {
|
||||
j.logDelete("deleting unused sprite file: %s", filename)
|
||||
j.deleteFile(path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) cleanSceneFiles(ctx context.Context, path string, typ string, getSceneFileHash func(filename string) (string, error), progress *job.Progress) error {
|
||||
if job.IsCancelled(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Cleaning %s files", typ)
|
||||
|
||||
// walk through the sprite directory
|
||||
if err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := info.Name()
|
||||
hash, err := getSceneFileHash(filename)
|
||||
if err != nil {
|
||||
logger.Warnf("Ignoring unknown %s file: %s", typ, filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
j.setProgressFromFilename(hash[0:2], progress)
|
||||
|
||||
var exists []*models.Scene
|
||||
|
||||
if err := j.Repository.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
exists, err = j.getScenesWithHash(ctx, hash)
|
||||
return err
|
||||
}); err != nil {
|
||||
logger.Errorf("error checking scene entry: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(exists) == 0 {
|
||||
j.logDelete("deleting unused %s file: %s", typ, filename)
|
||||
j.deleteFile(path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getScreenshotFileHash(basename string) (string, error) {
|
||||
var hash string
|
||||
var ext string
|
||||
// include the extension - which could be mp4/jpg/webp
|
||||
_, err := fmt.Sscanf(basename, j.hashPatternPrefix()+".%s", &hash, &ext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash), nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) cleanScreenshotFiles(ctx context.Context, progress *job.Progress) error {
|
||||
return j.cleanSceneFiles(ctx, j.Paths.Generated.Screenshots, "screenshot", j.getScreenshotFileHash, progress)
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getTranscodeFileHash(basename string) (string, error) {
|
||||
var hash string
|
||||
_, err := fmt.Sscanf(basename, j.hashPatternPrefix()+".mp4", &hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash), nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) cleanTranscodeFiles(ctx context.Context, progress *job.Progress) error {
|
||||
return j.cleanSceneFiles(ctx, j.Paths.Generated.Transcodes, "transcode", j.getTranscodeFileHash, progress)
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getMarkerSceneFileHash(basename string) (string, error) {
|
||||
var hash string
|
||||
_, err := fmt.Sscanf(basename, j.hashPatternPrefix(), &hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash), nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getMarkerFileSeconds(basename string) (int, error) {
|
||||
var ret int
|
||||
var ext string
|
||||
// include the extension - which could be mp4/jpg/webp
|
||||
_, err := fmt.Sscanf(basename, "%d.%s", &ret, &ext)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) cleanMarkerFiles(ctx context.Context, progress *job.Progress) error {
|
||||
if job.IsCancelled(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Cleaning marker files")
|
||||
|
||||
var scenes []*models.Scene
|
||||
var sceneHash string
|
||||
var markers []*models.SceneMarker
|
||||
|
||||
// walk through the markers directory
|
||||
if err := filepath.Walk(j.Paths.Generated.Markers, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// ignore markers directory
|
||||
if path == j.Paths.Generated.Markers {
|
||||
return nil
|
||||
}
|
||||
|
||||
markers = nil
|
||||
|
||||
if filepath.Dir(path) != j.Paths.Generated.Markers {
|
||||
logger.Warnf("Ignoring unknown marker directory: %s", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
sceneHash, err = j.getMarkerSceneFileHash(info.Name())
|
||||
if err != nil {
|
||||
logger.Warnf("Ignoring unknown marker directory: %s", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
j.setProgressFromFilename(sceneHash[0:2], progress)
|
||||
|
||||
// check if the scene exists
|
||||
if err := j.Repository.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
scenes, err = j.getScenesWithHash(ctx, sceneHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking scene entry: %v", err)
|
||||
}
|
||||
|
||||
if len(scenes) == 0 {
|
||||
j.logDelete("deleting unused marker directory: %s", sceneHash)
|
||||
j.deleteDir(path)
|
||||
} else {
|
||||
// get the markers now
|
||||
for _, scene := range scenes {
|
||||
thisMarkers, err := j.Repository.SceneMarker.FindBySceneID(ctx, scene.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting markers for scene: %v", err)
|
||||
}
|
||||
markers = append(markers, thisMarkers...)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
filename := info.Name()
|
||||
seconds, err := j.getMarkerFileSeconds(filename)
|
||||
if err != nil {
|
||||
logger.Warnf("Ignoring unknown marker file: %s", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// scenes should be set by the directory walk
|
||||
hash := filepath.Base(filepath.Dir(path))
|
||||
if hash != sceneHash {
|
||||
logger.Errorf("internal error: scene hash mismatch: %s != %s", hash, sceneHash)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(scenes) == 0 {
|
||||
logger.Errorf("no scenes found for marker file: %s", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// find the marker
|
||||
var marker *models.SceneMarker
|
||||
for _, m := range markers {
|
||||
if int(m.Seconds) == seconds {
|
||||
marker = m
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if marker == nil {
|
||||
// not found, delete the file
|
||||
j.logDelete("deleting unused marker file: %s", filename)
|
||||
j.deleteFile(path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getImagesWithHash(ctx context.Context, checksum string) ([]*models.Image, error) {
|
||||
var exists []*models.Image
|
||||
if err := j.Repository.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
// if scene entry does not exist, delete the file
|
||||
var err error
|
||||
exists, err = j.Repository.Image.FindByChecksum(ctx, checksum)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getThumbnailFileHash(basename string) (string, error) {
|
||||
var (
|
||||
hash string
|
||||
width int
|
||||
ext string
|
||||
)
|
||||
// include the extension - which could be jpg/webp
|
||||
_, err := fmt.Sscanf(basename, "%32x_%d.%s", &hash, &width, &ext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", hash), nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) cleanThumbnailFiles(ctx context.Context, progress *job.Progress) error {
|
||||
if job.IsCancelled(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Cleaning image thumbnail files")
|
||||
|
||||
// walk through the sprite directory
|
||||
if err := filepath.Walk(j.Paths.Generated.Thumbnails, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
if path == j.Paths.Generated.Thumbnails {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensure the directory is a hash prefix
|
||||
_, err := j.getIntraFolderPrefix(info.Name())
|
||||
if err != nil {
|
||||
logger.Warnf("Ignoring unknown thumbnail directory: %s", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// estimate progress by the hash prefix
|
||||
if filepath.Dir(path) == j.Paths.Generated.Thumbnails {
|
||||
hashPrefix := filepath.Base(path)
|
||||
j.setProgressFromFilename(hashPrefix, progress)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
filename := info.Name()
|
||||
checksum, err := j.getThumbnailFileHash(filename)
|
||||
if err != nil {
|
||||
logger.Warnf("Ignoring unknown thumbnail file: %s", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
exists, err := j.getImagesWithHash(ctx, checksum)
|
||||
if err != nil {
|
||||
logger.Errorf("error checking image entry: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(exists) == 0 {
|
||||
j.logDelete("deleting unused thumbnail file: %s", filename)
|
||||
j.deleteFile(path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
241
internal/manager/task/download_ffmpeg.go
Normal file
241
internal/manager/task/download_ffmpeg.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
type DownloadFFmpegJob struct {
|
||||
ConfigDirectory string
|
||||
OnComplete func(ctx context.Context)
|
||||
urls []string
|
||||
downloaded int
|
||||
}
|
||||
|
||||
func (s *DownloadFFmpegJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
if err := s.download(ctx, progress); err != nil {
|
||||
if job.IsCancelled(ctx) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if s.OnComplete != nil {
|
||||
s.OnComplete(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DownloadFFmpegJob) setTaskProgress(taskProgress float64, progress *job.Progress) {
|
||||
progress.SetPercent((float64(s.downloaded) + taskProgress) / float64(len(s.urls)))
|
||||
}
|
||||
|
||||
func (s *DownloadFFmpegJob) download(ctx context.Context, progress *job.Progress) error {
|
||||
s.urls = ffmpeg.GetFFmpegURL()
|
||||
|
||||
// set steps based on the number of URLs
|
||||
|
||||
for _, url := range s.urls {
|
||||
err := s.downloadSingle(ctx, url, progress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.downloaded++
|
||||
}
|
||||
|
||||
// validate that the urls contained what we needed
|
||||
executables := []string{fsutil.GetExeName("ffmpeg"), fsutil.GetExeName("ffprobe")}
|
||||
for _, executable := range executables {
|
||||
_, err := os.Stat(filepath.Join(s.ConfigDirectory, executable))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type downloadProgressReader struct {
|
||||
io.Reader
|
||||
setProgress func(taskProgress float64)
|
||||
bytesRead int64
|
||||
total int64
|
||||
}
|
||||
|
||||
func (r *downloadProgressReader) Read(p []byte) (int, error) {
|
||||
read, err := r.Reader.Read(p)
|
||||
if err == nil {
|
||||
r.bytesRead += int64(read)
|
||||
if r.total > 0 {
|
||||
progress := float64(r.bytesRead) / float64(r.total)
|
||||
r.setProgress(progress)
|
||||
}
|
||||
}
|
||||
|
||||
return read, err
|
||||
}
|
||||
|
||||
func (s *DownloadFFmpegJob) downloadSingle(ctx context.Context, url string, progress *job.Progress) error {
|
||||
if url == "" {
|
||||
return fmt.Errorf("no ffmpeg url for this platform")
|
||||
}
|
||||
|
||||
configDirectory := s.ConfigDirectory
|
||||
|
||||
// Configure where we want to download the archive
|
||||
urlBase := path.Base(url)
|
||||
archivePath := filepath.Join(configDirectory, urlBase)
|
||||
_ = os.Remove(archivePath) // remove archive if it already exists
|
||||
out, err := os.Create(archivePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
logger.Infof("Downloading %s...", url)
|
||||
|
||||
progress.ExecuteTask(fmt.Sprintf("Downloading %s", url), func() {
|
||||
err = s.downloadFile(ctx, url, out, progress)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download ffmpeg from %s: %w", url, err)
|
||||
}
|
||||
|
||||
logger.Info("Downloading complete")
|
||||
|
||||
logger.Infof("Unzipping %s...", archivePath)
|
||||
progress.ExecuteTask(fmt.Sprintf("Unzipping %s", archivePath), func() {
|
||||
err = s.unzip(archivePath)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unzip ffmpeg archive: %w", err)
|
||||
}
|
||||
|
||||
// On OSX or Linux set downloaded files permissions
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
|
||||
_, err = os.Stat(filepath.Join(configDirectory, "ffmpeg"))
|
||||
if !os.IsNotExist(err) {
|
||||
if err = os.Chmod(filepath.Join(configDirectory, "ffmpeg"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = os.Stat(filepath.Join(configDirectory, "ffprobe"))
|
||||
if !os.IsNotExist(err) {
|
||||
if err := os.Chmod(filepath.Join(configDirectory, "ffprobe"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: In future possible clear xattr to allow running on osx without user intervention
|
||||
// TODO: this however may not be required.
|
||||
// xattr -c /path/to/binary -- xattr.Remove(path, "com.apple.quarantine")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DownloadFFmpegJob) downloadFile(ctx context.Context, url string, out *os.File, progress *job.Progress) error {
|
||||
// Make the HTTP request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transport := &http.Transport{Proxy: http.ProxyFromEnvironment}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check server response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
reader := &downloadProgressReader{
|
||||
Reader: resp.Body,
|
||||
total: resp.ContentLength,
|
||||
setProgress: func(taskProgress float64) {
|
||||
s.setTaskProgress(taskProgress, progress)
|
||||
},
|
||||
}
|
||||
|
||||
// Write the response to the archive file location
|
||||
if _, err := io.Copy(out, reader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mime := resp.Header.Get("Content-Type")
|
||||
if mime != "application/zip" { // try detecting MIME type since some servers don't return the correct one
|
||||
data := make([]byte, 500) // http.DetectContentType only reads up to 500 bytes
|
||||
_, _ = out.ReadAt(data, 0)
|
||||
mime = http.DetectContentType(data)
|
||||
}
|
||||
|
||||
if mime != "application/zip" {
|
||||
return fmt.Errorf("downloaded file is not a zip archive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DownloadFFmpegJob) unzip(src string) error {
|
||||
zipReader, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipReader.Close()
|
||||
|
||||
for _, f := range zipReader.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
filename := f.FileInfo().Name()
|
||||
if filename != "ffprobe" && filename != "ffmpeg" && filename != "ffprobe.exe" && filename != "ffmpeg.exe" {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unzippedPath := filepath.Join(s.ConfigDirectory, filename)
|
||||
unzippedOutput, err := os.Create(unzippedPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(unzippedOutput, rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := unzippedOutput.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
153
internal/manager/task/migrate.go
Normal file
153
internal/manager/task/migrate.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/sqlite"
|
||||
)
|
||||
|
||||
type migrateJobConfig interface {
|
||||
GetBackupDirectoryPath() string
|
||||
GetBackupDirectoryPathOrDefault() string
|
||||
}
|
||||
|
||||
type MigrateJob struct {
|
||||
BackupPath string
|
||||
Config migrateJobConfig
|
||||
Database *sqlite.Database
|
||||
}
|
||||
|
||||
func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
required, err := s.required()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if required == 0 {
|
||||
logger.Infof("database is already at the latest schema version")
|
||||
return nil
|
||||
}
|
||||
|
||||
// set the number of tasks = required steps + optimise
|
||||
progress.SetTotal(int(required + 1))
|
||||
|
||||
database := s.Database
|
||||
|
||||
// always backup so that we can roll back to the previous version if
|
||||
// migration fails
|
||||
backupPath := s.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 := s.runMigrations(ctx, progress); 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 s.BackupPath == "" {
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
logger.Warnf("error removing unwanted database backup (%s): %s", backupPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MigrateJob) required() (uint, error) {
|
||||
database := s.Database
|
||||
|
||||
m, err := sqlite.NewMigrator(database)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
defer m.Close()
|
||||
|
||||
currentSchemaVersion := m.CurrentSchemaVersion()
|
||||
targetSchemaVersion := m.RequiredSchemaVersion()
|
||||
|
||||
if targetSchemaVersion < currentSchemaVersion {
|
||||
// shouldn't happen
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return targetSchemaVersion - currentSchemaVersion, nil
|
||||
}
|
||||
|
||||
func (s *MigrateJob) runMigrations(ctx context.Context, progress *job.Progress) error {
|
||||
database := s.Database
|
||||
|
||||
m, err := sqlite.NewMigrator(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer m.Close()
|
||||
|
||||
for {
|
||||
currentSchemaVersion := m.CurrentSchemaVersion()
|
||||
targetSchemaVersion := m.RequiredSchemaVersion()
|
||||
|
||||
if currentSchemaVersion >= targetSchemaVersion {
|
||||
break
|
||||
}
|
||||
|
||||
var err error
|
||||
progress.ExecuteTask(fmt.Sprintf("Migrating database to schema version %d", currentSchemaVersion+1), func() {
|
||||
err = m.RunMigration(ctx, currentSchemaVersion+1)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running migration for schema %d: %s", currentSchemaVersion+1, err)
|
||||
}
|
||||
|
||||
progress.Increment()
|
||||
}
|
||||
|
||||
// reinitialise the database
|
||||
if err := database.ReInitialise(); err != nil {
|
||||
return fmt.Errorf("error reinitialising database: %s", err)
|
||||
}
|
||||
|
||||
// optimise the database
|
||||
progress.ExecuteTask("Optimising database", func() {
|
||||
err = database.Optimise(ctx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error optimising database: %s", err)
|
||||
}
|
||||
|
||||
progress.Increment()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -26,7 +26,7 @@ type MigrateBlobsJob struct {
|
||||
DeleteOld bool
|
||||
}
|
||||
|
||||
func (j *MigrateBlobsJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *MigrateBlobsJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
var (
|
||||
count int
|
||||
err error
|
||||
@@ -37,13 +37,12 @@ func (j *MigrateBlobsJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Error counting blobs: %s", err.Error())
|
||||
return
|
||||
return fmt.Errorf("error counting blobs: %w", err)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
logger.Infof("No blobs to migrate")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Migrating %d blobs", count)
|
||||
@@ -54,12 +53,11 @@ func (j *MigrateBlobsJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Cancelled migrating blobs")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Error migrating blobs: %v", err)
|
||||
return
|
||||
return fmt.Errorf("error migrating blobs: %w", err)
|
||||
}
|
||||
|
||||
// run a vacuum to reclaim space
|
||||
@@ -71,6 +69,7 @@ func (j *MigrateBlobsJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
})
|
||||
|
||||
logger.Infof("Finished migrating blobs")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *MigrateBlobsJob) countBlobs(ctx context.Context) (int, error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package task
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -21,7 +22,7 @@ type MigrateSceneScreenshotsJob struct {
|
||||
TxnManager txn.Manager
|
||||
}
|
||||
|
||||
func (j *MigrateSceneScreenshotsJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *MigrateSceneScreenshotsJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
var err error
|
||||
progress.ExecuteTask("Counting files", func() {
|
||||
var count int
|
||||
@@ -30,8 +31,7 @@ func (j *MigrateSceneScreenshotsJob) Execute(ctx context.Context, progress *job.
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Error counting files: %s", err.Error())
|
||||
return
|
||||
return fmt.Errorf("error counting files: %w", err)
|
||||
}
|
||||
|
||||
progress.ExecuteTask("Migrating files", func() {
|
||||
@@ -40,15 +40,15 @@ func (j *MigrateSceneScreenshotsJob) Execute(ctx context.Context, progress *job.
|
||||
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Cancelled migrating scene screenshots")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Error migrating scene screenshots: %v", err)
|
||||
return
|
||||
return fmt.Errorf("error migrating scene screenshots: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("Finished migrating scene screenshots")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *MigrateSceneScreenshotsJob) countFiles(ctx context.Context) (int, error) {
|
||||
|
||||
@@ -30,13 +30,13 @@ type InstallPackagesJob struct {
|
||||
Packages []*models.PackageSpecInput
|
||||
}
|
||||
|
||||
func (j *InstallPackagesJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *InstallPackagesJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
progress.SetTotal(len(j.Packages))
|
||||
|
||||
for _, p := range j.Packages {
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Cancelled installing packages")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Installing package %s", p.ID)
|
||||
@@ -53,6 +53,7 @@ func (j *InstallPackagesJob) Execute(ctx context.Context, progress *job.Progress
|
||||
}
|
||||
|
||||
logger.Infof("Finished installing packages")
|
||||
return nil
|
||||
}
|
||||
|
||||
type UpdatePackagesJob struct {
|
||||
@@ -60,13 +61,12 @@ type UpdatePackagesJob struct {
|
||||
Packages []*models.PackageSpecInput
|
||||
}
|
||||
|
||||
func (j *UpdatePackagesJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *UpdatePackagesJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
// if no packages are specified, update all
|
||||
if len(j.Packages) == 0 {
|
||||
installed, err := j.PackageManager.InstalledStatus(ctx)
|
||||
if err != nil {
|
||||
logger.Errorf("Error getting installed packages: %v", err)
|
||||
return
|
||||
return fmt.Errorf("error getting installed packages: %w", err)
|
||||
}
|
||||
|
||||
for _, p := range installed {
|
||||
@@ -84,7 +84,7 @@ func (j *UpdatePackagesJob) Execute(ctx context.Context, progress *job.Progress)
|
||||
for _, p := range j.Packages {
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Cancelled updating packages")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Updating package %s", p.ID)
|
||||
@@ -101,6 +101,7 @@ func (j *UpdatePackagesJob) Execute(ctx context.Context, progress *job.Progress)
|
||||
}
|
||||
|
||||
logger.Infof("Finished updating packages")
|
||||
return nil
|
||||
}
|
||||
|
||||
type UninstallPackagesJob struct {
|
||||
@@ -108,13 +109,13 @@ type UninstallPackagesJob struct {
|
||||
Packages []*models.PackageSpecInput
|
||||
}
|
||||
|
||||
func (j *UninstallPackagesJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *UninstallPackagesJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
progress.SetTotal(len(j.Packages))
|
||||
|
||||
for _, p := range j.Packages {
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Cancelled installing packages")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Uninstalling package %s", p.ID)
|
||||
@@ -131,4 +132,5 @@ func (j *UninstallPackagesJob) Execute(ctx context.Context, progress *job.Progre
|
||||
}
|
||||
|
||||
logger.Infof("Finished uninstalling packages")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type autoTagJob struct {
|
||||
cache match.Cache
|
||||
}
|
||||
|
||||
func (j *autoTagJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *autoTagJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
begin := time.Now()
|
||||
|
||||
input := j.input
|
||||
@@ -38,6 +38,7 @@ func (j *autoTagJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
}
|
||||
|
||||
logger.Infof("Finished auto-tag after %s", time.Since(begin).String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *autoTagJob) isFileBasedAutoTag(input AutoTagMetadataInput) bool {
|
||||
@@ -162,6 +163,11 @@ func (j *autoTagJob) autoTagPerformers(ctx context.Context, progress *job.Progre
|
||||
return fmt.Errorf("performer with id %s not found", performerId)
|
||||
}
|
||||
|
||||
if performer.IgnoreAutoTag {
|
||||
logger.Infof("Skipping performer %s because auto-tag is disabled", performer.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := performer.LoadAliases(ctx, r.Performer); err != nil {
|
||||
return fmt.Errorf("loading aliases for performer %d: %w", performer.ID, err)
|
||||
}
|
||||
@@ -253,6 +259,11 @@ func (j *autoTagJob) autoTagStudios(ctx context.Context, progress *job.Progress,
|
||||
return fmt.Errorf("studio with id %s not found", studioId)
|
||||
}
|
||||
|
||||
if studio.IgnoreAutoTag {
|
||||
logger.Infof("Skipping studio %s because auto-tag is disabled", studio.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
studios = append(studios, studio)
|
||||
}
|
||||
|
||||
@@ -345,6 +356,11 @@ func (j *autoTagJob) autoTagTags(ctx context.Context, progress *job.Progress, pa
|
||||
return fmt.Errorf("tag with id %s not found", tagId)
|
||||
}
|
||||
|
||||
if tag.IgnoreAutoTag {
|
||||
logger.Infof("Skipping tag %s because auto-tag is disabled", tag.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
)
|
||||
|
||||
@@ -31,7 +32,7 @@ type cleanJob struct {
|
||||
scanSubs *subscriptionManager
|
||||
}
|
||||
|
||||
func (j *cleanJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *cleanJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
logger.Infof("Starting cleaning of tracked files")
|
||||
start := time.Now()
|
||||
if j.input.DryRun {
|
||||
@@ -46,7 +47,7 @@ func (j *cleanJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
j.cleanEmptyGalleries(ctx)
|
||||
@@ -54,6 +55,7 @@ func (j *cleanJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
j.scanSubs.notify()
|
||||
elapsed := time.Since(start)
|
||||
logger.Info(fmt.Sprintf("Finished Cleaning (%s)", elapsed))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *cleanJob) cleanEmptyGalleries(ctx context.Context) {
|
||||
@@ -129,7 +131,7 @@ func (j *cleanJob) deleteGallery(ctx context.Context, id int) {
|
||||
return err
|
||||
}
|
||||
|
||||
pluginCache.RegisterPostHooks(ctx, id, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
pluginCache.RegisterPostHooks(ctx, id, hook.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
Checksum: g.PrimaryChecksum(),
|
||||
Path: g.Path,
|
||||
}, nil)
|
||||
@@ -302,7 +304,7 @@ func (h *cleanHandler) handleRelatedScenes(ctx context.Context, fileDeleter *fil
|
||||
return err
|
||||
}
|
||||
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, scene.ID, plugin.SceneDestroyPost, plugin.SceneDestroyInput{
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, scene.ID, hook.SceneDestroyPost, plugin.SceneDestroyInput{
|
||||
Checksum: scene.Checksum,
|
||||
OSHash: scene.OSHash,
|
||||
Path: scene.Path,
|
||||
@@ -349,7 +351,7 @@ func (h *cleanHandler) handleRelatedGalleries(ctx context.Context, fileID models
|
||||
return err
|
||||
}
|
||||
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, g.ID, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, g.ID, hook.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
Checksum: g.PrimaryChecksum(),
|
||||
Path: g.Path,
|
||||
}, nil)
|
||||
@@ -389,7 +391,7 @@ func (h *cleanHandler) deleteRelatedFolderGalleries(ctx context.Context, folderI
|
||||
return err
|
||||
}
|
||||
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, g.ID, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, g.ID, hook.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
// No checksum for folders
|
||||
// Checksum: g.Checksum(),
|
||||
Path: g.Path,
|
||||
@@ -423,7 +425,7 @@ func (h *cleanHandler) handleRelatedImages(ctx context.Context, fileDeleter *fil
|
||||
return err
|
||||
}
|
||||
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, i.ID, plugin.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
mgr.PluginCache.RegisterPostHooks(ctx, i.ID, hook.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
Checksum: i.Checksum,
|
||||
Path: i.Path,
|
||||
}, nil)
|
||||
|
||||
@@ -527,7 +527,6 @@ func (t *ExportTask) exportScene(ctx context.Context, wg *sync.WaitGroup, jobCha
|
||||
newSceneJSON.Galleries = gallery.GetRefs(galleries)
|
||||
|
||||
newSceneJSON.ResumeTime = s.ResumeTime
|
||||
newSceneJSON.PlayCount = s.PlayCount
|
||||
newSceneJSON.PlayDuration = s.PlayDuration
|
||||
|
||||
performers, err := performerReader.FindBySceneID(ctx, s.ID)
|
||||
|
||||
@@ -31,6 +31,7 @@ type GenerateMetadataInput struct {
|
||||
Phashes bool `json:"phashes"`
|
||||
InteractiveHeatmapsSpeeds bool `json:"interactiveHeatmapsSpeeds"`
|
||||
ClipPreviews bool `json:"clipPreviews"`
|
||||
ImageThumbnails bool `json:"imageThumbnails"`
|
||||
// scene ids to generate for
|
||||
SceneIDs []string `json:"sceneIDs"`
|
||||
// marker ids to generate for
|
||||
@@ -60,6 +61,8 @@ type GenerateJob struct {
|
||||
|
||||
overwrite bool
|
||||
fileNamingAlgo models.HashAlgorithm
|
||||
|
||||
totals totalsGenerate
|
||||
}
|
||||
|
||||
type totalsGenerate struct {
|
||||
@@ -72,11 +75,12 @@ type totalsGenerate struct {
|
||||
phashes int64
|
||||
interactiveHeatmapSpeeds int64
|
||||
clipPreviews int64
|
||||
imageThumbnails int64
|
||||
|
||||
tasks int
|
||||
}
|
||||
|
||||
func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
var scenes []*models.Scene
|
||||
var err error
|
||||
var markers []*models.SceneMarker
|
||||
@@ -93,7 +97,6 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
go func() {
|
||||
defer close(queue)
|
||||
|
||||
var totals totalsGenerate
|
||||
sceneIDs, err := stringslice.StringSliceToIntSlice(j.input.SceneIDs)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
@@ -116,7 +119,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.Scene
|
||||
if len(j.input.SceneIDs) == 0 && len(j.input.MarkerIDs) == 0 {
|
||||
totals = j.queueTasks(ctx, g, queue)
|
||||
j.queueTasks(ctx, g, queue)
|
||||
} else {
|
||||
if len(j.input.SceneIDs) > 0 {
|
||||
scenes, err = qb.FindMany(ctx, sceneIDs)
|
||||
@@ -125,7 +128,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
return err
|
||||
}
|
||||
|
||||
j.queueSceneJobs(ctx, g, s, queue, &totals)
|
||||
j.queueSceneJobs(ctx, g, s, queue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +138,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
return err
|
||||
}
|
||||
for _, m := range markers {
|
||||
j.queueMarkerJob(g, m, queue, &totals)
|
||||
j.queueMarkerJob(g, m, queue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +149,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
return
|
||||
}
|
||||
|
||||
totals := j.totals
|
||||
logMsg := "Generating"
|
||||
if j.input.Covers {
|
||||
logMsg += fmt.Sprintf(" %d covers", totals.covers)
|
||||
@@ -174,6 +178,9 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
if j.input.ClipPreviews {
|
||||
logMsg += fmt.Sprintf(" %d Image Clip Previews", totals.clipPreviews)
|
||||
}
|
||||
if j.input.ImageThumbnails {
|
||||
logMsg += fmt.Sprintf(" %d Image Thumbnails", totals.imageThumbnails)
|
||||
}
|
||||
if logMsg == "Generating" {
|
||||
logMsg = "Nothing selected to generate"
|
||||
}
|
||||
@@ -216,16 +223,22 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
logger.Info(fmt.Sprintf("Generate finished (%s)", elapsed))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) totalsGenerate {
|
||||
var totals totalsGenerate
|
||||
func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
|
||||
j.totals = totalsGenerate{}
|
||||
|
||||
j.queueScenesTasks(ctx, g, queue)
|
||||
j.queueImagesTasks(ctx, g, queue)
|
||||
}
|
||||
|
||||
func (j *GenerateJob) queueScenesTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
|
||||
const batchSize = 1000
|
||||
|
||||
findFilter := models.BatchFindFilter(batchSize)
|
||||
@@ -234,26 +247,26 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
|
||||
|
||||
for more := true; more; {
|
||||
if job.IsCancelled(ctx) {
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
scenes, err := scene.Query(ctx, r.Scene, nil, findFilter)
|
||||
if err != nil {
|
||||
logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
for _, ss := range scenes {
|
||||
if job.IsCancelled(ctx) {
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
if err := ss.LoadFiles(ctx, r.Scene); err != nil {
|
||||
logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
j.queueSceneJobs(ctx, g, ss, queue, &totals)
|
||||
j.queueSceneJobs(ctx, g, ss, queue)
|
||||
}
|
||||
|
||||
if len(scenes) != batchSize {
|
||||
@@ -262,30 +275,37 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
|
||||
*findFilter.Page++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*findFilter.Page = 1
|
||||
for more := j.input.ClipPreviews; more; {
|
||||
func (j *GenerateJob) queueImagesTasks(ctx context.Context, g *generate.Generator, queue chan<- Task) {
|
||||
const batchSize = 1000
|
||||
|
||||
findFilter := models.BatchFindFilter(batchSize)
|
||||
|
||||
r := j.repository
|
||||
|
||||
for more := j.input.ClipPreviews || j.input.ImageThumbnails; more; {
|
||||
if job.IsCancelled(ctx) {
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
images, err := image.Query(ctx, r.Image, nil, findFilter)
|
||||
if err != nil {
|
||||
logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
for _, ss := range images {
|
||||
if job.IsCancelled(ctx) {
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
if err := ss.LoadFiles(ctx, r.Image); err != nil {
|
||||
logger.Errorf("Error encountered queuing files to scan: %s", err.Error())
|
||||
return totals
|
||||
return
|
||||
}
|
||||
|
||||
j.queueImageJob(g, ss, queue, &totals)
|
||||
j.queueImageJob(g, ss, queue)
|
||||
}
|
||||
|
||||
if len(images) != batchSize {
|
||||
@@ -294,8 +314,6 @@ func (j *GenerateJob) queueTasks(ctx context.Context, g *generate.Generator, que
|
||||
*findFilter.Page++
|
||||
}
|
||||
}
|
||||
|
||||
return totals
|
||||
}
|
||||
|
||||
func getGeneratePreviewOptions(optionsInput GeneratePreviewOptionsInput) generate.PreviewOptions {
|
||||
@@ -333,7 +351,7 @@ func getGeneratePreviewOptions(optionsInput GeneratePreviewOptionsInput) generat
|
||||
return ret
|
||||
}
|
||||
|
||||
func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, scene *models.Scene, queue chan<- Task, totals *totalsGenerate) {
|
||||
func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator, scene *models.Scene, queue chan<- Task) {
|
||||
r := j.repository
|
||||
|
||||
if j.input.Covers {
|
||||
@@ -344,8 +362,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
||||
}
|
||||
|
||||
if task.required(ctx) {
|
||||
totals.covers++
|
||||
totals.tasks++
|
||||
j.totals.covers++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
@@ -358,8 +376,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
||||
}
|
||||
|
||||
if task.required() {
|
||||
totals.sprites++
|
||||
totals.tasks++
|
||||
j.totals.sprites++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
@@ -382,13 +400,13 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
||||
|
||||
if task.required() {
|
||||
if task.videoPreviewRequired() {
|
||||
totals.previews++
|
||||
j.totals.previews++
|
||||
}
|
||||
if task.imagePreviewRequired() {
|
||||
totals.imagePreviews++
|
||||
j.totals.imagePreviews++
|
||||
}
|
||||
|
||||
totals.tasks++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
@@ -407,8 +425,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
||||
|
||||
markers := task.markersNeeded(ctx)
|
||||
if markers > 0 {
|
||||
totals.markers += int64(markers)
|
||||
totals.tasks++
|
||||
j.totals.markers += int64(markers)
|
||||
j.totals.tasks++
|
||||
|
||||
queue <- task
|
||||
}
|
||||
@@ -424,8 +442,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
||||
g: g,
|
||||
}
|
||||
if task.required() {
|
||||
totals.transcodes++
|
||||
totals.tasks++
|
||||
j.totals.transcodes++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
@@ -441,8 +459,8 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
||||
}
|
||||
|
||||
if task.required() {
|
||||
totals.phashes++
|
||||
totals.tasks++
|
||||
j.totals.phashes++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
@@ -457,14 +475,14 @@ func (j *GenerateJob) queueSceneJobs(ctx context.Context, g *generate.Generator,
|
||||
}
|
||||
|
||||
if task.required() {
|
||||
totals.interactiveHeatmapSpeeds++
|
||||
totals.tasks++
|
||||
j.totals.interactiveHeatmapSpeeds++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.SceneMarker, queue chan<- Task, totals *totalsGenerate) {
|
||||
func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.SceneMarker, queue chan<- Task) {
|
||||
task := &GenerateMarkersTask{
|
||||
repository: j.repository,
|
||||
Marker: marker,
|
||||
@@ -472,20 +490,35 @@ func (j *GenerateJob) queueMarkerJob(g *generate.Generator, marker *models.Scene
|
||||
fileNamingAlgorithm: j.fileNamingAlgo,
|
||||
generator: g,
|
||||
}
|
||||
totals.markers++
|
||||
totals.tasks++
|
||||
j.totals.markers++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
|
||||
func (j *GenerateJob) queueImageJob(g *generate.Generator, image *models.Image, queue chan<- Task, totals *totalsGenerate) {
|
||||
task := &GenerateClipPreviewTask{
|
||||
Image: *image,
|
||||
Overwrite: j.overwrite,
|
||||
func (j *GenerateJob) queueImageJob(g *generate.Generator, image *models.Image, queue chan<- Task) {
|
||||
if j.input.ImageThumbnails {
|
||||
task := &GenerateImageThumbnailTask{
|
||||
Image: *image,
|
||||
Overwrite: j.overwrite,
|
||||
}
|
||||
|
||||
if task.required() {
|
||||
j.totals.imageThumbnails++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
|
||||
if task.required() {
|
||||
totals.clipPreviews++
|
||||
totals.tasks++
|
||||
queue <- task
|
||||
if j.input.ClipPreviews {
|
||||
task := &GenerateClipPreviewTask{
|
||||
Image: *image,
|
||||
Overwrite: j.overwrite,
|
||||
}
|
||||
|
||||
if task.required() {
|
||||
j.totals.clipPreviews++
|
||||
j.totals.tasks++
|
||||
queue <- task
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
internal/manager/task_generate_image_thumbnail.go
Normal file
79
internal/manager/task_generate_image_thumbnail.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type GenerateImageThumbnailTask struct {
|
||||
Image models.Image
|
||||
Overwrite bool
|
||||
}
|
||||
|
||||
func (t *GenerateImageThumbnailTask) GetDescription() string {
|
||||
return fmt.Sprintf("Generating Thumbnail for image %s", t.Image.Path)
|
||||
}
|
||||
|
||||
func (t *GenerateImageThumbnailTask) Start(ctx context.Context) {
|
||||
if !t.required() {
|
||||
return
|
||||
}
|
||||
|
||||
thumbPath := GetInstance().Paths.Generated.GetThumbnailPath(t.Image.Checksum, models.DefaultGthumbWidth)
|
||||
f := t.Image.Files.Primary()
|
||||
path := f.Base().Path
|
||||
|
||||
logger.Debugf("Generating thumbnail for %s", path)
|
||||
|
||||
mgr := GetInstance()
|
||||
c := mgr.Config
|
||||
|
||||
clipPreviewOptions := image.ClipPreviewOptions{
|
||||
InputArgs: c.GetTranscodeInputArgs(),
|
||||
OutputArgs: c.GetTranscodeOutputArgs(),
|
||||
Preset: c.GetPreviewPreset().String(),
|
||||
}
|
||||
|
||||
encoder := image.NewThumbnailEncoder(mgr.FFMpeg, mgr.FFProbe, clipPreviewOptions)
|
||||
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
|
||||
|
||||
if err != nil {
|
||||
// don't log for animated images
|
||||
if !errors.Is(err, image.ErrNotSupportedForThumbnail) {
|
||||
logger.Errorf("[generator] getting thumbnail for image %s: %w", path, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = fsutil.WriteFile(thumbPath, data)
|
||||
if err != nil {
|
||||
logger.Errorf("[generator] writing thumbnail for image %s: %w", path, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (t *GenerateImageThumbnailTask) required() bool {
|
||||
vf, ok := t.Image.Files.Primary().(models.VisualFile)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if vf.GetHeight() <= models.DefaultGthumbWidth && vf.GetWidth() <= models.DefaultGthumbWidth {
|
||||
return false
|
||||
}
|
||||
|
||||
if t.Overwrite {
|
||||
return true
|
||||
}
|
||||
|
||||
thumbPath := GetInstance().Paths.Generated.GetThumbnailPath(t.Image.Checksum, models.DefaultGthumbWidth)
|
||||
exists, _ := fsutil.FileExists(thumbPath)
|
||||
|
||||
return !exists
|
||||
}
|
||||
@@ -25,19 +25,38 @@ func (t *GeneratePhashTask) Start(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := videophash.Generate(instance.FFMpeg, t.File)
|
||||
if err != nil {
|
||||
logger.Errorf("error generating phash: %s", err.Error())
|
||||
logErrorOutput(err)
|
||||
return
|
||||
var hash int64
|
||||
set := false
|
||||
|
||||
// #4393 - if there is a file with the same oshash, we can use the same phash
|
||||
// only use this if we're not overwriting
|
||||
if !t.Overwrite {
|
||||
existing, err := t.findExistingPhash(ctx)
|
||||
if err != nil {
|
||||
logger.Warnf("Error finding existing phash: %v", err)
|
||||
} else if existing != nil {
|
||||
logger.Infof("Using existing phash for %s", t.File.Path)
|
||||
hash = existing.(int64)
|
||||
set = true
|
||||
}
|
||||
}
|
||||
|
||||
if !set {
|
||||
generated, err := videophash.Generate(instance.FFMpeg, t.File)
|
||||
if err != nil {
|
||||
logger.Errorf("Error generating phash: %v", err)
|
||||
logErrorOutput(err)
|
||||
return
|
||||
}
|
||||
|
||||
hash = int64(*generated)
|
||||
}
|
||||
|
||||
r := t.repository
|
||||
if err := r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
hashValue := int64(*hash)
|
||||
t.File.Fingerprints = t.File.Fingerprints.AppendUnique(models.Fingerprint{
|
||||
Type: models.FingerprintTypePhash,
|
||||
Fingerprint: hashValue,
|
||||
Fingerprint: hash,
|
||||
})
|
||||
|
||||
return r.File.Update(ctx, t.File)
|
||||
@@ -46,6 +65,36 @@ func (t *GeneratePhashTask) Start(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *GeneratePhashTask) findExistingPhash(ctx context.Context) (interface{}, error) {
|
||||
r := t.repository
|
||||
var ret interface{}
|
||||
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
oshash := t.File.Fingerprints.Get(models.FingerprintTypeOshash)
|
||||
|
||||
// find other files with the same oshash
|
||||
files, err := r.File.FindByFingerprint(ctx, models.Fingerprint{
|
||||
Type: models.FingerprintTypeOshash,
|
||||
Fingerprint: oshash,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding files by oshash: %w", err)
|
||||
}
|
||||
|
||||
// find the first file with a phash
|
||||
for _, file := range files {
|
||||
if phash := file.Base().Fingerprints.Get(models.FingerprintTypePhash); phash != nil {
|
||||
ret = phash
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (t *GeneratePhashTask) required() bool {
|
||||
if t.Overwrite {
|
||||
return true
|
||||
|
||||
@@ -34,18 +34,17 @@ func CreateIdentifyJob(input identify.Options) *IdentifyJob {
|
||||
}
|
||||
}
|
||||
|
||||
func (j *IdentifyJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *IdentifyJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
j.progress = progress
|
||||
|
||||
// if no sources provided - just return
|
||||
if len(j.input.Sources) == 0 {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
sources, err := j.getSources()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
// if scene ids provided, use those
|
||||
@@ -84,8 +83,10 @@ func (j *IdentifyJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
logger.Errorf("Error encountered while identifying scenes: %v", err)
|
||||
return fmt.Errorf("error encountered while identifying scenes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *IdentifyJob) identifyAllScenes(ctx context.Context, sources []identify.ScraperSource) error {
|
||||
|
||||
@@ -2,6 +2,7 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
@@ -17,7 +18,7 @@ type OptimiseDatabaseJob struct {
|
||||
Optimiser Optimiser
|
||||
}
|
||||
|
||||
func (j *OptimiseDatabaseJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *OptimiseDatabaseJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
logger.Info("Optimising database")
|
||||
progress.SetTotal(2)
|
||||
|
||||
@@ -31,11 +32,10 @@ func (j *OptimiseDatabaseJob) Execute(ctx context.Context, progress *job.Progres
|
||||
})
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
logger.Errorf("Error analyzing database: %v", err)
|
||||
return
|
||||
return fmt.Errorf("Error analyzing database: %w", err)
|
||||
}
|
||||
|
||||
progress.ExecuteTask("Vacuuming database", func() {
|
||||
@@ -44,13 +44,13 @@ func (j *OptimiseDatabaseJob) Execute(ctx context.Context, progress *job.Progres
|
||||
})
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
logger.Errorf("Error vacuuming database: %v", err)
|
||||
return
|
||||
return fmt.Errorf("error vacuuming database: %w", err)
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
logger.Infof("Finished optimising database after %s", elapsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,19 +9,23 @@ import (
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
)
|
||||
|
||||
func (s *Manager) RunPluginTask(ctx context.Context, pluginID string, taskName string, args []*plugin.PluginArgInput) int {
|
||||
j := job.MakeJobExec(func(jobCtx context.Context, progress *job.Progress) {
|
||||
func (s *Manager) RunPluginTask(
|
||||
ctx context.Context,
|
||||
pluginID string,
|
||||
taskName *string,
|
||||
description *string,
|
||||
args plugin.OperationInput,
|
||||
) int {
|
||||
j := job.MakeJobExec(func(jobCtx context.Context, progress *job.Progress) error {
|
||||
pluginProgress := make(chan float64)
|
||||
task, err := s.PluginCache.CreateTask(ctx, pluginID, taskName, args, pluginProgress)
|
||||
if err != nil {
|
||||
logger.Errorf("Error creating plugin task: %s", err.Error())
|
||||
return
|
||||
return fmt.Errorf("Error creating plugin task: %w", err)
|
||||
}
|
||||
|
||||
err = task.Start()
|
||||
if err != nil {
|
||||
logger.Errorf("Error running plugin task: %s", err.Error())
|
||||
return
|
||||
return fmt.Errorf("Error running plugin task: %w", err)
|
||||
}
|
||||
|
||||
done := make(chan bool)
|
||||
@@ -44,17 +48,24 @@ func (s *Manager) RunPluginTask(ctx context.Context, pluginID string, taskName s
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
return nil
|
||||
case p := <-pluginProgress:
|
||||
progress.SetPercent(p)
|
||||
case <-jobCtx.Done():
|
||||
if err := task.Stop(); err != nil {
|
||||
logger.Errorf("Error stopping plugin operation: %s", err.Error())
|
||||
}
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, fmt.Sprintf("Running plugin task: %s", taskName), j)
|
||||
displayName := pluginID
|
||||
if taskName != nil {
|
||||
displayName = *taskName
|
||||
}
|
||||
if description != nil {
|
||||
displayName = *description
|
||||
}
|
||||
return s.JobManager.Add(ctx, fmt.Sprintf("Running plugin task: %s", displayName), j)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
@@ -35,12 +34,13 @@ type ScanJob struct {
|
||||
subscriptions *subscriptionManager
|
||||
}
|
||||
|
||||
func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
cfg := config.GetInstance()
|
||||
input := j.input
|
||||
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
sp := getScanPaths(input.Paths)
|
||||
@@ -56,7 +56,7 @@ func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
start := time.Now()
|
||||
|
||||
const taskQueueSize = 200000
|
||||
taskQueue := job.NewTaskQueue(ctx, progress, taskQueueSize, c.GetParallelTasksWithAutoDetection())
|
||||
taskQueue := job.NewTaskQueue(ctx, progress, taskQueueSize, cfg.GetParallelTasksWithAutoDetection())
|
||||
|
||||
var minModTime time.Time
|
||||
if j.input.Filter != nil && j.input.Filter.MinModTime != nil {
|
||||
@@ -66,22 +66,24 @@ func (j *ScanJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
j.scanner.Scan(ctx, getScanHandlers(j.input, taskQueue, progress), file.ScanOptions{
|
||||
Paths: paths,
|
||||
ScanFilters: []file.PathFilter{newScanFilter(c, repo, minModTime)},
|
||||
ZipFileExtensions: c.GetGalleryExtensions(),
|
||||
ParallelTasks: c.GetParallelTasksWithAutoDetection(),
|
||||
HandlerRequiredFilters: []file.Filter{newHandlerRequiredFilter(c, repo)},
|
||||
ZipFileExtensions: cfg.GetGalleryExtensions(),
|
||||
ParallelTasks: cfg.GetParallelTasksWithAutoDetection(),
|
||||
HandlerRequiredFilters: []file.Filter{newHandlerRequiredFilter(cfg, repo)},
|
||||
Rescan: j.input.Rescan,
|
||||
}, progress)
|
||||
|
||||
taskQueue.Close()
|
||||
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
logger.Info(fmt.Sprintf("Scan finished (%s)", elapsed))
|
||||
|
||||
j.subscriptions.notify()
|
||||
return nil
|
||||
}
|
||||
|
||||
type extensionConfig struct {
|
||||
@@ -177,7 +179,8 @@ func (f *handlerRequiredFilter) Accept(ctx context.Context, ff models.File) bool
|
||||
// if create galleries from folder is enabled and the file is not in a zip
|
||||
// file, then check if there is a folder-based gallery for the file's
|
||||
// directory
|
||||
if isImageFile && instance.Config.GetCreateGalleriesFromFolders() && ff.Base().ZipFileID == nil {
|
||||
// #4611 - also check for .forcegallery
|
||||
if isImageFile && ff.Base().ZipFileID == nil {
|
||||
// only do this for the first time it encounters the folder
|
||||
// the first instance should create the gallery
|
||||
_, found := f.FolderCache.Get(ctx, ff.Base().ParentFolderID.String())
|
||||
@@ -186,9 +189,23 @@ func (f *handlerRequiredFilter) Accept(ctx context.Context, ff models.File) bool
|
||||
return false
|
||||
}
|
||||
|
||||
g, _ := f.GalleryFinder.FindByFolderID(ctx, ff.Base().ParentFolderID)
|
||||
f.FolderCache.Add(ctx, ff.Base().ParentFolderID.String(), true)
|
||||
|
||||
createGallery := instance.Config.GetCreateGalleriesFromFolders()
|
||||
if !createGallery {
|
||||
// check for presence of .forcegallery
|
||||
forceGalleryPath := filepath.Join(filepath.Dir(path), ".forcegallery")
|
||||
if exists, _ := fsutil.FileExists(forceGalleryPath); exists {
|
||||
createGallery = true
|
||||
}
|
||||
}
|
||||
|
||||
if !createGallery {
|
||||
return false
|
||||
}
|
||||
|
||||
g, _ := f.GalleryFinder.FindByFolderID(ctx, ff.Base().ParentFolderID)
|
||||
|
||||
if len(g) == 0 {
|
||||
// no folder gallery. Return true so that it creates one.
|
||||
return true
|
||||
@@ -264,6 +281,12 @@ func (f *scanFilter) Accept(ctx context.Context, path string, info fs.FileInfo)
|
||||
return false
|
||||
}
|
||||
|
||||
s := f.stashPaths.GetStashFromDirPath(path)
|
||||
if s == nil {
|
||||
logger.Debugf("Skipping %s as it is not in the stash library", path)
|
||||
return false
|
||||
}
|
||||
|
||||
isVideoFile := useAsVideo(path)
|
||||
isImageFile := useAsImage(path)
|
||||
isZipFile := fsutil.MatchExtension(path, f.zipExt)
|
||||
@@ -288,13 +311,6 @@ func (f *scanFilter) Accept(ctx context.Context, path string, info fs.FileInfo)
|
||||
return false
|
||||
}
|
||||
|
||||
s := f.stashPaths.GetStashFromDirPath(path)
|
||||
|
||||
if s == nil {
|
||||
logger.Debugf("Skipping %s as it is not in the stash library", path)
|
||||
return false
|
||||
}
|
||||
|
||||
// shortcut: skip the directory entirely if it matches both exclusion patterns
|
||||
// add a trailing separator so that it correctly matches against patterns like path/.*
|
||||
pathExcludeTest := path + string(filepath.Separator)
|
||||
@@ -411,21 +427,24 @@ func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f model
|
||||
t := g.input
|
||||
path := f.Base().Path
|
||||
|
||||
// this is a bit of a hack: the task requires files to be loaded, but
|
||||
// we don't really need to since we already have the file
|
||||
ii := *i
|
||||
ii.Files = models.NewRelatedFiles([]models.File{f})
|
||||
|
||||
if t.ScanGenerateThumbnails {
|
||||
// this should be quick, so always generate sequentially
|
||||
if err := g.generateThumbnail(ctx, i, f); err != nil {
|
||||
logger.Errorf("Error generating thumbnail for %s: %v", path, err)
|
||||
taskThumbnail := GenerateImageThumbnailTask{
|
||||
Image: ii,
|
||||
Overwrite: overwrite,
|
||||
}
|
||||
|
||||
taskThumbnail.Start(ctx)
|
||||
}
|
||||
|
||||
// avoid adding a task if the file isn't a video file
|
||||
_, isVideo := f.(*models.VideoFile)
|
||||
if isVideo && t.ScanGenerateClipPreviews {
|
||||
// this is a bit of a hack: the task requires files to be loaded, but
|
||||
// we don't really need to since we already have the file
|
||||
ii := *i
|
||||
ii.Files = models.NewRelatedFiles([]models.File{f})
|
||||
|
||||
progress.AddTotal(1)
|
||||
previewsFn := func(ctx context.Context) {
|
||||
taskPreview := GenerateClipPreviewTask{
|
||||
@@ -447,54 +466,6 @@ func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f model
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *imageGenerators) generateThumbnail(ctx context.Context, i *models.Image, f models.File) error {
|
||||
thumbPath := g.paths.Generated.GetThumbnailPath(i.Checksum, models.DefaultGthumbWidth)
|
||||
exists, _ := fsutil.FileExists(thumbPath)
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := f.Base().Path
|
||||
|
||||
vf, ok := f.(models.VisualFile)
|
||||
if !ok {
|
||||
return fmt.Errorf("file %s is not a visual file", path)
|
||||
}
|
||||
|
||||
if vf.GetHeight() <= models.DefaultGthumbWidth && vf.GetWidth() <= models.DefaultGthumbWidth {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debugf("Generating thumbnail for %s", path)
|
||||
|
||||
mgr := GetInstance()
|
||||
c := mgr.Config
|
||||
|
||||
clipPreviewOptions := image.ClipPreviewOptions{
|
||||
InputArgs: c.GetTranscodeInputArgs(),
|
||||
OutputArgs: c.GetTranscodeOutputArgs(),
|
||||
Preset: c.GetPreviewPreset().String(),
|
||||
}
|
||||
|
||||
encoder := image.NewThumbnailEncoder(mgr.FFMpeg, mgr.FFProbe, clipPreviewOptions)
|
||||
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
|
||||
|
||||
if err != nil {
|
||||
// don't log for animated images
|
||||
if !errors.Is(err, image.ErrNotSupportedForThumbnail) {
|
||||
return fmt.Errorf("getting thumbnail for image %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err = fsutil.WriteFile(thumbPath, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing thumbnail for image %s: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type sceneGenerators struct {
|
||||
input ScanMetadataInput
|
||||
taskQueue *job.TaskQueue
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
"github.com/stashapp/stash/pkg/studio"
|
||||
)
|
||||
@@ -155,6 +156,10 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m
|
||||
|
||||
partial := p.ToPartial(t.box.Endpoint, excluded, existingStashIDs)
|
||||
|
||||
if err := performer.ValidateUpdate(ctx, t.performer.ID, partial, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := qb.UpdatePartial(ctx, t.performer.ID, partial); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -185,6 +190,10 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m
|
||||
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.Performer
|
||||
|
||||
if err := performer.ValidateCreate(ctx, *newPerformer, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.Create(ctx, newPerformer); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -302,13 +311,13 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode
|
||||
return err
|
||||
}
|
||||
|
||||
partial := s.ToPartial(s.StoredID, t.box.Endpoint, excluded, existingStashIDs)
|
||||
partial := s.ToPartial(*s.StoredID, t.box.Endpoint, excluded, existingStashIDs)
|
||||
|
||||
if err := studio.ValidateModify(ctx, *partial, qb); err != nil {
|
||||
if err := studio.ValidateModify(ctx, partial, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := qb.UpdatePartial(ctx, *partial); err != nil {
|
||||
if _, err := qb.UpdatePartial(ctx, partial); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -346,6 +355,10 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode
|
||||
err = r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.Studio
|
||||
|
||||
if err := studio.ValidateCreate(ctx, *newStudio, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.Create(ctx, newStudio); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -422,13 +435,13 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent *
|
||||
return err
|
||||
}
|
||||
|
||||
partial := parent.ToPartial(parent.StoredID, t.box.Endpoint, excluded, existingStashIDs)
|
||||
partial := parent.ToPartial(*parent.StoredID, t.box.Endpoint, excluded, existingStashIDs)
|
||||
|
||||
if err := studio.ValidateModify(ctx, *partial, qb); err != nil {
|
||||
if err := studio.ValidateModify(ctx, partial, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := qb.UpdatePartial(ctx, *partial); err != nil {
|
||||
if _, err := qb.UpdatePartial(ctx, partial); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ func (t *GenerateTranscodeTask) GetDescription() string {
|
||||
return fmt.Sprintf("Generating transcode for %s", t.Scene.Path)
|
||||
}
|
||||
|
||||
func (t *GenerateTranscodeTask) Start(ctc context.Context) {
|
||||
func (t *GenerateTranscodeTask) Start(ctx context.Context) {
|
||||
hasTranscode := HasTranscode(&t.Scene, t.fileNamingAlgorithm)
|
||||
if !t.Overwrite && hasTranscode {
|
||||
return
|
||||
@@ -72,23 +72,26 @@ func (t *GenerateTranscodeTask) Start(ctc context.Context) {
|
||||
|
||||
w, h := videoFile.TranscodeScale(transcodeSize.GetMaxResolution())
|
||||
|
||||
options := generate.TranscodeOptions{
|
||||
Width: w,
|
||||
Height: h,
|
||||
}
|
||||
// if scale is being set, then we can't use stream copy
|
||||
scaleSet := w == 0 && h == 0
|
||||
|
||||
if videoCodec == ffmpeg.H264 { // for non supported h264 files stream copy the video part
|
||||
if scaleSet && videoCodec == ffmpeg.H264 { // for non supported h264 files stream copy the video part
|
||||
if audioCodec == ffmpeg.MissingUnsupported {
|
||||
err = t.g.TranscodeCopyVideo(context.TODO(), videoFile.Path, sceneHash, options)
|
||||
err = t.g.TranscodeCopyVideo(ctx, videoFile.Path, sceneHash)
|
||||
} else {
|
||||
err = t.g.TranscodeAudio(context.TODO(), videoFile.Path, sceneHash, options)
|
||||
err = t.g.TranscodeAudio(ctx, videoFile.Path, sceneHash)
|
||||
}
|
||||
} else {
|
||||
options := generate.TranscodeOptions{
|
||||
Width: w,
|
||||
Height: h,
|
||||
}
|
||||
|
||||
if audioCodec == ffmpeg.MissingUnsupported {
|
||||
// ffmpeg fails if it tries to transcode an unsupported audio codec
|
||||
err = t.g.TranscodeVideo(context.TODO(), videoFile.Path, sceneHash, options)
|
||||
err = t.g.TranscodeVideo(ctx, videoFile.Path, sceneHash, options)
|
||||
} else {
|
||||
err = t.g.Transcode(context.TODO(), videoFile.Path, sceneHash, options)
|
||||
err = t.g.Transcode(ctx, videoFile.Path, sceneHash, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,6 +25,8 @@ var (
|
||||
VideoCodecVVPX VideoCodec = "vp8_vaapi"
|
||||
)
|
||||
|
||||
const minHeight int = 256
|
||||
|
||||
// Tests all (given) hardware codec's
|
||||
func (f *FFMpeg) InitHWSupport(ctx context.Context) {
|
||||
var hwCodecSupport []VideoCodec
|
||||
@@ -39,15 +42,13 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
|
||||
var args Args
|
||||
args = append(args, "-hide_banner")
|
||||
args = args.LogLevel(LogLevelWarning)
|
||||
args = f.hwDeviceInit(args, codec)
|
||||
args = f.hwDeviceInit(args, codec, false)
|
||||
args = args.Format("lavfi")
|
||||
args = args.Input("color=c=red")
|
||||
args = args.Input(fmt.Sprintf("color=c=red:s=%dx%d", 1280, 720))
|
||||
args = args.Duration(0.1)
|
||||
|
||||
videoFilter := f.hwFilterInit(codec)
|
||||
// Test scaling
|
||||
videoFilter = videoFilter.ScaleDimensions(-2, 160)
|
||||
videoFilter = f.hwCodecFilter(videoFilter, codec)
|
||||
videoFilter := f.hwMaxResFilter(codec, 1280, 720, minHeight, false)
|
||||
args = append(args, CodecInit(codec)...)
|
||||
args = args.VideoFilter(videoFilter)
|
||||
|
||||
@@ -59,12 +60,7 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logger.Debugf("[InitHWSupport] error starting command: %w", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if err := cmd.Run(); err != nil {
|
||||
errOutput := stderr.String()
|
||||
|
||||
if len(errOutput) == 0 {
|
||||
@@ -77,7 +73,7 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
outstr := "[InitHWSupport] Supported HW codecs:\n"
|
||||
outstr := fmt.Sprintf("[InitHWSupport] Supported HW codecs [%d]:\n", len(hwCodecSupport))
|
||||
for _, codec := range hwCodecSupport {
|
||||
outstr += fmt.Sprintf("\t%s\n", codec)
|
||||
}
|
||||
@@ -86,66 +82,157 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
|
||||
f.hwCodecSupport = hwCodecSupport
|
||||
}
|
||||
|
||||
func (f *FFMpeg) hwCanFullHWTranscode(ctx context.Context, codec VideoCodec, vf *models.VideoFile, reqHeight int) bool {
|
||||
if codec == VideoCodecCopy {
|
||||
return false
|
||||
}
|
||||
|
||||
var args Args
|
||||
args = append(args, "-hide_banner")
|
||||
args = args.LogLevel(LogLevelWarning)
|
||||
args = args.XError()
|
||||
args = f.hwDeviceInit(args, codec, true)
|
||||
args = args.Input(vf.Path)
|
||||
args = args.Duration(0.1)
|
||||
|
||||
videoFilter := f.hwMaxResFilter(codec, vf.Width, vf.Height, reqHeight, true)
|
||||
args = append(args, CodecInit(codec)...)
|
||||
args = args.VideoFilter(videoFilter)
|
||||
|
||||
args = args.Format("null")
|
||||
args = args.Output("-")
|
||||
|
||||
cmd := f.Command(ctx, args)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
errOutput := stderr.String()
|
||||
|
||||
if len(errOutput) == 0 {
|
||||
errOutput = err.Error()
|
||||
}
|
||||
|
||||
logger.Debugf("[InitHWSupport] Full hardware transcode for file %s not supported. Error output:\n%s", vf.Basename, errOutput)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Prepend input for hardware encoding only
|
||||
func (f *FFMpeg) hwDeviceInit(args Args, codec VideoCodec) Args {
|
||||
switch codec {
|
||||
func (f *FFMpeg) hwDeviceInit(args Args, toCodec VideoCodec, fullhw bool) Args {
|
||||
switch toCodec {
|
||||
case VideoCodecN264:
|
||||
args = append(args, "-hwaccel_device")
|
||||
args = append(args, "0")
|
||||
if fullhw {
|
||||
args = append(args, "-hwaccel")
|
||||
args = append(args, "cuda")
|
||||
args = append(args, "-hwaccel_output_format")
|
||||
args = append(args, "cuda")
|
||||
args = append(args, "-extra_hw_frames")
|
||||
args = append(args, "5")
|
||||
}
|
||||
case VideoCodecV264,
|
||||
VideoCodecVVP9:
|
||||
args = append(args, "-vaapi_device")
|
||||
args = append(args, "/dev/dri/renderD128")
|
||||
if fullhw {
|
||||
args = append(args, "-hwaccel")
|
||||
args = append(args, "vaapi")
|
||||
args = append(args, "-hwaccel_output_format")
|
||||
args = append(args, "vaapi")
|
||||
}
|
||||
case VideoCodecI264,
|
||||
VideoCodecIVP9:
|
||||
args = append(args, "-init_hw_device")
|
||||
args = append(args, "qsv=hw")
|
||||
args = append(args, "-filter_hw_device")
|
||||
args = append(args, "hw")
|
||||
if fullhw {
|
||||
args = append(args, "-hwaccel")
|
||||
args = append(args, "qsv")
|
||||
args = append(args, "-hwaccel_output_format")
|
||||
args = append(args, "qsv")
|
||||
} else {
|
||||
args = append(args, "-init_hw_device")
|
||||
args = append(args, "qsv=hw")
|
||||
args = append(args, "-filter_hw_device")
|
||||
args = append(args, "hw")
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// Initialise a video filter for HW encoding
|
||||
func (f *FFMpeg) hwFilterInit(codec VideoCodec) VideoFilter {
|
||||
func (f *FFMpeg) hwFilterInit(toCodec VideoCodec, fullhw bool) VideoFilter {
|
||||
var videoFilter VideoFilter
|
||||
switch codec {
|
||||
switch toCodec {
|
||||
case VideoCodecV264,
|
||||
VideoCodecVVP9:
|
||||
videoFilter = videoFilter.Append("format=nv12")
|
||||
videoFilter = videoFilter.Append("hwupload")
|
||||
if !fullhw {
|
||||
videoFilter = videoFilter.Append("format=nv12")
|
||||
videoFilter = videoFilter.Append("hwupload")
|
||||
}
|
||||
case VideoCodecN264:
|
||||
videoFilter = videoFilter.Append("format=nv12")
|
||||
videoFilter = videoFilter.Append("hwupload_cuda")
|
||||
if !fullhw {
|
||||
videoFilter = videoFilter.Append("format=nv12")
|
||||
videoFilter = videoFilter.Append("hwupload_cuda")
|
||||
}
|
||||
case VideoCodecI264,
|
||||
VideoCodecIVP9:
|
||||
videoFilter = videoFilter.Append("hwupload=extra_hw_frames=64")
|
||||
videoFilter = videoFilter.Append("format=qsv")
|
||||
if !fullhw {
|
||||
videoFilter = videoFilter.Append("hwupload=extra_hw_frames=64")
|
||||
videoFilter = videoFilter.Append("format=qsv")
|
||||
}
|
||||
}
|
||||
|
||||
return videoFilter
|
||||
}
|
||||
|
||||
var scaler_re = regexp.MustCompile(`scale=(?P<value>[-\d]+:[-\d]+)`)
|
||||
|
||||
func templateReplaceScale(input string, template string, match []int, minusonehack bool) string {
|
||||
result := []byte{}
|
||||
|
||||
res := string(scaler_re.ExpandString(result, template, input, match))
|
||||
|
||||
// BUG: [scale_qsv]: Size values less than -1 are not acceptable.
|
||||
// Fix: Replace all instances of -2 with -1 in a scale operation
|
||||
if minusonehack {
|
||||
res = strings.ReplaceAll(res, "-2", "-1")
|
||||
}
|
||||
|
||||
matchStart := match[0]
|
||||
matchEnd := match[1]
|
||||
|
||||
return input[0:matchStart] + res + input[matchEnd:]
|
||||
}
|
||||
|
||||
// Replace video filter scaling with hardware scaling for full hardware transcoding
|
||||
func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec) VideoFilter {
|
||||
func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec, fullhw bool) VideoFilter {
|
||||
sargs := string(args)
|
||||
|
||||
if strings.Contains(sargs, "scale=") {
|
||||
switch codec {
|
||||
case VideoCodecN264:
|
||||
args = VideoFilter(strings.Replace(sargs, "scale=", "scale_cuda=", 1))
|
||||
case VideoCodecV264,
|
||||
VideoCodecVVP9:
|
||||
args = VideoFilter(strings.Replace(sargs, "scale=", "scale_vaapi=", 1))
|
||||
case VideoCodecI264,
|
||||
VideoCodecIVP9:
|
||||
// BUG: [scale_qsv]: Size values less than -1 are not acceptable.
|
||||
// Fix: Replace all instances of -2 with -1 in a scale operation
|
||||
re := regexp.MustCompile(`(scale=)([\d:]*)(-2)(.*)`)
|
||||
sargs = re.ReplaceAllString(sargs, "scale=$2-1$4")
|
||||
args = VideoFilter(strings.Replace(sargs, "scale=", "scale_qsv=", 1))
|
||||
match := scaler_re.FindStringSubmatchIndex(sargs)
|
||||
if match == nil {
|
||||
return args
|
||||
}
|
||||
|
||||
switch codec {
|
||||
case VideoCodecN264:
|
||||
template := "scale_cuda=$value"
|
||||
// In 10bit inputs you might get an error like "10 bit encode not supported"
|
||||
if fullhw && f.version.major >= 5 {
|
||||
template += ":format=nv12"
|
||||
}
|
||||
args = VideoFilter(templateReplaceScale(sargs, template, match, false))
|
||||
case VideoCodecV264,
|
||||
VideoCodecVVP9:
|
||||
template := "scale_vaapi=$value"
|
||||
args = VideoFilter(templateReplaceScale(sargs, template, match, false))
|
||||
case VideoCodecI264,
|
||||
VideoCodecIVP9:
|
||||
template := "scale_qsv=$value"
|
||||
args = VideoFilter(templateReplaceScale(sargs, template, match, true))
|
||||
}
|
||||
|
||||
return args
|
||||
@@ -153,7 +240,9 @@ func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec) VideoFilter {
|
||||
|
||||
// Returns the max resolution for a given codec, or a default
|
||||
func (f *FFMpeg) hwCodecMaxRes(codec VideoCodec, dW int, dH int) (int, int) {
|
||||
if codec == VideoCodecN264 {
|
||||
switch codec {
|
||||
case VideoCodecN264,
|
||||
VideoCodecI264:
|
||||
return 4096, 4096
|
||||
}
|
||||
|
||||
@@ -161,11 +250,14 @@ func (f *FFMpeg) hwCodecMaxRes(codec VideoCodec, dW int, dH int) (int, int) {
|
||||
}
|
||||
|
||||
// Return a maxres filter
|
||||
func (f *FFMpeg) hwMaxResFilter(codec VideoCodec, width int, height int, max int) VideoFilter {
|
||||
videoFilter := f.hwFilterInit(codec)
|
||||
maxWidth, maxHeight := f.hwCodecMaxRes(codec, width, height)
|
||||
videoFilter = videoFilter.ScaleMaxLM(width, height, max, maxWidth, maxHeight)
|
||||
return f.hwCodecFilter(videoFilter, codec)
|
||||
func (f *FFMpeg) hwMaxResFilter(toCodec VideoCodec, width int, height int, reqHeight int, fullhw bool) VideoFilter {
|
||||
if width == 0 || height == 0 {
|
||||
return ""
|
||||
}
|
||||
videoFilter := f.hwFilterInit(toCodec, fullhw)
|
||||
maxWidth, maxHeight := f.hwCodecMaxRes(toCodec, width, height)
|
||||
videoFilter = videoFilter.ScaleMaxLM(width, height, reqHeight, maxWidth, maxHeight)
|
||||
return f.hwCodecFilter(videoFilter, toCodec, fullhw)
|
||||
}
|
||||
|
||||
// Return if a hardware accelerated for HLS is available
|
||||
|
||||
@@ -1,179 +1,10 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
stashExec "github.com/stashapp/stash/pkg/exec"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
func GetPaths(paths []string) (string, string) {
|
||||
var ffmpegPath, ffprobePath string
|
||||
|
||||
// Check if ffmpeg exists in the PATH
|
||||
if pathBinaryHasCorrectFlags() {
|
||||
ffmpegPath, _ = exec.LookPath("ffmpeg")
|
||||
ffprobePath, _ = exec.LookPath("ffprobe")
|
||||
}
|
||||
|
||||
// Check if ffmpeg exists in the config directory
|
||||
if ffmpegPath == "" {
|
||||
ffmpegPath = fsutil.FindInPaths(paths, getFFMpegFilename())
|
||||
}
|
||||
if ffprobePath == "" {
|
||||
ffprobePath = fsutil.FindInPaths(paths, getFFProbeFilename())
|
||||
}
|
||||
|
||||
return ffmpegPath, ffprobePath
|
||||
}
|
||||
|
||||
func Download(ctx context.Context, configDirectory string) error {
|
||||
for _, url := range getFFmpegURL() {
|
||||
err := downloadSingle(ctx, configDirectory, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// validate that the urls contained what we needed
|
||||
executables := []string{getFFMpegFilename(), getFFProbeFilename()}
|
||||
for _, executable := range executables {
|
||||
_, err := os.Stat(filepath.Join(configDirectory, executable))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type progressReader struct {
|
||||
io.Reader
|
||||
lastProgress int64
|
||||
bytesRead int64
|
||||
total int64
|
||||
}
|
||||
|
||||
func (r *progressReader) Read(p []byte) (int, error) {
|
||||
read, err := r.Reader.Read(p)
|
||||
if err == nil {
|
||||
r.bytesRead += int64(read)
|
||||
if r.total > 0 {
|
||||
progress := int64(float64(r.bytesRead) / float64(r.total) * 100)
|
||||
if progress/5 > r.lastProgress {
|
||||
logger.Infof("%d%% downloaded...", progress)
|
||||
r.lastProgress = progress / 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return read, err
|
||||
}
|
||||
|
||||
func downloadSingle(ctx context.Context, configDirectory, url string) error {
|
||||
if url == "" {
|
||||
return fmt.Errorf("no ffmpeg url for this platform")
|
||||
}
|
||||
|
||||
// Configure where we want to download the archive
|
||||
urlBase := path.Base(url)
|
||||
archivePath := filepath.Join(configDirectory, urlBase)
|
||||
_ = os.Remove(archivePath) // remove archive if it already exists
|
||||
out, err := os.Create(archivePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
logger.Infof("Downloading %s...", url)
|
||||
|
||||
// Make the HTTP request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transport := &http.Transport{Proxy: http.ProxyFromEnvironment}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check server response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
reader := &progressReader{
|
||||
Reader: resp.Body,
|
||||
total: resp.ContentLength,
|
||||
}
|
||||
|
||||
// Write the response to the archive file location
|
||||
_, err = io.Copy(out, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("Downloading complete")
|
||||
|
||||
mime := resp.Header.Get("Content-Type")
|
||||
if mime != "application/zip" { // try detecting MIME type since some servers don't return the correct one
|
||||
data := make([]byte, 500) // http.DetectContentType only reads up to 500 bytes
|
||||
_, _ = out.ReadAt(data, 0)
|
||||
mime = http.DetectContentType(data)
|
||||
}
|
||||
|
||||
if mime == "application/zip" {
|
||||
logger.Infof("Unzipping %s...", archivePath)
|
||||
if err := unzip(archivePath, configDirectory); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// On OSX or Linux set downloaded files permissions
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
|
||||
_, err = os.Stat(filepath.Join(configDirectory, "ffmpeg"))
|
||||
if !os.IsNotExist(err) {
|
||||
if err = os.Chmod(filepath.Join(configDirectory, "ffmpeg"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = os.Stat(filepath.Join(configDirectory, "ffprobe"))
|
||||
if !os.IsNotExist(err) {
|
||||
if err := os.Chmod(filepath.Join(configDirectory, "ffprobe"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: In future possible clear xattr to allow running on osx without user intervention
|
||||
// TODO: this however may not be required.
|
||||
// xattr -c /path/to/binary -- xattr.Remove(path, "com.apple.quarantine")
|
||||
}
|
||||
|
||||
} else {
|
||||
return fmt.Errorf("ffmpeg was downloaded to %s", archivePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFFmpegURL() []string {
|
||||
func GetFFmpegURL() []string {
|
||||
var urls []string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
@@ -208,60 +39,3 @@ func getFFProbeFilename() string {
|
||||
}
|
||||
return "ffprobe"
|
||||
}
|
||||
|
||||
// Checks if ffmpeg in the path has the correct flags
|
||||
func pathBinaryHasCorrectFlags() bool {
|
||||
ffmpegPath, err := exec.LookPath("ffmpeg")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
cmd := stashExec.Command(ffmpegPath)
|
||||
bytes, _ := cmd.CombinedOutput()
|
||||
output := string(bytes)
|
||||
hasOpus := strings.Contains(output, "--enable-libopus")
|
||||
hasVpx := strings.Contains(output, "--enable-libvpx")
|
||||
hasX264 := strings.Contains(output, "--enable-libx264")
|
||||
hasX265 := strings.Contains(output, "--enable-libx265")
|
||||
hasWebp := strings.Contains(output, "--enable-libwebp")
|
||||
return hasOpus && hasVpx && hasX264 && hasX265 && hasWebp
|
||||
}
|
||||
|
||||
func unzip(src, configDirectory string) error {
|
||||
zipReader, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipReader.Close()
|
||||
|
||||
for _, f := range zipReader.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
filename := f.FileInfo().Name()
|
||||
if filename != "ffprobe" && filename != "ffmpeg" && filename != "ffprobe.exe" && filename != "ffmpeg.exe" {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unzippedPath := filepath.Join(configDirectory, filename)
|
||||
unzippedOutput, err := os.Create(unzippedPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(unzippedOutput, rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := unzippedOutput.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user