mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 07:41:08 -05:00
Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1998cb5b0 | ||
|
|
60446af145 | ||
|
|
dbfa450ace | ||
|
|
4b8af18fab | ||
|
|
124ea609fe | ||
|
|
0a07194110 | ||
|
|
b232e58b06 | ||
|
|
b3f8839ef7 | ||
|
|
540e80c86b | ||
|
|
eec31723bd | ||
|
|
3b146588c6 | ||
|
|
2b699fcf95 | ||
|
|
d6158d70a9 | ||
|
|
cf45ac883e | ||
|
|
e4267a0d83 | ||
|
|
2ca53714a6 | ||
|
|
0ff0f9c8ec | ||
|
|
9c8bd853c5 | ||
|
|
bf0e0f2210 | ||
|
|
c314515b8f | ||
|
|
28b5fbfd4d | ||
|
|
3dd218e1ba | ||
|
|
eb67f7f4d6 | ||
|
|
98d210f7f9 | ||
|
|
4794a1d453 | ||
|
|
77ef16570b | ||
|
|
99d97804f4 | ||
|
|
89553864f5 | ||
|
|
865208844c | ||
|
|
062d566195 | ||
|
|
bfc60bb23f | ||
|
|
0fa71be697 | ||
|
|
5ba1ea8fbc | ||
|
|
4d3dc0aec8 | ||
|
|
b12269e477 | ||
|
|
e32593023e | ||
|
|
3e3e8b95e2 | ||
|
|
769540be55 | ||
|
|
1ffca39e1d | ||
|
|
dd84714a16 | ||
|
|
ad844a225c | ||
|
|
ca5febc65b | ||
|
|
c8aeb7966a | ||
|
|
1d565a7cbd | ||
|
|
408d6fc988 | ||
|
|
237a904ca4 | ||
|
|
12af7d6515 | ||
|
|
77ee620877 | ||
|
|
c5fef3977e | ||
|
|
29859fa4ad | ||
|
|
1cee1ccfe2 | ||
|
|
9cc26f7b75 | ||
|
|
c5abe28375 | ||
|
|
1b99a03847 | ||
|
|
22d14fd89e | ||
|
|
0bba8889b8 | ||
|
|
141f60f8fb | ||
|
|
560bdcd60d | ||
|
|
c43e7b4351 | ||
|
|
4c0d9d0a07 | ||
|
|
157b2e7bae | ||
|
|
ec6acab2f4 | ||
|
|
911da87264 | ||
|
|
f7b87379d4 | ||
|
|
ad60f0ebd6 | ||
|
|
c83635c7a8 | ||
|
|
034fd4407d | ||
|
|
7086109d78 | ||
|
|
a369613d42 | ||
|
|
62b8ffb2b6 | ||
|
|
213c2830d1 | ||
|
|
32770203ba | ||
|
|
8c454582c7 | ||
|
|
e5929389b4 | ||
|
|
fa172c2dfd | ||
|
|
9ceea952b6 | ||
|
|
49cd214c9d | ||
|
|
3d0a8f653a | ||
|
|
ae6d1a8109 | ||
|
|
7ac7963972 | ||
|
|
bf7cb78d6d | ||
|
|
95d0e5dd34 | ||
|
|
d995ce7ecb | ||
|
|
3521dc133e | ||
|
|
9f5b1c33f6 | ||
|
|
c5bc106c1a | ||
|
|
9735d0fad1 | ||
|
|
353d889fd5 | ||
|
|
c7b2314bb1 | ||
|
|
4614471ad9 |
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: stashapp
|
||||
# patreon: # Replace with a single Patreon username
|
||||
open_collective: stashapp
|
||||
# ko_fi: # Replace with a single Ko-fi username
|
||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
# liberapay: StashApp
|
||||
# issuehunt: # Replace with a single IssueHunt username
|
||||
# otechie: # Replace with a single Otechie username
|
||||
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
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
|
||||
|
||||
|
||||
5
Makefile
5
Makefile
@@ -353,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
|
||||
|
||||
@@ -48,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())
|
||||
|
||||
@@ -18,7 +18,7 @@ COPY --from=binary /stash /usr/bin/
|
||||
RUN apk add --no-cache --virtual .build-deps gcc python3-dev musl-dev \
|
||||
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg ruby tzdata \
|
||||
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.18/community vips=8.14.3-r0 vips-tools=8.14.3-r0 \
|
||||
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx \
|
||||
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx stashapp-tools \
|
||||
&& gem install faraday \
|
||||
&& apk del .build-deps
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
21
go.mod
21
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,11 +49,11 @@ 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.17.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.15.0
|
||||
golang.org/x/term v0.15.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
|
||||
)
|
||||
|
||||
139
go.sum
139
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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
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,23 +928,25 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.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.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.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/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
@@ -963,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=
|
||||
@@ -1026,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=
|
||||
@@ -1069,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=
|
||||
@@ -1080,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=
|
||||
@@ -1104,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=
|
||||
|
||||
@@ -226,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
|
||||
@@ -349,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"
|
||||
|
||||
@@ -81,6 +81,10 @@ input ConfigGeneralInput {
|
||||
blobsPath: String
|
||||
"Where to store blobs"
|
||||
blobsStorage: BlobsStorageType
|
||||
"Path to the ffmpeg binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffmpegPath: String
|
||||
"Path to the ffprobe binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffprobePath: String
|
||||
"Whether to calculate MD5 checksums for scene video files"
|
||||
calculateMD5: Boolean
|
||||
"Hash algorithm to use for generated file naming"
|
||||
@@ -199,6 +203,10 @@ type ConfigGeneralResult {
|
||||
blobsPath: String!
|
||||
"Where to store blobs"
|
||||
blobsStorage: BlobsStorageType!
|
||||
"Path to the ffmpeg binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffmpegPath: String!
|
||||
"Path to the ffprobe binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffprobePath: String!
|
||||
"Whether to calculate MD5 checksums for scene video files"
|
||||
calculateMD5: Boolean!
|
||||
"Hash algorithm to use for generated file naming"
|
||||
@@ -461,6 +469,8 @@ input ConfigDLNAInput {
|
||||
serverName: String
|
||||
"True if DLNA service should be enabled by default"
|
||||
enabled: Boolean
|
||||
"Defaults to 1338"
|
||||
port: Int
|
||||
"List of IPs whitelisted for DLNA service"
|
||||
whitelistedIPs: [String!]
|
||||
"List of interfaces to run DLNA on. Empty for all"
|
||||
@@ -473,6 +483,8 @@ type ConfigDLNAResult {
|
||||
serverName: String!
|
||||
"True if DLNA service should be enabled by default"
|
||||
enabled: Boolean!
|
||||
"Defaults to 1338"
|
||||
port: Int!
|
||||
"List of IPs whitelisted for DLNA service"
|
||||
whitelistedIPs: [String!]!
|
||||
"List of interfaces to run DLNA on. Empty for all"
|
||||
|
||||
@@ -8,6 +8,7 @@ input FindFilterType {
|
||||
page: Int
|
||||
"use per_page = -1 to indicate all results. Defaults to 25."
|
||||
per_page: Int
|
||||
# TODO - this should be refactored to not use a string
|
||||
sort: String
|
||||
direction: SortDirectionEnum
|
||||
}
|
||||
@@ -143,6 +144,8 @@ input PerformerFilterType {
|
||||
image_count: IntCriterionInput
|
||||
"Filter by gallery count"
|
||||
gallery_count: IntCriterionInput
|
||||
"Filter by play count"
|
||||
play_count: IntCriterionInput
|
||||
"Filter by o count"
|
||||
o_counter: IntCriterionInput
|
||||
"Filter by StashID"
|
||||
@@ -229,6 +232,8 @@ input SceneFilterType {
|
||||
orientation: OrientationCriterionInput
|
||||
"Filter by frame rate"
|
||||
framerate: IntCriterionInput
|
||||
"Filter by bit rate"
|
||||
bitrate: IntCriterionInput
|
||||
"Filter by video codec"
|
||||
video_codec: StringCriterionInput
|
||||
"Filter by audio codec"
|
||||
@@ -243,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"
|
||||
@@ -273,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"
|
||||
@@ -321,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"
|
||||
@@ -368,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"
|
||||
@@ -411,6 +424,9 @@ input TagFilterType {
|
||||
"Filter by tag aliases"
|
||||
aliases: StringCriterionInput
|
||||
|
||||
"Filter by favorite"
|
||||
favorite: Boolean
|
||||
|
||||
"Filter by tag description"
|
||||
description: StringCriterionInput
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ enum JobStatus {
|
||||
FINISHED
|
||||
STOPPING
|
||||
CANCELLED
|
||||
FAILED
|
||||
}
|
||||
|
||||
type Job {
|
||||
@@ -15,6 +16,7 @@ type Job {
|
||||
startTime: Time
|
||||
endTime: Time
|
||||
addTime: Time!
|
||||
error: String
|
||||
}
|
||||
|
||||
input FindJobInput {
|
||||
|
||||
@@ -75,6 +75,8 @@ input ScanMetaDataFilterInput {
|
||||
input ScanMetadataInput {
|
||||
paths: [String!]
|
||||
|
||||
"Forces a rescan on files even if modification time is unchanged"
|
||||
rescan: Boolean
|
||||
"Generate covers during scan"
|
||||
scanGenerateCovers: Boolean
|
||||
"Generate previews during scan"
|
||||
@@ -95,6 +97,8 @@ input ScanMetadataInput {
|
||||
}
|
||||
|
||||
type ScanMetadataOptions {
|
||||
"Forces a rescan on files even if modification time is unchanged"
|
||||
rescan: Boolean!
|
||||
"Generate covers during scan"
|
||||
scanGenerateCovers: Boolean!
|
||||
"Generate previews during scan"
|
||||
@@ -326,6 +330,8 @@ type SystemStatus {
|
||||
os: String!
|
||||
workingDir: String!
|
||||
homeDir: String!
|
||||
ffmpegPath: String
|
||||
ffprobePath: String
|
||||
}
|
||||
|
||||
input MigrateInput {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (r *mutationResolver) SetPluginsEnabled(ctx context.Context, enabledMap map
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(config.DisabledPlugins, newDisabled)
|
||||
c.SetInterface(config.DisabledPlugins, newDisabled)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -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)
|
||||
@@ -103,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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -106,6 +106,10 @@ func (r *queryResolver) FindImages(
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &FindImagesResultType{
|
||||
Count: result.Count,
|
||||
Images: images,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -293,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
|
||||
@@ -316,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
|
||||
}
|
||||
@@ -323,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() {
|
||||
@@ -354,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.
|
||||
@@ -365,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
|
||||
@@ -405,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
|
||||
@@ -414,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
|
||||
}
|
||||
|
||||
@@ -434,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
|
||||
}
|
||||
|
||||
@@ -448,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
|
||||
@@ -458,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 {
|
||||
@@ -487,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
|
||||
}
|
||||
@@ -498,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.
|
||||
@@ -533,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{
|
||||
@@ -603,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))
|
||||
}
|
||||
@@ -628,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
|
||||
@@ -636,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
|
||||
@@ -644,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
|
||||
@@ -750,16 +854,15 @@ func (i *Config) GetAllPluginConfiguration() map[string]map[string]interface{} {
|
||||
|
||||
ret := make(map[string]map[string]interface{})
|
||||
|
||||
sub := i.viper(PluginsSetting).GetStringMap(PluginsSetting)
|
||||
v := i.forKey(PluginsSetting)
|
||||
|
||||
sub := v.Cut(PluginsSetting)
|
||||
if sub == nil {
|
||||
return ret
|
||||
}
|
||||
|
||||
for plugin := range sub {
|
||||
// HACK: viper changes map keys to case insensitive values, so the workaround is to
|
||||
// convert map keys to snake case for storage
|
||||
name := fromSnakeCase(plugin)
|
||||
ret[name] = fromSnakeCaseMap(i.viper(PluginsSetting).GetStringMap(PluginsSettingPrefix + plugin))
|
||||
for plugin := range sub.Raw() {
|
||||
ret[plugin] = sub.Cut(plugin).Raw()
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -769,26 +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 {
|
||||
@@ -1028,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
|
||||
@@ -1042,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
|
||||
}
|
||||
@@ -1066,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
|
||||
}
|
||||
@@ -1082,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
|
||||
@@ -1118,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1142,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
|
||||
@@ -1178,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 {
|
||||
@@ -1349,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
|
||||
@@ -1368,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
|
||||
@@ -1387,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
|
||||
@@ -1406,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
|
||||
@@ -1455,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 {
|
||||
@@ -1502,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
|
||||
}
|
||||
@@ -1534,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()
|
||||
}
|
||||
|
||||
@@ -1604,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)
|
||||
}
|
||||
}
|
||||
@@ -1615,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},
|
||||
}
|
||||
@@ -1635,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,
|
||||
@@ -1696,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1717,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()) == "" {
|
||||
@@ -1725,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
|
||||
|
||||
@@ -192,7 +192,6 @@ func (s *Manager) postInit(ctx context.Context) error {
|
||||
s.RefreshScraperCache()
|
||||
s.RefreshScraperSourceManager()
|
||||
|
||||
s.RefreshStreamManager()
|
||||
s.RefreshDLNA()
|
||||
|
||||
s.SetBlobStoreOptions()
|
||||
@@ -239,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
|
||||
}
|
||||
@@ -260,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,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
|
||||
@@ -256,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 {
|
||||
@@ -269,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)
|
||||
@@ -303,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
|
||||
@@ -437,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,
|
||||
@@ -446,6 +410,8 @@ func (s *Manager) GetSystemStatus() *SystemStatus {
|
||||
AppSchema: appSchema,
|
||||
Status: status,
|
||||
ConfigPath: &configFile,
|
||||
FfmpegPath: &ffmpegPath,
|
||||
FfprobePath: &ffprobePath,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ func (s *Manager) Import(ctx context.Context) (int, error) {
|
||||
return 0, errors.New("metadata path must be set in config")
|
||||
}
|
||||
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
task := ImportTask{
|
||||
repository: s.Repository,
|
||||
resetter: s.Database,
|
||||
@@ -147,6 +147,9 @@ func (s *Manager) Import(ctx context.Context) (int, error) {
|
||||
fileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
}
|
||||
task.Start(ctx)
|
||||
|
||||
// TODO - return error from task
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, "Importing...", j), nil
|
||||
@@ -159,7 +162,7 @@ func (s *Manager) Export(ctx context.Context) (int, error) {
|
||||
return 0, errors.New("metadata path must be set in config")
|
||||
}
|
||||
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
task := ExportTask{
|
||||
@@ -168,6 +171,8 @@ func (s *Manager) Export(ctx context.Context) (int, error) {
|
||||
fileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
}
|
||||
task.Start(ctx, &wg)
|
||||
// TODO - return error from task
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, "Exporting...", j), nil
|
||||
@@ -177,9 +182,11 @@ func (s *Manager) RunSingleTask(ctx context.Context, t Task) int {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
t.Start(ctx)
|
||||
wg.Done()
|
||||
defer wg.Done()
|
||||
// TODO - return error from task
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, t.GetDescription(), j)
|
||||
@@ -215,11 +222,10 @@ func (s *Manager) generateScreenshot(ctx context.Context, sceneId string, at *fl
|
||||
logger.Warnf("failure generating screenshot: %v", err)
|
||||
}
|
||||
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
sceneIdInt, err := strconv.Atoi(sceneId)
|
||||
if err != nil {
|
||||
logger.Errorf("Error parsing scene id %s: %v", sceneId, err)
|
||||
return
|
||||
return fmt.Errorf("error parsing scene id %s: %w", sceneId, err)
|
||||
}
|
||||
|
||||
var scene *models.Scene
|
||||
@@ -234,8 +240,7 @@ func (s *Manager) generateScreenshot(ctx context.Context, sceneId string, at *fl
|
||||
|
||||
return scene.LoadPrimaryFile(ctx, s.Repository.File)
|
||||
}); err != nil {
|
||||
logger.Errorf("error finding scene for screenshot generation: %v", err)
|
||||
return
|
||||
return fmt.Errorf("error finding scene for screenshot generation: %w", err)
|
||||
}
|
||||
|
||||
task := GenerateCoverTask{
|
||||
@@ -248,6 +253,9 @@ func (s *Manager) generateScreenshot(ctx context.Context, sceneId string, at *fl
|
||||
task.Start(ctx)
|
||||
|
||||
logger.Infof("Generate screenshot finished")
|
||||
|
||||
// TODO - return error from task
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, fmt.Sprintf("Generating screenshot for scene id %s", sceneId), j)
|
||||
@@ -309,7 +317,7 @@ func (s *Manager) OptimiseDatabase(ctx context.Context) int {
|
||||
}
|
||||
|
||||
func (s *Manager) MigrateHash(ctx context.Context) int {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm()
|
||||
logger.Infof("Migrating generated files for %s naming hash", fileNamingAlgo.String())
|
||||
|
||||
@@ -319,8 +327,7 @@ func (s *Manager) MigrateHash(ctx context.Context) int {
|
||||
scenes, err = s.Repository.Scene.All(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
logger.Errorf("failed to fetch list of scenes for migration: %s", err.Error())
|
||||
return
|
||||
return fmt.Errorf("failed to fetch list of scenes for migration: %w", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -331,7 +338,7 @@ func (s *Manager) MigrateHash(ctx context.Context) int {
|
||||
progress.Increment()
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if scene == nil {
|
||||
@@ -351,6 +358,7 @@ func (s *Manager) MigrateHash(ctx context.Context) int {
|
||||
}
|
||||
|
||||
logger.Info("Finished migrating")
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, "Migrating scene hashes...", j)
|
||||
@@ -381,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]
|
||||
|
||||
@@ -435,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
|
||||
@@ -493,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))
|
||||
@@ -513,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]
|
||||
|
||||
@@ -620,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))
|
||||
@@ -640,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 {
|
||||
|
||||
@@ -106,17 +106,15 @@ func (j *CleanGeneratedJob) logError(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) Execute(ctx context.Context, progress *job.Progress) {
|
||||
func (j *CleanGeneratedJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||
j.tasksComplete = 0
|
||||
|
||||
if !j.BlobsStorageType.IsValid() {
|
||||
logger.Errorf("invalid blobs storage type: %s", j.BlobsStorageType)
|
||||
return
|
||||
return fmt.Errorf("invalid blobs storage type: %s", j.BlobsStorageType)
|
||||
}
|
||||
|
||||
if !j.VideoFileNamingAlgorithm.IsValid() {
|
||||
logger.Errorf("invalid video file naming algorithm: %s", j.VideoFileNamingAlgorithm)
|
||||
return
|
||||
return fmt.Errorf("invalid video file naming algorithm: %s", j.VideoFileNamingAlgorithm)
|
||||
}
|
||||
|
||||
if j.Options.DryRun {
|
||||
@@ -183,10 +181,11 @@ func (j *CleanGeneratedJob) Execute(ctx context.Context, progress *job.Progress)
|
||||
|
||||
if job.IsCancelled(ctx) {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("Finished cleaning generated files")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) setTaskProgress(taskProgress float64, progress *job.Progress) {
|
||||
@@ -652,9 +651,13 @@ func (j *CleanGeneratedJob) getImagesWithHash(ctx context.Context, checksum stri
|
||||
}
|
||||
|
||||
func (j *CleanGeneratedJob) getThumbnailFileHash(basename string) (string, error) {
|
||||
var hash string
|
||||
var width int
|
||||
_, err := fmt.Sscanf(basename, "%32x_%d.jpg", &hash, &width)
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -32,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 {
|
||||
@@ -47,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)
|
||||
@@ -55,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) {
|
||||
|
||||
@@ -80,7 +80,7 @@ type totalsGenerate struct {
|
||||
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
|
||||
@@ -223,11 +223,12 @@ 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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -16,18 +16,16 @@ func (s *Manager) RunPluginTask(
|
||||
description *string,
|
||||
args plugin.OperationInput,
|
||||
) int {
|
||||
j := job.MakeJobExec(func(jobCtx context.Context, progress *job.Progress) {
|
||||
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)
|
||||
@@ -50,14 +48,14 @@ func (s *Manager) RunPluginTask(
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -34,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)
|
||||
@@ -55,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 {
|
||||
@@ -65,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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -2,15 +2,203 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
stashExec "github.com/stashapp/stash/pkg/exec"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
func ffmpegHelp(ffmpegPath string) (string, error) {
|
||||
cmd := stashExec.Command(ffmpegPath, "-h")
|
||||
bytes, err := cmd.CombinedOutput()
|
||||
output := string(bytes)
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return "", fmt.Errorf("error running ffmpeg: %v", output)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("error running ffmpeg: %v", err)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func ValidateFFMpeg(ffmpegPath string) error {
|
||||
_, err := ffmpegHelp(ffmpegPath)
|
||||
return err
|
||||
}
|
||||
|
||||
func ValidateFFMpegCodecSupport(ffmpegPath string) error {
|
||||
output, err := ffmpegHelp(ffmpegPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var missingSupport []string
|
||||
|
||||
if !strings.Contains(output, "--enable-libopus") {
|
||||
missingSupport = append(missingSupport, "libopus")
|
||||
}
|
||||
if !strings.Contains(output, "--enable-libvpx") {
|
||||
missingSupport = append(missingSupport, "libvpx")
|
||||
}
|
||||
if !strings.Contains(output, "--enable-libx264") {
|
||||
missingSupport = append(missingSupport, "libx264")
|
||||
}
|
||||
if !strings.Contains(output, "--enable-libx265") {
|
||||
missingSupport = append(missingSupport, "libx265")
|
||||
}
|
||||
if !strings.Contains(output, "--enable-libwebp") {
|
||||
missingSupport = append(missingSupport, "libwebp")
|
||||
}
|
||||
|
||||
if len(missingSupport) > 0 {
|
||||
return fmt.Errorf("ffmpeg missing codec support: %v", missingSupport)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LookPathFFMpeg() string {
|
||||
ret, _ := exec.LookPath(getFFMpegFilename())
|
||||
|
||||
if ret != "" {
|
||||
// ensure ffmpeg has the correct flags
|
||||
if err := ValidateFFMpeg(ret); err != nil {
|
||||
logger.Warnf("ffmpeg found (%s), could not be executed: %v", ret, err)
|
||||
ret = ""
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func FindFFMpeg(path string) string {
|
||||
ret := fsutil.FindInPaths([]string{path}, getFFMpegFilename())
|
||||
|
||||
if ret != "" {
|
||||
// ensure ffmpeg has the correct flags
|
||||
if err := ValidateFFMpeg(ret); err != nil {
|
||||
logger.Warnf("ffmpeg found (%s), could not be executed: %v", ret, err)
|
||||
ret = ""
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// ResolveFFMpeg attempts to resolve the path to the ffmpeg executable.
|
||||
// It first looks in the provided path, then resolves from the environment, and finally looks in the fallback path.
|
||||
// It will prefer an ffmpeg binary that has the required codec support.
|
||||
// Returns an empty string if a valid ffmpeg cannot be found.
|
||||
func ResolveFFMpeg(path string, fallbackPath string) string {
|
||||
var ret string
|
||||
// look in the provided path first
|
||||
pathFound := FindFFMpeg(path)
|
||||
if pathFound != "" {
|
||||
err := ValidateFFMpegCodecSupport(pathFound)
|
||||
if err == nil {
|
||||
return pathFound
|
||||
}
|
||||
|
||||
logger.Warnf("ffmpeg found (%s), but it is missing required flags: %v", pathFound, err)
|
||||
ret = pathFound
|
||||
}
|
||||
|
||||
// then resolve from the environment
|
||||
envFound := LookPathFFMpeg()
|
||||
if envFound != "" {
|
||||
err := ValidateFFMpegCodecSupport(envFound)
|
||||
if err == nil {
|
||||
return envFound
|
||||
}
|
||||
|
||||
logger.Warnf("ffmpeg found (%s), but it is missing required flags: %v", envFound, err)
|
||||
if ret == "" {
|
||||
ret = envFound
|
||||
}
|
||||
}
|
||||
|
||||
// finally, look in the fallback path
|
||||
fallbackFound := FindFFMpeg(fallbackPath)
|
||||
if fallbackFound != "" {
|
||||
err := ValidateFFMpegCodecSupport(fallbackFound)
|
||||
if err == nil {
|
||||
return fallbackFound
|
||||
}
|
||||
|
||||
logger.Warnf("ffmpeg found (%s), but it is missing required flags: %v", fallbackFound, err)
|
||||
if ret == "" {
|
||||
ret = fallbackFound
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f *FFMpeg) getVersion() error {
|
||||
var args Args
|
||||
args = append(args, "-version")
|
||||
cmd := f.Command(context.Background(), args)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
var err error
|
||||
if err = cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version_re := regexp.MustCompile(`ffmpeg version ((\d+)\.(\d+)(?:\.(\d+))?)`)
|
||||
stdoutStr := stdout.String()
|
||||
match := version_re.FindStringSubmatchIndex(stdoutStr)
|
||||
if match == nil {
|
||||
return errors.New("version string malformed")
|
||||
}
|
||||
|
||||
majorS := stdoutStr[match[4]:match[5]]
|
||||
minorS := stdoutStr[match[6]:match[7]]
|
||||
|
||||
// patch is optional
|
||||
var patchS string
|
||||
if match[8] != -1 && match[9] != -1 {
|
||||
patchS = stdoutStr[match[8]:match[9]]
|
||||
}
|
||||
|
||||
if i, err := strconv.Atoi(majorS); err == nil {
|
||||
f.version.major = i
|
||||
}
|
||||
if i, err := strconv.Atoi(minorS); err == nil {
|
||||
f.version.minor = i
|
||||
}
|
||||
if i, err := strconv.Atoi(patchS); err == nil {
|
||||
f.version.patch = i
|
||||
}
|
||||
logger.Debugf("FFMpeg version %d.%d.%d detected", f.version.major, f.version.minor, f.version.patch)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FFMpeg version params
|
||||
type FFMpegVersion struct {
|
||||
major int
|
||||
minor int
|
||||
patch int
|
||||
}
|
||||
|
||||
// FFMpeg provides an interface to ffmpeg.
|
||||
type FFMpeg struct {
|
||||
ffmpeg string
|
||||
version FFMpegVersion
|
||||
hwCodecSupport []VideoCodec
|
||||
}
|
||||
|
||||
@@ -19,6 +207,9 @@ func NewEncoder(ffmpegPath string) *FFMpeg {
|
||||
ret := &FFMpeg{
|
||||
ffmpeg: ffmpegPath,
|
||||
}
|
||||
if err := ret.getVersion(); err != nil {
|
||||
logger.Warnf("FFMpeg version not detected %v", err)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -27,3 +218,7 @@ func NewEncoder(ffmpegPath string) *FFMpeg {
|
||||
func (f *FFMpeg) Command(ctx context.Context, args []string) *exec.Cmd {
|
||||
return stashExec.CommandContext(ctx, string(f.ffmpeg), args...)
|
||||
}
|
||||
|
||||
func (f *FFMpeg) Path() string {
|
||||
return f.ffmpeg
|
||||
}
|
||||
|
||||
@@ -2,17 +2,83 @@ package ffmpeg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/exec"
|
||||
stashExec "github.com/stashapp/stash/pkg/exec"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
func ValidateFFProbe(ffprobePath string) error {
|
||||
cmd := stashExec.Command(ffprobePath, "-h")
|
||||
bytes, err := cmd.CombinedOutput()
|
||||
output := string(bytes)
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return fmt.Errorf("error running ffprobe: %v", output)
|
||||
}
|
||||
|
||||
return fmt.Errorf("error running ffprobe: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LookPathFFProbe() string {
|
||||
ret, _ := exec.LookPath(getFFProbeFilename())
|
||||
|
||||
if ret != "" {
|
||||
if err := ValidateFFProbe(ret); err != nil {
|
||||
logger.Warnf("ffprobe found in PATH (%s), but it is missing required flags: %v", ret, err)
|
||||
ret = ""
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func FindFFProbe(path string) string {
|
||||
ret := fsutil.FindInPaths([]string{path}, getFFProbeFilename())
|
||||
|
||||
if ret != "" {
|
||||
if err := ValidateFFProbe(ret); err != nil {
|
||||
logger.Warnf("ffprobe found (%s), but it is missing required flags: %v", ret, err)
|
||||
ret = ""
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// ResolveFFMpeg attempts to resolve the path to the ffmpeg executable.
|
||||
// It first looks in the provided path, then resolves from the environment, and finally looks in the fallback path.
|
||||
// Returns an empty string if a valid ffmpeg cannot be found.
|
||||
func ResolveFFProbe(path string, fallbackPath string) string {
|
||||
// look in the provided path first
|
||||
ret := FindFFProbe(path)
|
||||
if ret != "" {
|
||||
return ret
|
||||
}
|
||||
|
||||
// then resolve from the environment
|
||||
ret = LookPathFFProbe()
|
||||
if ret != "" {
|
||||
return ret
|
||||
}
|
||||
|
||||
// finally, look in the fallback path
|
||||
ret = FindFFProbe(fallbackPath)
|
||||
return ret
|
||||
}
|
||||
|
||||
// VideoFile represents the ffprobe output for a video file.
|
||||
type VideoFile struct {
|
||||
JSON FFProbeJSON
|
||||
@@ -75,10 +141,14 @@ func (v *VideoFile) TranscodeScale(maxSize int) (int, int) {
|
||||
// FFProbe provides an interface to the ffprobe executable.
|
||||
type FFProbe string
|
||||
|
||||
func (f *FFProbe) Path() string {
|
||||
return string(*f)
|
||||
}
|
||||
|
||||
// NewVideoFile runs ffprobe on the given path and returns a VideoFile.
|
||||
func (f *FFProbe) NewVideoFile(videoPath string) (*VideoFile, error) {
|
||||
args := []string{"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "-show_error", videoPath}
|
||||
cmd := exec.Command(string(*f), args...)
|
||||
cmd := stashExec.Command(string(*f), args...)
|
||||
out, err := cmd.Output()
|
||||
|
||||
if err != nil {
|
||||
@@ -97,7 +167,7 @@ func (f *FFProbe) NewVideoFile(videoPath string) (*VideoFile, error) {
|
||||
// Used when the frame count is missing or incorrect.
|
||||
func (f *FFProbe) GetReadFrameCount(path string) (int64, error) {
|
||||
args := []string{"-v", "quiet", "-print_format", "json", "-count_frames", "-show_format", "-show_streams", "-show_error", path}
|
||||
out, err := exec.Command(string(*f), args...).Output()
|
||||
out, err := stashExec.Command(string(*f), args...).Output()
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", path, string(out), err.Error())
|
||||
|
||||
@@ -81,6 +81,7 @@ var (
|
||||
"-f", "hls",
|
||||
"-start_number", fmt.Sprint(segment),
|
||||
"-hls_time", fmt.Sprint(segmentLength),
|
||||
"-hls_flags", "split_by_time",
|
||||
"-hls_segment_type", "mpegts",
|
||||
"-hls_playlist_type", "vod",
|
||||
"-hls_segment_filename", filepath.Join(outputDir, ".%d.ts"),
|
||||
@@ -110,6 +111,7 @@ var (
|
||||
"-f", "hls",
|
||||
"-start_number", fmt.Sprint(segment),
|
||||
"-hls_time", fmt.Sprint(segmentLength),
|
||||
"-hls_flags", "split_by_time",
|
||||
"-hls_segment_type", "mpegts",
|
||||
"-hls_playlist_type", "vod",
|
||||
"-hls_segment_filename", filepath.Join(outputDir, ".%d.ts"),
|
||||
@@ -328,7 +330,8 @@ func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args {
|
||||
|
||||
codec := HLSGetCodec(sm, s.streamType.Name)
|
||||
|
||||
args = sm.encoder.hwDeviceInit(args, codec)
|
||||
fullhw := sm.config.GetTranscodeHardwareAcceleration() && sm.encoder.hwCanFullHWTranscode(sm.context, codec, s.vf, s.maxTranscodeSize)
|
||||
args = sm.encoder.hwDeviceInit(args, codec, fullhw)
|
||||
args = append(args, extraInputArgs...)
|
||||
|
||||
if segment > 0 {
|
||||
@@ -339,7 +342,7 @@ func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args {
|
||||
|
||||
videoOnly := ProbeAudioCodec(s.vf.AudioCodec) == MissingUnsupported
|
||||
|
||||
videoFilter := sm.encoder.hwMaxResFilter(codec, s.vf.Width, s.vf.Height, s.maxTranscodeSize)
|
||||
videoFilter := sm.encoder.hwMaxResFilter(codec, s.vf.Width, s.vf.Height, s.maxTranscodeSize, fullhw)
|
||||
|
||||
args = append(args, s.streamType.Args(codec, segment, videoFilter, videoOnly, s.outputDir)...)
|
||||
|
||||
|
||||
@@ -138,14 +138,30 @@ type TranscodeOptions struct {
|
||||
StartTime float64
|
||||
}
|
||||
|
||||
func FileGetCodec(sm *StreamManager, mimetype string) (codec VideoCodec) {
|
||||
switch mimetype {
|
||||
func (o TranscodeOptions) FileGetCodec(sm *StreamManager, maxTranscodeSize int) (codec VideoCodec) {
|
||||
needsResize := false
|
||||
|
||||
if maxTranscodeSize != 0 {
|
||||
if o.VideoFile.Width > o.VideoFile.Height {
|
||||
needsResize = o.VideoFile.Width > maxTranscodeSize
|
||||
} else {
|
||||
needsResize = o.VideoFile.Height > maxTranscodeSize
|
||||
}
|
||||
}
|
||||
|
||||
switch o.StreamType.MimeType {
|
||||
case MimeMp4Video:
|
||||
if !needsResize && o.VideoFile.VideoCodec == H264 {
|
||||
return VideoCodecCopy
|
||||
}
|
||||
codec = VideoCodecLibX264
|
||||
if hwcodec := sm.encoder.hwCodecMP4Compatible(); hwcodec != nil && sm.config.GetTranscodeHardwareAcceleration() {
|
||||
codec = *hwcodec
|
||||
}
|
||||
case MimeWebmVideo:
|
||||
if !needsResize && (o.VideoFile.VideoCodec == Vp8 || o.VideoFile.VideoCodec == Vp9) {
|
||||
return VideoCodecCopy
|
||||
}
|
||||
codec = VideoCodecVP9
|
||||
if hwcodec := sm.encoder.hwCodecWEBMCompatible(); hwcodec != nil && sm.config.GetTranscodeHardwareAcceleration() {
|
||||
codec = *hwcodec
|
||||
@@ -168,9 +184,10 @@ func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args {
|
||||
args := Args{"-hide_banner"}
|
||||
args = args.LogLevel(LogLevelError)
|
||||
|
||||
codec := FileGetCodec(sm, o.StreamType.MimeType)
|
||||
codec := o.FileGetCodec(sm, maxTranscodeSize)
|
||||
|
||||
args = sm.encoder.hwDeviceInit(args, codec)
|
||||
fullhw := sm.config.GetTranscodeHardwareAcceleration() && sm.encoder.hwCanFullHWTranscode(sm.context, codec, o.VideoFile, maxTranscodeSize)
|
||||
args = sm.encoder.hwDeviceInit(args, codec, fullhw)
|
||||
args = append(args, extraInputArgs...)
|
||||
|
||||
if o.StartTime != 0 {
|
||||
@@ -181,7 +198,7 @@ func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args {
|
||||
|
||||
videoOnly := ProbeAudioCodec(o.VideoFile.AudioCodec) == MissingUnsupported
|
||||
|
||||
videoFilter := sm.encoder.hwMaxResFilter(codec, o.VideoFile.Width, o.VideoFile.Height, maxTranscodeSize)
|
||||
videoFilter := sm.encoder.hwMaxResFilter(codec, o.VideoFile.Width, o.VideoFile.Height, maxTranscodeSize, fullhw)
|
||||
|
||||
args = append(args, o.StreamType.Args(codec, videoFilter, videoOnly)...)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ type cleanJob struct {
|
||||
options CleanOptions
|
||||
}
|
||||
|
||||
// ScanOptions provides options for scanning files.
|
||||
// CleanOptions provides options for scanning files.
|
||||
type CleanOptions struct {
|
||||
Paths []string
|
||||
|
||||
|
||||
@@ -134,6 +134,9 @@ type ScanOptions struct {
|
||||
HandlerRequiredFilters []Filter
|
||||
|
||||
ParallelTasks int
|
||||
|
||||
// When true files in path will be rescanned even if they haven't changed
|
||||
Rescan bool
|
||||
}
|
||||
|
||||
// Scan starts the scanning process.
|
||||
@@ -895,28 +898,35 @@ func (s *scanJob) handleRename(ctx context.Context, f models.File, fp []models.F
|
||||
// assume does not exist, update existing file
|
||||
// it's possible that there may be multiple missing files.
|
||||
// just use the first one to rename.
|
||||
// #4775 - using the new file instance means that any changes made to the existing
|
||||
// file will be lost. Update the existing file instead.
|
||||
other := missing[0]
|
||||
otherBase := other.Base()
|
||||
updated := other.Clone()
|
||||
updatedBase := updated.Base()
|
||||
|
||||
fBase := f.Base()
|
||||
fBaseCopy := *(f.Base())
|
||||
|
||||
logger.Infof("%s moved to %s. Updating path...", otherBase.Path, fBase.Path)
|
||||
fBase.ID = otherBase.ID
|
||||
fBase.CreatedAt = otherBase.CreatedAt
|
||||
fBase.Fingerprints = otherBase.Fingerprints
|
||||
oldPath := updatedBase.Path
|
||||
newPath := fBaseCopy.Path
|
||||
|
||||
logger.Infof("%s moved to %s. Updating path...", oldPath, newPath)
|
||||
fBaseCopy.ID = updatedBase.ID
|
||||
fBaseCopy.CreatedAt = updatedBase.CreatedAt
|
||||
fBaseCopy.Fingerprints = updatedBase.Fingerprints
|
||||
*updatedBase = fBaseCopy
|
||||
|
||||
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err := s.Repository.File.Update(ctx, f); err != nil {
|
||||
return fmt.Errorf("updating file for rename %q: %w", fBase.Path, err)
|
||||
if err := s.Repository.File.Update(ctx, updated); err != nil {
|
||||
return fmt.Errorf("updating file for rename %q: %w", newPath, err)
|
||||
}
|
||||
|
||||
if s.isZipFile(fBase.Basename) {
|
||||
if err := transferZipHierarchy(ctx, s.Repository.Folder, s.Repository.File, fBase.ID, otherBase.Path, fBase.Path); err != nil {
|
||||
return fmt.Errorf("moving zip hierarchy for renamed zip file %q: %w", fBase.Path, err)
|
||||
if s.isZipFile(updatedBase.Basename) {
|
||||
if err := transferZipHierarchy(ctx, s.Repository.Folder, s.Repository.File, updatedBase.ID, oldPath, newPath); err != nil {
|
||||
return fmt.Errorf("moving zip hierarchy for renamed zip file %q: %w", newPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.fireHandlers(ctx, f, other); err != nil {
|
||||
if err := s.fireHandlers(ctx, updated, other); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -925,7 +935,7 @@ func (s *scanJob) handleRename(ctx context.Context, f models.File, fp []models.F
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *scanJob) isHandlerRequired(ctx context.Context, f models.File) bool {
|
||||
@@ -1016,14 +1026,20 @@ func (s *scanJob) onExistingFile(ctx context.Context, f scanFile, existing model
|
||||
|
||||
fileModTime := f.ModTime
|
||||
updated := !fileModTime.Equal(base.ModTime)
|
||||
forceRescan := s.options.Rescan
|
||||
|
||||
if !updated {
|
||||
if !updated && !forceRescan {
|
||||
return s.onUnchangedFile(ctx, f, existing)
|
||||
}
|
||||
|
||||
oldBase := *base
|
||||
|
||||
logger.Infof("%s has been updated: rescanning", path)
|
||||
if !updated && forceRescan {
|
||||
logger.Infof("rescanning %s", path)
|
||||
} else {
|
||||
logger.Infof("%s has been updated: rescanning", path)
|
||||
}
|
||||
|
||||
base.ModTime = fileModTime
|
||||
base.Size = f.Size
|
||||
base.UpdatedAt = time.Now()
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
@@ -163,3 +164,12 @@ func SanitiseBasename(v string) string {
|
||||
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
|
||||
// GetExeName returns the name of the given executable for the current platform.
|
||||
// One windows it returns the name with the .exe extension.
|
||||
func GetExeName(base string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return base + ".exe"
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
26
pkg/javascript/console.go
Normal file
26
pkg/javascript/console.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package javascript
|
||||
|
||||
import "fmt"
|
||||
|
||||
type console struct {
|
||||
Log
|
||||
}
|
||||
|
||||
func (c *console) AddToVM(globalName string, vm *VM) error {
|
||||
console := vm.NewObject()
|
||||
if err := SetAll(console,
|
||||
ObjectValueDef{"log", c.logInfo},
|
||||
ObjectValueDef{"error", c.logError},
|
||||
ObjectValueDef{"warn", c.logWarn},
|
||||
ObjectValueDef{"info", c.logInfo},
|
||||
ObjectValueDef{"debug", c.logDebug},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := vm.Set(globalName, console); err != nil {
|
||||
return fmt.Errorf("unable to set console: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
106
pkg/javascript/gql.go
Normal file
106
pkg/javascript/gql.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package javascript
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
type responseWriter struct {
|
||||
r strings.Builder
|
||||
header http.Header
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *responseWriter) Header() http.Header {
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteHeader(statusCode int) {
|
||||
w.statusCode = statusCode
|
||||
}
|
||||
|
||||
func (w *responseWriter) Write(b []byte) (int, error) {
|
||||
return w.r.Write(b)
|
||||
}
|
||||
|
||||
type GQL struct {
|
||||
Context context.Context
|
||||
Cookie *http.Cookie
|
||||
GQLHandler http.Handler
|
||||
}
|
||||
|
||||
func (g *GQL) gqlRequestFunc(vm *VM) func(query string, variables map[string]interface{}) (goja.Value, error) {
|
||||
return func(query string, variables map[string]interface{}) (goja.Value, error) {
|
||||
in := struct {
|
||||
Query string `json:"query"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||
}{
|
||||
Query: query,
|
||||
Variables: variables,
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
err := json.NewEncoder(&body).Encode(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := http.NewRequestWithContext(g.Context, "POST", "/graphql", &body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not make request")
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if g.Cookie != nil {
|
||||
r.AddCookie(g.Cookie)
|
||||
}
|
||||
|
||||
w := &responseWriter{
|
||||
header: make(http.Header),
|
||||
}
|
||||
|
||||
g.GQLHandler.ServeHTTP(w, r)
|
||||
|
||||
if w.statusCode != http.StatusOK && w.statusCode != 0 {
|
||||
vm.Throw(fmt.Errorf("graphQL query failed: %d - %s. Query: %s. Variables: %v", w.statusCode, w.r.String(), in.Query, in.Variables))
|
||||
}
|
||||
|
||||
output := w.r.String()
|
||||
// convert to JSON
|
||||
var obj map[string]interface{}
|
||||
if err = json.Unmarshal([]byte(output), &obj); err != nil {
|
||||
vm.Throw(fmt.Errorf("could not unmarshal object %s: %s", output, err.Error()))
|
||||
}
|
||||
|
||||
retErr, hasErr := obj["errors"]
|
||||
|
||||
if hasErr {
|
||||
errOut, _ := json.Marshal(retErr)
|
||||
vm.Throw(fmt.Errorf("graphql error: %s", string(errOut)))
|
||||
}
|
||||
|
||||
v := vm.ToValue(obj["data"])
|
||||
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GQL) AddToVM(globalName string, vm *VM) error {
|
||||
gql := vm.NewObject()
|
||||
|
||||
if err := gql.Set("Do", g.gqlRequestFunc(vm)); err != nil {
|
||||
return fmt.Errorf("unable to set GraphQL Do function: %w", err)
|
||||
}
|
||||
|
||||
if err := vm.Set(globalName, gql); err != nil {
|
||||
return fmt.Errorf("unable to set gql: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
90
pkg/javascript/log.go
Normal file
90
pkg/javascript/log.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package javascript
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
// Log provides log wrappers for usable from the JS VM.
|
||||
type Log struct {
|
||||
// Logger is the LoggerImpl to forward log messages to.
|
||||
Logger logger.LoggerImpl
|
||||
// Prefix is the prefix to prepend to log messages.
|
||||
Prefix string
|
||||
// ProgressChan is a channel that receives float64s indicating the current progress of an operation.
|
||||
ProgressChan chan float64
|
||||
}
|
||||
|
||||
func (l *Log) argToString(call goja.FunctionCall) string {
|
||||
arg := call.Argument(0)
|
||||
var o map[string]interface{}
|
||||
if arg.ExportType() == reflect.TypeOf(o) {
|
||||
ii := arg.Export()
|
||||
o = ii.(map[string]interface{})
|
||||
data, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
logger.Warnf("Couldn't json encode object")
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
return arg.String()
|
||||
}
|
||||
|
||||
func (l *Log) logTrace(call goja.FunctionCall) goja.Value {
|
||||
l.Logger.Trace(l.Prefix, l.argToString(call))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) logDebug(call goja.FunctionCall) goja.Value {
|
||||
l.Logger.Debug(l.Prefix, l.argToString(call))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) logInfo(call goja.FunctionCall) goja.Value {
|
||||
l.Logger.Info(l.Prefix, l.argToString(call))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) logWarn(call goja.FunctionCall) goja.Value {
|
||||
l.Logger.Warn(l.Prefix, l.argToString(call))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) logError(call goja.FunctionCall) goja.Value {
|
||||
l.Logger.Error(l.Prefix, l.argToString(call))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Progress logs the current progress value. The progress value should be
|
||||
// between 0 and 1.0 inclusively, with 1 representing that the task is
|
||||
// complete. Values outside of this range will be clamp to be within it.
|
||||
func (l *Log) logProgress(value float64) {
|
||||
value = math.Min(math.Max(0, value), 1)
|
||||
l.ProgressChan <- value
|
||||
}
|
||||
|
||||
func (l *Log) AddToVM(globalName string, vm *VM) error {
|
||||
log := vm.NewObject()
|
||||
if err := SetAll(log,
|
||||
ObjectValueDef{"Trace", l.logTrace},
|
||||
ObjectValueDef{"Debug", l.logDebug},
|
||||
ObjectValueDef{"Info", l.logInfo},
|
||||
ObjectValueDef{"Warn", l.logWarn},
|
||||
ObjectValueDef{"Error", l.logError},
|
||||
ObjectValueDef{"Progress", l.logProgress},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := vm.Set(globalName, log); err != nil {
|
||||
return fmt.Errorf("unable to set log: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
25
pkg/javascript/util.go
Normal file
25
pkg/javascript/util.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package javascript
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Util struct{}
|
||||
|
||||
func (u *Util) sleepFunc(ms int64) {
|
||||
time.Sleep(time.Millisecond * time.Duration(ms))
|
||||
}
|
||||
|
||||
func (u *Util) AddToVM(globalName string, vm *VM) error {
|
||||
util := vm.NewObject()
|
||||
if err := util.Set("Sleep", u.sleepFunc); err != nil {
|
||||
return fmt.Errorf("unable to set sleep func: %w", err)
|
||||
}
|
||||
|
||||
if err := vm.Set(globalName, util); err != nil {
|
||||
return fmt.Errorf("unable to set util: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
96
pkg/javascript/vm.go
Normal file
96
pkg/javascript/vm.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package javascript
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
type VM struct {
|
||||
*goja.Runtime
|
||||
|
||||
Progress chan float64
|
||||
SessionCookie *http.Cookie
|
||||
GQLHandler http.Handler
|
||||
}
|
||||
|
||||
// optionalFieldNameMapper wraps a goja.FieldNameMapper and returns the field name if the wrapped mapper returns an empty string.
|
||||
type optionalFieldNameMapper struct {
|
||||
mapper goja.FieldNameMapper
|
||||
}
|
||||
|
||||
func (tfm optionalFieldNameMapper) FieldName(t reflect.Type, f reflect.StructField) string {
|
||||
if ret := tfm.mapper.FieldName(t, f); ret != "" {
|
||||
return ret
|
||||
}
|
||||
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (tfm optionalFieldNameMapper) MethodName(t reflect.Type, m reflect.Method) string {
|
||||
return tfm.mapper.MethodName(t, m)
|
||||
}
|
||||
|
||||
func NewVM() *VM {
|
||||
r := goja.New()
|
||||
|
||||
// enable console for backwards compatibility
|
||||
c := console{
|
||||
Log{
|
||||
Logger: logger.Logger,
|
||||
},
|
||||
}
|
||||
|
||||
// there should not be any reason for this to fail
|
||||
_ = c.AddToVM("console", &VM{Runtime: r})
|
||||
|
||||
r.SetFieldNameMapper(optionalFieldNameMapper{goja.TagFieldNameMapper("json", true)})
|
||||
return &VM{Runtime: r}
|
||||
}
|
||||
|
||||
type APIAdder interface {
|
||||
AddToVM(globalName string, vm *VM) error
|
||||
}
|
||||
|
||||
type ObjectValueDef struct {
|
||||
Name string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
type setter interface {
|
||||
Set(name string, value interface{}) error
|
||||
}
|
||||
|
||||
func Compile(path string) (*goja.Program, error) {
|
||||
js, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
return goja.Compile(path, string(js), true)
|
||||
}
|
||||
|
||||
func CompileScript(name, script string) (*goja.Program, error) {
|
||||
return goja.Compile(name, string(script), true)
|
||||
}
|
||||
|
||||
func SetAll(s setter, defs ...ObjectValueDef) error {
|
||||
for _, def := range defs {
|
||||
if err := s.Set(def.Name, def.Value); err != nil {
|
||||
return fmt.Errorf("failed to set %s: %w", def.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VM) Throw(err error) {
|
||||
e, newErr := v.New(v.Get("Error"), v.ToValue(err))
|
||||
if newErr != nil {
|
||||
panic(newErr)
|
||||
}
|
||||
|
||||
panic(e)
|
||||
}
|
||||
@@ -5,22 +5,24 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type JobExecFn func(ctx context.Context, progress *Progress) error
|
||||
|
||||
// JobExec represents the implementation of a Job to be executed.
|
||||
type JobExec interface {
|
||||
Execute(ctx context.Context, progress *Progress)
|
||||
Execute(ctx context.Context, progress *Progress) error
|
||||
}
|
||||
|
||||
type jobExecImpl struct {
|
||||
fn func(ctx context.Context, progress *Progress)
|
||||
fn JobExecFn
|
||||
}
|
||||
|
||||
func (j *jobExecImpl) Execute(ctx context.Context, progress *Progress) {
|
||||
j.fn(ctx, progress)
|
||||
func (j *jobExecImpl) Execute(ctx context.Context, progress *Progress) error {
|
||||
return j.fn(ctx, progress)
|
||||
}
|
||||
|
||||
// MakeJobExec returns a simple JobExec implementation using the provided
|
||||
// function.
|
||||
func MakeJobExec(fn func(ctx context.Context, progress *Progress)) JobExec {
|
||||
func MakeJobExec(fn JobExecFn) JobExec {
|
||||
return &jobExecImpl{
|
||||
fn: fn,
|
||||
}
|
||||
@@ -56,6 +58,7 @@ type Job struct {
|
||||
StartTime *time.Time
|
||||
EndTime *time.Time
|
||||
AddTime time.Time
|
||||
Error *string
|
||||
|
||||
outerCtx context.Context
|
||||
exec JobExec
|
||||
@@ -87,6 +90,12 @@ func (j *Job) cancel() {
|
||||
}
|
||||
}
|
||||
|
||||
func (j *Job) error(err error) {
|
||||
errStr := err.Error()
|
||||
j.Error = &errStr
|
||||
j.Status = StatusFailed
|
||||
}
|
||||
|
||||
// IsCancelled returns true if cancel has been called on the context.
|
||||
func IsCancelled(ctx context.Context) bool {
|
||||
select {
|
||||
|
||||
@@ -206,7 +206,10 @@ func (m *Manager) executeJob(ctx context.Context, j *Job, done chan struct{}) {
|
||||
}()
|
||||
|
||||
progress := m.newProgress(j)
|
||||
j.exec.Execute(ctx, progress)
|
||||
if err := j.exec.Execute(ctx, progress); err != nil {
|
||||
logger.Errorf("task failed due to error: %v", err)
|
||||
j.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) onJobFinish(job *Job) {
|
||||
|
||||
@@ -24,7 +24,7 @@ func newTestExec(finish chan struct{}) *testExec {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *testExec) Execute(ctx context.Context, p *Progress) {
|
||||
func (e *testExec) Execute(ctx context.Context, p *Progress) error {
|
||||
e.progress = p
|
||||
close(e.started)
|
||||
|
||||
@@ -38,6 +38,8 @@ func (e *testExec) Execute(ctx context.Context, p *Progress) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
|
||||
@@ -27,6 +27,8 @@ type GalleryFilterType struct {
|
||||
AverageResolution *ResolutionCriterionInput `json:"average_resolution"`
|
||||
// Filter to only include scenes which have chapters. `true` or `false`
|
||||
HasChapters *string `json:"has_chapters"`
|
||||
// Filter to only include galleries with these scenes
|
||||
Scenes *MultiCriterionInput `json:"scenes"`
|
||||
// Filter to only include galleries with this studio
|
||||
Studios *HierarchicalMultiCriterionInput `json:"studios"`
|
||||
// Filter to only include galleries with these tags
|
||||
|
||||
@@ -18,6 +18,7 @@ type Studio struct {
|
||||
CreatedAt json.JSONTime `json:"created_at,omitempty"`
|
||||
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
|
||||
Rating int `json:"rating,omitempty"`
|
||||
Favorite bool `json:"favorite,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
StashIDs []models.StashID `json:"stash_ids,omitempty"`
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
type Tag struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Favorite bool `json:"favorite,omitempty"`
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
Parents []string `json:"parents,omitempty"`
|
||||
|
||||
@@ -111,6 +111,7 @@ type File interface {
|
||||
Base() *BaseFile
|
||||
SetFingerprints(fp Fingerprints)
|
||||
Open(fs FS) (io.ReadCloser, error)
|
||||
Clone() File
|
||||
}
|
||||
|
||||
// BaseFile represents a file in the file system.
|
||||
@@ -173,6 +174,12 @@ func (f *BaseFile) Open(fs FS) (io.ReadCloser, error) {
|
||||
return fs.Open(f.Path)
|
||||
}
|
||||
|
||||
func (f *BaseFile) Clone() (ret File) {
|
||||
clone := *f
|
||||
ret = &clone
|
||||
return
|
||||
}
|
||||
|
||||
func (f *BaseFile) Info(fs FS) (fs.FileInfo, error) {
|
||||
return f.info(fs, f.Path)
|
||||
}
|
||||
@@ -249,6 +256,13 @@ func (f ImageFile) GetFormat() string {
|
||||
return f.Format
|
||||
}
|
||||
|
||||
func (f ImageFile) Clone() (ret File) {
|
||||
clone := f
|
||||
clone.BaseFile = f.BaseFile.Clone().(*BaseFile)
|
||||
ret = &clone
|
||||
return
|
||||
}
|
||||
|
||||
// VideoFile is an extension of BaseFile to represent video files.
|
||||
type VideoFile struct {
|
||||
*BaseFile
|
||||
@@ -277,6 +291,13 @@ func (f VideoFile) GetFormat() string {
|
||||
return f.Format
|
||||
}
|
||||
|
||||
func (f VideoFile) Clone() (ret File) {
|
||||
clone := f
|
||||
clone.BaseFile = f.BaseFile.Clone().(*BaseFile)
|
||||
ret = &clone
|
||||
return
|
||||
}
|
||||
|
||||
// #1572 - Inf and NaN values cause the JSON marshaller to fail
|
||||
// Replace these values with 0 rather than erroring
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ type Studio struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating *int `json:"rating"`
|
||||
Favorite bool `json:"favorite"`
|
||||
Details string `json:"details"`
|
||||
IgnoreAutoTag bool `json:"ignore_auto_tag"`
|
||||
|
||||
@@ -37,6 +38,7 @@ type StudioPartial struct {
|
||||
ParentID OptionalInt
|
||||
// Rating expressed in 1-100 scale
|
||||
Rating OptionalInt
|
||||
Favorite OptionalBool
|
||||
Details OptionalString
|
||||
CreatedAt OptionalTime
|
||||
UpdatedAt OptionalTime
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
type Tag struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Favorite bool `json:"favorite"`
|
||||
Description string `json:"description"`
|
||||
IgnoreAutoTag bool `json:"ignore_auto_tag"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -24,6 +25,7 @@ func NewTag() Tag {
|
||||
type TagPartial struct {
|
||||
Name OptionalString
|
||||
Description OptionalString
|
||||
Favorite OptionalBool
|
||||
IgnoreAutoTag OptionalBool
|
||||
CreatedAt OptionalTime
|
||||
UpdatedAt OptionalTime
|
||||
|
||||
@@ -160,6 +160,8 @@ type PerformerFilterType struct {
|
||||
ImageCount *IntCriterionInput `json:"image_count"`
|
||||
// Filter by gallery count
|
||||
GalleryCount *IntCriterionInput `json:"gallery_count"`
|
||||
// Filter by play count
|
||||
PlayCount *IntCriterionInput `json:"play_count"`
|
||||
// Filter by O count
|
||||
OCounter *IntCriterionInput `json:"o_counter"`
|
||||
// Filter by StashID
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import "context"
|
||||
|
||||
// FileGetter provides methods to get files by ID.
|
||||
// BlobReader provides methods to get files by ID.
|
||||
type BlobReader interface {
|
||||
EntryExists(ctx context.Context, checksum string) (bool, error)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ type SceneFilterType struct {
|
||||
Orientation *OrientationCriterionInput `json:"orientation"`
|
||||
// Filter by framerate
|
||||
Framerate *IntCriterionInput `json:"framerate"`
|
||||
// Filter by bitrate
|
||||
Bitrate *IntCriterionInput `json:"bitrate"`
|
||||
// Filter by video codec
|
||||
VideoCodec *StringCriterionInput `json:"video_codec"`
|
||||
// Filter by audio codec
|
||||
@@ -57,6 +59,8 @@ type SceneFilterType struct {
|
||||
Studios *HierarchicalMultiCriterionInput `json:"studios"`
|
||||
// Filter to only include scenes with this movie
|
||||
Movies *MultiCriterionInput `json:"movies"`
|
||||
// Filter to only include scenes with this gallery
|
||||
Galleries *MultiCriterionInput `json:"galleries"`
|
||||
// Filter to only include scenes with these tags
|
||||
Tags *HierarchicalMultiCriterionInput `json:"tags"`
|
||||
// Filter by tag count
|
||||
@@ -89,6 +93,8 @@ type SceneFilterType struct {
|
||||
PlayCount *IntCriterionInput `json:"play_count"`
|
||||
// Filter by play duration (in seconds)
|
||||
PlayDuration *IntCriterionInput `json:"play_duration"`
|
||||
// Filter by last played at
|
||||
LastPlayedAt *TimestampCriterionInput `json:"last_played_at"`
|
||||
// Filter by date
|
||||
Date *DateCriterionInput `json:"date"`
|
||||
// Filter by created at
|
||||
|
||||
@@ -16,6 +16,8 @@ type StudioFilterType struct {
|
||||
IsMissing *string `json:"is_missing"`
|
||||
// Filter by rating expressed as 1-100
|
||||
Rating100 *IntCriterionInput `json:"rating100"`
|
||||
// Filter by favorite
|
||||
Favorite *bool `json:"favorite"`
|
||||
// Filter by scene count
|
||||
SceneCount *IntCriterionInput `json:"scene_count"`
|
||||
// Filter by image count
|
||||
@@ -44,6 +46,7 @@ type StudioCreateInput struct {
|
||||
Image *string `json:"image"`
|
||||
StashIds []StashID `json:"stash_ids"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Favorite *bool `json:"favorite"`
|
||||
Details *string `json:"details"`
|
||||
Aliases []string `json:"aliases"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
@@ -58,6 +61,7 @@ type StudioUpdateInput struct {
|
||||
Image *string `json:"image"`
|
||||
StashIds []StashID `json:"stash_ids"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Favorite *bool `json:"favorite"`
|
||||
Details *string `json:"details"`
|
||||
Aliases []string `json:"aliases"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
|
||||
@@ -8,6 +8,8 @@ type TagFilterType struct {
|
||||
Name *StringCriterionInput `json:"name"`
|
||||
// Filter by tag aliases
|
||||
Aliases *StringCriterionInput `json:"aliases"`
|
||||
// Filter by tag favorites
|
||||
Favorite *bool `json:"favorite"`
|
||||
// Filter by tag description
|
||||
Description *StringCriterionInput `json:"description"`
|
||||
// Filter to only include tags missing this property
|
||||
|
||||
@@ -177,7 +177,7 @@ func NewOptionalBoolPtr(v *bool) OptionalBool {
|
||||
return OptionalBool{*v, false, true}
|
||||
}
|
||||
|
||||
// OptionalBool represents an optional float64 argument that may be null. See OptionalString.
|
||||
// OptionalFloat64 represents an optional float64 argument that may be null. See OptionalString.
|
||||
type OptionalFloat64 struct {
|
||||
Value float64
|
||||
Null bool
|
||||
|
||||
@@ -77,6 +77,14 @@ func (m ArgsMap) Float(key string) float64 {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m ArgsMap) ToMap() map[string]interface{} {
|
||||
ret := make(map[string]interface{})
|
||||
for k, v := range m {
|
||||
ret[k] = v
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// PluginInput is the data structure that is sent to plugin instances when they
|
||||
// are spawned.
|
||||
type PluginInput struct {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/python"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
@@ -294,16 +295,18 @@ func (c Config) getConfigPath() string {
|
||||
}
|
||||
|
||||
func (c Config) getExecCommand(task *OperationConfig) []string {
|
||||
ret := c.Exec
|
||||
// #4859 - don't modify the original exec command
|
||||
ret := append([]string{}, c.Exec...)
|
||||
|
||||
if task != nil {
|
||||
ret = append(ret, task.ExecArgs...)
|
||||
}
|
||||
|
||||
if len(ret) > 0 {
|
||||
// #4859 - don't use the plugin path in the exec command if it is a python command
|
||||
if len(ret) > 0 && !python.IsPythonCommand(ret[0]) {
|
||||
_, err := exec.LookPath(ret[0])
|
||||
if err != nil {
|
||||
// change command to use absolute path
|
||||
// change command to run from the plugin path
|
||||
pluginPath := filepath.Dir(c.path)
|
||||
ret[0] = filepath.Join(pluginPath, ret[0])
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/stashapp/stash/pkg/javascript"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/plugin/common"
|
||||
"github.com/stashapp/stash/pkg/plugin/js"
|
||||
)
|
||||
|
||||
var errStop = errors.New("stop")
|
||||
@@ -27,7 +28,7 @@ type jsPluginTask struct {
|
||||
|
||||
started bool
|
||||
waitGroup sync.WaitGroup
|
||||
vm *otto.Otto
|
||||
vm *javascript.VM
|
||||
}
|
||||
|
||||
func (t *jsPluginTask) onError(err error) {
|
||||
@@ -37,24 +38,73 @@ func (t *jsPluginTask) onError(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *jsPluginTask) makeOutput(o otto.Value) {
|
||||
func (t *jsPluginTask) makeOutput(o goja.Value) {
|
||||
t.result = &common.PluginOutput{}
|
||||
|
||||
asObj := o.Object()
|
||||
asObj := o.ToObject(t.vm.Runtime)
|
||||
if asObj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
output, _ := asObj.Get("Output")
|
||||
t.result.Output, _ = output.Export()
|
||||
|
||||
err, _ := asObj.Get("Error")
|
||||
if !err.IsUndefined() {
|
||||
t.result.Output = asObj.Get("Output")
|
||||
err := asObj.Get("Error")
|
||||
if !goja.IsNull(err) && !goja.IsUndefined(err) {
|
||||
errStr := err.String()
|
||||
t.result.Error = &errStr
|
||||
}
|
||||
}
|
||||
|
||||
func (t *jsPluginTask) initVM() error {
|
||||
// converting the Args field to map[string]interface{} is required, otherwise
|
||||
// it gets converted to an empty object
|
||||
// ideally this should have included json tags with the correct casing but changing
|
||||
// it now will result in a breaking change
|
||||
type pluginInput struct {
|
||||
// Server details to connect to the stash server.
|
||||
ServerConnection common.StashServerConnection
|
||||
|
||||
// Arguments to the plugin operation.
|
||||
Args map[string]interface{}
|
||||
}
|
||||
|
||||
input := pluginInput{
|
||||
ServerConnection: t.input.ServerConnection,
|
||||
Args: t.input.Args.ToMap(),
|
||||
}
|
||||
|
||||
if err := t.vm.Set("input", input); err != nil {
|
||||
return fmt.Errorf("error setting input: %w", err)
|
||||
}
|
||||
|
||||
const pluginPrefix = "[Plugin / %s] "
|
||||
|
||||
log := &javascript.Log{
|
||||
Logger: logger.Logger,
|
||||
Prefix: fmt.Sprintf(pluginPrefix, t.plugin.Name),
|
||||
ProgressChan: t.progress,
|
||||
}
|
||||
|
||||
if err := log.AddToVM("log", t.vm); err != nil {
|
||||
return fmt.Errorf("error adding log API: %w", err)
|
||||
}
|
||||
|
||||
util := &javascript.Util{}
|
||||
if err := util.AddToVM("util", t.vm); err != nil {
|
||||
return fmt.Errorf("error adding util API: %w", err)
|
||||
}
|
||||
|
||||
gql := &javascript.GQL{
|
||||
Context: context.TODO(),
|
||||
Cookie: t.input.ServerConnection.SessionCookie,
|
||||
GQLHandler: t.gqlHandler,
|
||||
}
|
||||
if err := gql.AddToVM("gql", t.vm); err != nil {
|
||||
return fmt.Errorf("error adding GraphQL API: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *jsPluginTask) Start() error {
|
||||
if t.started {
|
||||
return errors.New("task already started")
|
||||
@@ -68,31 +118,17 @@ func (t *jsPluginTask) Start() error {
|
||||
|
||||
scriptFile := t.plugin.Exec[0]
|
||||
|
||||
t.vm = otto.New()
|
||||
t.vm = javascript.NewVM()
|
||||
pluginPath := t.plugin.getConfigPath()
|
||||
script, err := t.vm.Compile(filepath.Join(pluginPath, scriptFile), nil)
|
||||
script, err := javascript.Compile(filepath.Join(pluginPath, scriptFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := t.vm.Set("input", t.input); err != nil {
|
||||
return fmt.Errorf("error setting input: %w", err)
|
||||
if err := t.initVM(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := js.AddLogAPI(t.vm, t.progress); err != nil {
|
||||
return fmt.Errorf("error adding log API: %w", err)
|
||||
}
|
||||
|
||||
if err := js.AddUtilAPI(t.vm); err != nil {
|
||||
return fmt.Errorf("error adding util API: %w", err)
|
||||
}
|
||||
|
||||
if err := js.AddGQLAPI(context.TODO(), t.vm, t.input.ServerConnection.SessionCookie, t.gqlHandler); err != nil {
|
||||
return fmt.Errorf("error adding GraphQL API: %w", err)
|
||||
}
|
||||
|
||||
t.vm.Interrupt = make(chan func(), 1)
|
||||
|
||||
t.waitGroup.Add(1)
|
||||
|
||||
go func() {
|
||||
@@ -107,7 +143,7 @@ func (t *jsPluginTask) Start() error {
|
||||
}
|
||||
}()
|
||||
|
||||
output, err := t.vm.Run(script)
|
||||
output, err := t.vm.RunProgram(script)
|
||||
|
||||
if err != nil {
|
||||
t.onError(err)
|
||||
@@ -124,9 +160,6 @@ func (t *jsPluginTask) Wait() {
|
||||
}
|
||||
|
||||
func (t *jsPluginTask) Stop() error {
|
||||
// TODO - need another way of doing this that doesn't require panic
|
||||
t.vm.Interrupt <- func() {
|
||||
panic(errStop)
|
||||
}
|
||||
t.vm.Interrupt(errStop)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package js
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
type responseWriter struct {
|
||||
r strings.Builder
|
||||
header http.Header
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *responseWriter) Header() http.Header {
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteHeader(statusCode int) {
|
||||
w.statusCode = statusCode
|
||||
}
|
||||
|
||||
func (w *responseWriter) Write(b []byte) (int, error) {
|
||||
return w.r.Write(b)
|
||||
}
|
||||
|
||||
func throw(vm *otto.Otto, str string) {
|
||||
value, _ := vm.Call("new Error", nil, str)
|
||||
panic(value)
|
||||
}
|
||||
|
||||
func gqlRequestFunc(ctx context.Context, vm *otto.Otto, cookie *http.Cookie, gqlHandler http.Handler) func(call otto.FunctionCall) otto.Value {
|
||||
return func(call otto.FunctionCall) otto.Value {
|
||||
if len(call.ArgumentList) == 0 {
|
||||
throw(vm, "missing argument")
|
||||
}
|
||||
|
||||
query := call.Argument(0)
|
||||
vars := call.Argument(1)
|
||||
var variables map[string]interface{}
|
||||
if !vars.IsUndefined() {
|
||||
exported, _ := vars.Export()
|
||||
variables, _ = exported.(map[string]interface{})
|
||||
}
|
||||
|
||||
in := struct {
|
||||
Query string `json:"query"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||
}{
|
||||
Query: query.String(),
|
||||
Variables: variables,
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
err := json.NewEncoder(&body).Encode(in)
|
||||
if err != nil {
|
||||
throw(vm, err.Error())
|
||||
}
|
||||
|
||||
r, err := http.NewRequestWithContext(ctx, "POST", "/graphql", &body)
|
||||
if err != nil {
|
||||
throw(vm, "could not make request")
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if cookie != nil {
|
||||
r.AddCookie(cookie)
|
||||
}
|
||||
|
||||
w := &responseWriter{
|
||||
header: make(http.Header),
|
||||
}
|
||||
|
||||
gqlHandler.ServeHTTP(w, r)
|
||||
|
||||
if w.statusCode != http.StatusOK && w.statusCode != 0 {
|
||||
throw(vm, fmt.Sprintf("graphQL query failed: %d - %s. Query: %s. Variables: %v", w.statusCode, w.r.String(), in.Query, in.Variables))
|
||||
}
|
||||
|
||||
output := w.r.String()
|
||||
// convert to JSON
|
||||
var obj map[string]interface{}
|
||||
if err = json.Unmarshal([]byte(output), &obj); err != nil {
|
||||
throw(vm, fmt.Sprintf("could not unmarshal object %s: %s", output, err.Error()))
|
||||
}
|
||||
|
||||
retErr, hasErr := obj["errors"]
|
||||
|
||||
if hasErr {
|
||||
errOut, _ := json.Marshal(retErr)
|
||||
throw(vm, fmt.Sprintf("graphql error: %s", string(errOut)))
|
||||
}
|
||||
|
||||
v, err := vm.ToValue(obj["data"])
|
||||
if err != nil {
|
||||
throw(vm, fmt.Sprintf("could not create return value: %s", err.Error()))
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func AddGQLAPI(ctx context.Context, vm *otto.Otto, cookie *http.Cookie, gqlHandler http.Handler) error {
|
||||
gql, _ := vm.Object("({})")
|
||||
if err := gql.Set("Do", gqlRequestFunc(ctx, vm, cookie, gqlHandler)); err != nil {
|
||||
return fmt.Errorf("unable to set GraphQL Do function: %w", err)
|
||||
}
|
||||
|
||||
if err := vm.Set("gql", gql); err != nil {
|
||||
return fmt.Errorf("unable to set gql: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package js
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
const pluginPrefix = "[Plugin] "
|
||||
|
||||
func argToString(call otto.FunctionCall) string {
|
||||
arg := call.Argument(0)
|
||||
if arg.IsObject() {
|
||||
o, _ := arg.Export()
|
||||
data, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
logger.Warnf("Couldn't json encode object")
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
return arg.String()
|
||||
}
|
||||
|
||||
func logTrace(call otto.FunctionCall) otto.Value {
|
||||
logger.Trace(pluginPrefix + argToString(call))
|
||||
return otto.UndefinedValue()
|
||||
}
|
||||
|
||||
func logDebug(call otto.FunctionCall) otto.Value {
|
||||
logger.Debug(pluginPrefix + argToString(call))
|
||||
return otto.UndefinedValue()
|
||||
}
|
||||
|
||||
func logInfo(call otto.FunctionCall) otto.Value {
|
||||
logger.Info(pluginPrefix + argToString(call))
|
||||
return otto.UndefinedValue()
|
||||
}
|
||||
|
||||
func logWarn(call otto.FunctionCall) otto.Value {
|
||||
logger.Warn(pluginPrefix + argToString(call))
|
||||
return otto.UndefinedValue()
|
||||
}
|
||||
|
||||
func logError(call otto.FunctionCall) otto.Value {
|
||||
logger.Error(pluginPrefix + argToString(call))
|
||||
return otto.UndefinedValue()
|
||||
}
|
||||
|
||||
// Progress logs the current progress value. The progress value should be
|
||||
// between 0 and 1.0 inclusively, with 1 representing that the task is
|
||||
// complete. Values outside of this range will be clamp to be within it.
|
||||
func logProgressFunc(c chan float64) func(call otto.FunctionCall) otto.Value {
|
||||
return func(call otto.FunctionCall) otto.Value {
|
||||
arg := call.Argument(0)
|
||||
if !arg.IsNumber() {
|
||||
return otto.UndefinedValue()
|
||||
}
|
||||
|
||||
progress, _ := arg.ToFloat()
|
||||
progress = math.Min(math.Max(0, progress), 1)
|
||||
c <- progress
|
||||
|
||||
return otto.UndefinedValue()
|
||||
}
|
||||
}
|
||||
|
||||
func AddLogAPI(vm *otto.Otto, progress chan float64) error {
|
||||
log, _ := vm.Object("({})")
|
||||
if err := log.Set("Trace", logTrace); err != nil {
|
||||
return fmt.Errorf("error setting Trace: %w", err)
|
||||
}
|
||||
if err := log.Set("Debug", logDebug); err != nil {
|
||||
return fmt.Errorf("error setting Debug: %w", err)
|
||||
}
|
||||
if err := log.Set("Info", logInfo); err != nil {
|
||||
return fmt.Errorf("error setting Info: %w", err)
|
||||
}
|
||||
if err := log.Set("Warn", logWarn); err != nil {
|
||||
return fmt.Errorf("error setting Warn: %w", err)
|
||||
}
|
||||
if err := log.Set("Error", logError); err != nil {
|
||||
return fmt.Errorf("error setting Error: %w", err)
|
||||
}
|
||||
if err := log.Set("Progress", logProgressFunc(progress)); err != nil {
|
||||
return fmt.Errorf("error setting Progress: %v", err)
|
||||
}
|
||||
if err := vm.Set("log", log); err != nil {
|
||||
return fmt.Errorf("unable to set log: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package js
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
func sleepFunc(call otto.FunctionCall) otto.Value {
|
||||
arg := call.Argument(0)
|
||||
ms, _ := arg.ToInteger()
|
||||
|
||||
time.Sleep(time.Millisecond * time.Duration(ms))
|
||||
return otto.UndefinedValue()
|
||||
}
|
||||
|
||||
func AddUtilAPI(vm *otto.Otto) error {
|
||||
util, _ := vm.Object("({})")
|
||||
if err := util.Set("Sleep", sleepFunc); err != nil {
|
||||
return fmt.Errorf("unable to set sleep func: %w", err)
|
||||
}
|
||||
|
||||
if err := vm.Set("util", util); err != nil {
|
||||
return fmt.Errorf("unable to set util: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -91,7 +91,7 @@ type PluginSetting struct {
|
||||
type ServerConfig interface {
|
||||
GetHost() string
|
||||
GetPort() int
|
||||
GetConfigPath() string
|
||||
GetConfigPathAbs() string
|
||||
HasTLSConfig() bool
|
||||
GetPluginsPath() string
|
||||
GetDisabledPlugins() []string
|
||||
@@ -249,7 +249,7 @@ func (c Cache) makeServerConnection(ctx context.Context) common.StashServerConne
|
||||
Host: c.config.GetHost(),
|
||||
Port: c.config.GetPort(),
|
||||
SessionCookie: cookie,
|
||||
Dir: c.config.GetConfigPath(),
|
||||
Dir: c.config.GetConfigPathAbs(),
|
||||
}
|
||||
|
||||
if c.config.HasTLSConfig() {
|
||||
|
||||
@@ -35,7 +35,7 @@ func Resolve(configuredPythonPath string) (*Python, error) {
|
||||
case err == nil && !isFile:
|
||||
logger.Warnf("configured python path is not a file: %s", configuredPythonPath)
|
||||
case err != nil:
|
||||
logger.Warnf("unable to use configured python path: %s", err)
|
||||
logger.Warnf("unable to use configured python path: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func Resolve(configuredPythonPath string) (*Python, error) {
|
||||
if err != nil {
|
||||
python, err := exec.LookPath("python")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("python executable not in PATH: %s", err)
|
||||
return nil, fmt.Errorf("python executable not in PATH: %w", err)
|
||||
}
|
||||
ret := Python(python)
|
||||
return &ret, nil
|
||||
|
||||
@@ -32,19 +32,19 @@ func (g Generator) TranscodeVideo(ctx context.Context, input string, hash string
|
||||
}
|
||||
|
||||
// TranscodeAudio will copy the video stream as is, and transcode audio.
|
||||
func (g Generator) TranscodeAudio(ctx context.Context, input string, hash string, options TranscodeOptions) error {
|
||||
func (g Generator) TranscodeAudio(ctx context.Context, input string, hash string) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, input)
|
||||
defer lockCtx.Cancel()
|
||||
|
||||
return g.makeTranscode(lockCtx, hash, g.transcodeAudio(input, options))
|
||||
return g.makeTranscode(lockCtx, hash, g.transcodeAudio(input))
|
||||
}
|
||||
|
||||
// TranscodeCopyVideo will copy the video stream as is, and drop the audio stream.
|
||||
func (g Generator) TranscodeCopyVideo(ctx context.Context, input string, hash string, options TranscodeOptions) error {
|
||||
func (g Generator) TranscodeCopyVideo(ctx context.Context, input string, hash string) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, input)
|
||||
defer lockCtx.Cancel()
|
||||
|
||||
return g.makeTranscode(lockCtx, hash, g.transcodeCopyVideo(input, options))
|
||||
return g.makeTranscode(lockCtx, hash, g.transcodeCopyVideo(input))
|
||||
}
|
||||
|
||||
func (g Generator) makeTranscode(lockCtx *fsutil.LockContext, hash string, generateFn generateFn) error {
|
||||
@@ -129,19 +129,11 @@ func (g Generator) transcodeVideo(input string, options TranscodeOptions) genera
|
||||
}
|
||||
}
|
||||
|
||||
func (g Generator) transcodeAudio(input string, options TranscodeOptions) generateFn {
|
||||
func (g Generator) transcodeAudio(input string) generateFn {
|
||||
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||
var videoArgs ffmpeg.Args
|
||||
if options.Width != 0 && options.Height != 0 {
|
||||
var videoFilter ffmpeg.VideoFilter
|
||||
videoFilter = videoFilter.ScaleDimensions(options.Width, options.Height)
|
||||
videoArgs = videoArgs.VideoFilter(videoFilter)
|
||||
}
|
||||
|
||||
args := transcoder.Transcode(input, transcoder.TranscodeOptions{
|
||||
OutputPath: tmpFn,
|
||||
VideoCodec: ffmpeg.VideoCodecCopy,
|
||||
VideoArgs: videoArgs,
|
||||
AudioCodec: ffmpeg.AudioCodecAAC,
|
||||
})
|
||||
|
||||
@@ -149,14 +141,8 @@ func (g Generator) transcodeAudio(input string, options TranscodeOptions) genera
|
||||
}
|
||||
}
|
||||
|
||||
func (g Generator) transcodeCopyVideo(input string, options TranscodeOptions) generateFn {
|
||||
func (g Generator) transcodeCopyVideo(input string) generateFn {
|
||||
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||
var videoArgs ffmpeg.Args
|
||||
if options.Width != 0 && options.Height != 0 {
|
||||
var videoFilter ffmpeg.VideoFilter
|
||||
videoFilter = videoFilter.ScaleDimensions(options.Width, options.Height)
|
||||
videoArgs = videoArgs.VideoFilter(videoFilter)
|
||||
}
|
||||
|
||||
var audioArgs ffmpeg.Args
|
||||
audioArgs = audioArgs.SkipAudio()
|
||||
@@ -164,7 +150,6 @@ func (g Generator) transcodeCopyVideo(input string, options TranscodeOptions) ge
|
||||
args := transcoder.Transcode(input, transcoder.TranscodeOptions{
|
||||
OutputPath: tmpFn,
|
||||
VideoCodec: ffmpeg.VideoCodecCopy,
|
||||
VideoArgs: videoArgs,
|
||||
AudioArgs: audioArgs,
|
||||
})
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/stashapp/stash/pkg/javascript"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
@@ -528,19 +528,19 @@ func (p *postProcessLbToKg) Apply(ctx context.Context, value string, q mappedQue
|
||||
type postProcessJavascript string
|
||||
|
||||
func (p *postProcessJavascript) Apply(ctx context.Context, value string, q mappedQuery) string {
|
||||
vm := otto.New()
|
||||
vm := javascript.NewVM()
|
||||
if err := vm.Set("value", value); err != nil {
|
||||
logger.Warnf("javascript failed to set value: %v", err)
|
||||
return value
|
||||
}
|
||||
|
||||
script, err := vm.Compile("", "(function() { "+string(*p)+"})()")
|
||||
script, err := javascript.CompileScript("", "(function() { "+string(*p)+"})()")
|
||||
if err != nil {
|
||||
logger.Warnf("javascript failed to compile: %v", err)
|
||||
return value
|
||||
}
|
||||
|
||||
output, err := vm.Run(script)
|
||||
output, err := vm.RunProgram(script)
|
||||
if err != nil {
|
||||
logger.Warnf("javascript failed to run: %v", err)
|
||||
return value
|
||||
|
||||
@@ -1034,6 +1034,7 @@ type TagQueryInput struct {
|
||||
// Filter to search name - assumes like query unless quoted
|
||||
Name *string `json:"name,omitempty"`
|
||||
// Filter to category ID
|
||||
IsFavorite *bool `json:"is_favorite,omitempty"`
|
||||
CategoryID *string `json:"category_id,omitempty"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
|
||||
@@ -676,6 +676,9 @@ func performerFragmentToScrapedPerformer(p graphql.PerformerFragment) *models.Sc
|
||||
return !strings.EqualFold(s, p.Name)
|
||||
})
|
||||
|
||||
// #4596 - stash-box may return duplicate aliases. Filter these out
|
||||
p.Aliases = stringslice.UniqueFold(p.Aliases)
|
||||
|
||||
alias := strings.Join(p.Aliases, ", ")
|
||||
sp.Aliases = &alias
|
||||
}
|
||||
|
||||
@@ -29,3 +29,18 @@ func FromString(s string, sep string) []string {
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Unique returns a slice containing only unique values from the provided slice.
|
||||
// The comparison is case-insensitive.
|
||||
func UniqueFold(s []string) []string {
|
||||
seen := make(map[string]struct{})
|
||||
var ret []string
|
||||
for _, v := range s {
|
||||
if _, exists := seen[strings.ToLower(v)]; exists {
|
||||
continue
|
||||
}
|
||||
seen[strings.ToLower(v)] = struct{}{}
|
||||
ret = append(ret, v)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -10,9 +10,6 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
sqlite3mig "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
@@ -33,7 +30,7 @@ const (
|
||||
dbConnTimeout = 30
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 55
|
||||
var appSchemaVersion uint = 58
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
@@ -144,7 +141,7 @@ func (db *Database) Open(dbPath string) error {
|
||||
|
||||
if databaseSchemaVersion == 0 {
|
||||
// new database, just run the migrations
|
||||
if err := db.RunMigrations(); err != nil {
|
||||
if err := db.RunAllMigrations(); err != nil {
|
||||
return fmt.Errorf("error running initial schema migrations: %w", err)
|
||||
}
|
||||
} else {
|
||||
@@ -312,11 +309,6 @@ func (db *Database) RestoreFromBackup(backupPath string) error {
|
||||
return os.Rename(backupPath, db.dbPath)
|
||||
}
|
||||
|
||||
// Migrate the database
|
||||
func (db *Database) needsMigration() bool {
|
||||
return db.schemaVersion != appSchemaVersion
|
||||
}
|
||||
|
||||
func (db *Database) AppSchemaVersion() uint {
|
||||
return appSchemaVersion
|
||||
}
|
||||
@@ -349,100 +341,6 @@ func (db *Database) Version() uint {
|
||||
return db.schemaVersion
|
||||
}
|
||||
|
||||
func (db *Database) getMigrate() (*migrate.Migrate, error) {
|
||||
migrations, err := iofs.New(migrationsBox, "migrations")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
const disableForeignKeys = true
|
||||
conn, err := db.open(disableForeignKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
driver, err := sqlite3mig.WithInstance(conn.DB, &sqlite3mig.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use sqlite3Driver so that migration has access to durationToTinyInt
|
||||
return migrate.NewWithInstance(
|
||||
"iofs",
|
||||
migrations,
|
||||
db.dbPath,
|
||||
driver,
|
||||
)
|
||||
}
|
||||
|
||||
func (db *Database) getDatabaseSchemaVersion() (uint, error) {
|
||||
m, err := db.getMigrate()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
ret, _, _ := m.Version()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Migrate the database
|
||||
func (db *Database) RunMigrations() error {
|
||||
ctx := context.Background()
|
||||
|
||||
m, err := db.getMigrate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
databaseSchemaVersion, _, _ := m.Version()
|
||||
stepNumber := appSchemaVersion - databaseSchemaVersion
|
||||
if stepNumber != 0 {
|
||||
logger.Infof("Migrating database from version %d to %d", databaseSchemaVersion, appSchemaVersion)
|
||||
|
||||
// run each migration individually, and run custom migrations as needed
|
||||
var i uint = 1
|
||||
for ; i <= stepNumber; i++ {
|
||||
newVersion := databaseSchemaVersion + i
|
||||
|
||||
// run pre migrations as needed
|
||||
if err := db.runCustomMigrations(ctx, preMigrations[newVersion]); err != nil {
|
||||
return fmt.Errorf("running pre migrations for schema version %d: %w", newVersion, err)
|
||||
}
|
||||
|
||||
err = m.Steps(1)
|
||||
if err != nil {
|
||||
// migration failed
|
||||
return err
|
||||
}
|
||||
|
||||
// run post migrations as needed
|
||||
if err := db.runCustomMigrations(ctx, postMigrations[newVersion]); err != nil {
|
||||
return fmt.Errorf("running post migrations for schema version %d: %w", newVersion, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update the schema version
|
||||
db.schemaVersion, _, _ = m.Version()
|
||||
|
||||
// re-initialise the database
|
||||
const disableForeignKeys = false
|
||||
db.db, err = db.open(disableForeignKeys)
|
||||
if err != nil {
|
||||
return fmt.Errorf("re-initializing the database: %w", err)
|
||||
}
|
||||
|
||||
// optimize database after migration
|
||||
err = db.Optimise(ctx)
|
||||
if err != nil {
|
||||
logger.Warnf("error while performing post-migration optimisation: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) Optimise(ctx context.Context) error {
|
||||
logger.Info("Optimising database")
|
||||
|
||||
@@ -524,28 +422,3 @@ func (db *Database) QuerySQL(ctx context.Context, query string, args []interface
|
||||
|
||||
return cols, ret, nil
|
||||
}
|
||||
|
||||
func (db *Database) runCustomMigrations(ctx context.Context, fns []customMigrationFunc) error {
|
||||
for _, fn := range fns {
|
||||
if err := db.runCustomMigration(ctx, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) runCustomMigration(ctx context.Context, fn customMigrationFunc) error {
|
||||
const disableForeignKeys = false
|
||||
d, err := db.open(disableForeignKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer d.Close()
|
||||
if err := fn(ctx, d); 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