mirror of
https://github.com/stashapp/stash.git
synced 2025-12-14 04:13:56 +01:00
commit
bd5ee9ba6b
876 changed files with 106938 additions and 105659 deletions
6
.gitattributes
vendored
6
.gitattributes
vendored
|
|
@ -1,2 +1,6 @@
|
|||
go.mod text eol=lf
|
||||
go.sum text eol=lf
|
||||
go.sum text eol=lf
|
||||
*.go text eol=lf
|
||||
vendor/** -text
|
||||
ui/v2.5/**/*.ts* text eol=lf
|
||||
ui/v2.5/**/*.scss text eol=lf
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -23,6 +23,8 @@ A clear and concise description of what you expected to happen.
|
|||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem please ensure that your screenshots are SFW or at least appropriately censored.
|
||||
|
||||
**Stash Version: (from Settings -> About):**
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -21,6 +21,7 @@
|
|||
# GraphQL generated output
|
||||
pkg/models/generated_*.go
|
||||
ui/v2/src/core/generated-*.tsx
|
||||
ui/v2.5/src/core/generated-*.tsx
|
||||
|
||||
# packr generated files
|
||||
*-packr.go
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@
|
|||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/certs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ui/v1/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ui/v2/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ui/v2/node_modules" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ui/v2.5/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ui/v2.5/node_modules" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
</module>
|
||||
|
|
|
|||
17
.travis.yml
17
.travis.yml
|
|
@ -1,4 +1,8 @@
|
|||
if: tag != latest_develop # dont build for the latest_develop tagged version
|
||||
|
||||
dist: xenial
|
||||
git:
|
||||
depth: false
|
||||
language: go
|
||||
go:
|
||||
- 1.11.x
|
||||
|
|
@ -9,14 +13,15 @@ env:
|
|||
- GO111MODULE=on
|
||||
before_install:
|
||||
- echo -e "machine github.com\n login $CI_USER_TOKEN" > ~/.netrc
|
||||
- travis_retry yarn --cwd ui/v2 install --frozen-lockfile
|
||||
- nvm install 12
|
||||
- travis_retry yarn --cwd ui/v2.5 install --frozen-lockfile
|
||||
- make generate
|
||||
- CI=false yarn --cwd ui/v2 build # TODO: Fix warnings
|
||||
- CI=false yarn --cwd ui/v2.5 build-ci
|
||||
#- go get -v github.com/mgechev/revive
|
||||
script:
|
||||
# left lint off to avoid getting extra dependency
|
||||
#- make lint
|
||||
#- make vet
|
||||
- make it
|
||||
- make fmt-check vet it
|
||||
after_success:
|
||||
- docker pull stashapp/compiler:develop
|
||||
- sh ./scripts/cross-compile.sh
|
||||
|
|
@ -31,6 +36,8 @@ before_deploy:
|
|||
deploy:
|
||||
# latest develop release
|
||||
- provider: releases
|
||||
# use the v2 release provider for proper release note setting
|
||||
edge: true
|
||||
api_key:
|
||||
secure: tGJ2q62CfPdayid2qEtW2aGRhMgCl3lBXYYQqp3eH0vFgIIf6cs7IDX7YC/x3XKMEQ/iMLZmtCXZvSTqNrD6Sk7MSnt30GIs+4uxIZDnnd8mV5X3K4n4gjD+NAORc4DrQBvUGrYMKJsR5gtkH0nu6diWb1o1If7OiJEuCPRhrmQYcza7NUdABnA9Z2wn2RNUV9Ga33WUCqLMEU5GtNBlfQPiP/khCQrqn/ocR6wUjYut3J6YagzqH4wsfJi3glHyWtowcNIw1LZi5zFxHD/bRBT4Tln7yypkjWNq9eQILA6i6kRUGf7ggyTx26/k8n4tnu+QD0vVh4EcjlThpU/LGyUXzKrrxjRwaDZnM0oYxg5AfHcBuAiAdo0eWnV3lEWRfTJMIVb9MPf4qDmzR4RREfB5OXOxwq3ODeCcJE8sTIMD/wBPZrlqS/QrRpND2gn2X4snkVukN9t9F4CMTFMtVSzFV7TDJW5E5Lq6VEExulteQhs6kcK9NRPNAaLgRQAw7X9kVWfDtiGUP+fE2i8F9Bo8bm7sOT5O5VPMPykx3EgeNg1IqIgMTCsMlhMJT4xBJoQUgmd2wWyf3Ryw+P+sFgdb5Sd7+lFgJBjMUUoOxMxAOiEgdFvCXcr+/Udyz2RdtetU1/6VzXzLPcKOw0wubZeBkISqu7o9gpfdMP9Eq00=
|
||||
file:
|
||||
|
|
@ -41,7 +48,7 @@ deploy:
|
|||
skip_cleanup: true
|
||||
overwrite: true
|
||||
name: "${STASH_VERSION}: Latest development build"
|
||||
body: ${RELEASE_DATE}\n This is always the latest committed version on the develop branch. Use as your own risk!
|
||||
release_notes: "**${RELEASE_DATE}**\n This is always the latest committed version on the develop branch. Use as your own risk!"
|
||||
prerelease: true
|
||||
on:
|
||||
repo: stashapp/stash
|
||||
|
|
|
|||
33
Makefile
33
Makefile
|
|
@ -21,13 +21,18 @@ clean:
|
|||
.PHONY: generate
|
||||
generate:
|
||||
go generate -mod=vendor
|
||||
cd ui/v2 && yarn run gqlgen
|
||||
cd ui/v2.5 && yarn run gqlgen
|
||||
|
||||
# Runs gofmt -w on the project's source code, modifying any files that do not match its style.
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
# Ensures that changed files have had gofmt run on them
|
||||
.PHONY: fmt-check
|
||||
fmt-check:
|
||||
sh ./scripts/check-gofmt.sh
|
||||
|
||||
# Runs go vet on the project's source code.
|
||||
.PHONY: vet
|
||||
vet:
|
||||
|
|
@ -47,7 +52,31 @@ test:
|
|||
it:
|
||||
go test -mod=vendor -tags=integration ./...
|
||||
|
||||
# installs UI dependencies. Run when first cloning repository, or if UI
|
||||
# dependencies have changed
|
||||
.PHONY: pre-ui
|
||||
pre-ui:
|
||||
cd ui/v2.5 && yarn install --frozen-lockfile
|
||||
|
||||
.PHONY: ui
|
||||
ui:
|
||||
cd ui/v2 && yarn build
|
||||
cd ui/v2.5 && yarn build
|
||||
packr2
|
||||
|
||||
fmt-ui:
|
||||
cd ui/v2.5 && yarn format
|
||||
|
||||
# runs tests and checks on the UI and builds it
|
||||
.PHONY: ui-validate
|
||||
ui-validate:
|
||||
cd ui/v2.5 && yarn run validate
|
||||
|
||||
# just repacks the packr files - use when updating migrations and packed files without
|
||||
# rebuilding the UI
|
||||
.PHONY: packr
|
||||
packr:
|
||||
packr2
|
||||
|
||||
# runs all of the tests and checks required for a PR to be accepted
|
||||
.PHONY: validate
|
||||
validate: ui-validate fmt-check vet lint it
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -69,7 +69,7 @@ Join the [Discord server](https://discord.gg/2TsNFKt).
|
|||
* Go Install: `go get github.com/gobuffalo/packr/v2/packr2@v2.0.2`
|
||||
* [Binary Download](https://github.com/gobuffalo/packr/releases)
|
||||
* [Yarn](https://yarnpkg.com/en/docs/install) - Yarn package manager
|
||||
* Run `yarn install --frozen-lockfile` in the `stash/ui/v2` folder (before running make generate for first time).
|
||||
* Run `yarn install --frozen-lockfile` in the `stash/ui/v2.5` folder (before running make generate for first time).
|
||||
|
||||
NOTE: You may need to run the `go get` commands outside the project directory to avoid modifying the projects module file.
|
||||
|
||||
|
|
@ -92,11 +92,18 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
|
|||
|
||||
## Commands
|
||||
|
||||
* `make generate` - Generate Go GraphQL and packr2 files
|
||||
* `make generate` - Generate Go and UI GraphQL files
|
||||
* `make build` - Builds the binary (make sure to build the UI as well... see below)
|
||||
* `make ui` - Builds the frontend
|
||||
* `make pre-ui` - Installs the UI dependencies. Only needs to be run once before building the UI for the first time, or if the dependencies are updated
|
||||
* `make fmt-ui` - Formats the UI source code.
|
||||
* `make ui` - Builds the frontend and the packr2 files
|
||||
* `make packr` - Generate packr2 files (sub-target of `ui`. Use to regenerate packr2 files without rebuilding UI)
|
||||
* `make vet` - Run `go vet`
|
||||
* `make lint` - Run the linter
|
||||
* `make fmt` - Run `go fmt`
|
||||
* `make fmt-check` - Ensure changed files are formatted correctly
|
||||
* `make it` - Run the unit and integration tests
|
||||
* `make validate` - Run all of the tests and checks required to submit a PR
|
||||
|
||||
## Building a release
|
||||
|
||||
|
|
@ -111,3 +118,10 @@ where the app can be cross-compiled. This process is kicked off by CI via the `
|
|||
command to open a bash shell to the container to poke around:
|
||||
|
||||
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashappdev/compiler:latest /bin/bash`
|
||||
|
||||
## Customization
|
||||
|
||||
You can make Stash interface fit your desired style with [Custom CSS snippets](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets) and [CSS Tweaks](https://github.com/stashapp/stash/wiki/CSS-Tweaks).
|
||||
|
||||
[Stash Plex Theme](https://github.com/stashapp/stash/wiki/Stash-Plex-Theme) is a community created theme inspired by popular Plex Interface.
|
||||
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ RUN wget -O /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-rele
|
|||
mv /ffmpeg*/ /ffmpeg/
|
||||
|
||||
# copy the ui yarn stuff so that it doesn't get rebuilt every time
|
||||
COPY ./ui/v2/package.json ./ui/v2/yarn.lock /stash/ui/v2/
|
||||
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
|
||||
|
||||
WORKDIR /stash
|
||||
RUN yarn --cwd ui/v2 install --frozen-lockfile
|
||||
RUN yarn --cwd ui/v2.5 install --frozen-lockfile
|
||||
|
||||
COPY . /stash/
|
||||
ENV GO111MODULE=on
|
||||
|
|
|
|||
17
go.mod
17
go.mod
|
|
@ -2,29 +2,30 @@ module github.com/stashapp/stash
|
|||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.9.0
|
||||
github.com/PuerkitoBio/goquery v1.5.0
|
||||
github.com/antchfx/htmlquery v1.2.0
|
||||
github.com/antchfx/xpath v1.1.2 // indirect
|
||||
github.com/bmatcuk/doublestar v1.1.5
|
||||
github.com/antchfx/htmlquery v1.2.3
|
||||
github.com/bmatcuk/doublestar v1.3.1
|
||||
github.com/disintegration/imaging v1.6.0
|
||||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/gobuffalo/packr/v2 v2.0.2
|
||||
github.com/golang-migrate/migrate/v4 v4.3.1
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
github.com/h2non/filetype v1.0.8
|
||||
// this is required for generate
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/mattn/go-sqlite3 v1.10.0
|
||||
github.com/json-iterator/go v1.1.9
|
||||
github.com/mattn/go-sqlite3 v1.13.0
|
||||
github.com/rs/cors v1.6.0
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/spf13/viper v1.4.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/vektah/gqlparser v1.1.2
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
|
||||
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2 // indirect
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092
|
||||
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2
|
||||
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
)
|
||||
|
||||
|
|
|
|||
40
go.sum
40
go.sum
|
|
@ -16,8 +16,6 @@ github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0
|
|||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
|
||||
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
|
||||
|
|
@ -27,13 +25,11 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
|||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/antchfx/htmlquery v1.2.0 h1:oKShnsGlnOHX6t4uj5OHgLKkABcJoqnXpqnscoi9Lpw=
|
||||
github.com/antchfx/htmlquery v1.2.0/go.mod h1:MS9yksVSQXls00iXkiMqXr0J+umL/AmxXKuP28SUJM8=
|
||||
github.com/antchfx/xpath v1.1.2 h1:YziPrtM0gEJBnhdUGxYcIVYXZ8FXbtbovxOi+UW/yWQ=
|
||||
github.com/antchfx/xpath v1.1.2/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/antchfx/htmlquery v1.2.3 h1:sP3NFDneHx2stfNXCKbhHFo8XgNjCACnU/4AO5gWz6M=
|
||||
github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0=
|
||||
github.com/antchfx/xpath v1.1.6 h1:6sVh6hB5T6phw1pFpHRQ+C4bd8sNI+O58flqtg7h0R0=
|
||||
github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
|
|
@ -41,8 +37,8 @@ github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
|
|||
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/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
|
||||
github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk=
|
||||
github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY=
|
||||
github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
|
|
@ -81,6 +77,7 @@ github.com/docker/docker v0.7.3-0.20190108045446-77df18c24acf/go.mod h1:eEKB0N0r
|
|||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
|
|
@ -325,6 +322,8 @@ github.com/golang-migrate/migrate/v4 v4.3.1/go.mod h1:mJ89KBgbXmM3P49BqOxRL3riNF
|
|||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
|
|
@ -340,6 +339,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
|
|||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
|
|
@ -352,9 +352,12 @@ github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
|||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.2/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
|
||||
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
|
|
@ -383,6 +386,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
|
|||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||
github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o=
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
|
|
@ -391,6 +396,8 @@ github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx
|
|||
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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
|
|
@ -445,6 +452,8 @@ github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/
|
|||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c=
|
||||
github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
|
|
@ -453,6 +462,10 @@ github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:F
|
|||
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mongodb/mongo-go-driver v0.3.0/go.mod h1:NK/HWDIIZkaYsnYa0hmtP443T5ELr0KDecmIioVuuyU=
|
||||
github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
|
|
@ -574,6 +587,8 @@ github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
|
|
@ -633,7 +648,6 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
|
|||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
@ -660,6 +674,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd h1:QPwSajcTUrFriMF1nJ3XzgoqakqQEsnZf9LdXdi2nkI=
|
||||
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
|
@ -703,6 +719,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190426135247-a129542de9ae h1:mQLHiymj/JXKnnjc62tb7nD5pZLs940/sXJu+Xp3DBA=
|
||||
golang.org/x/sys v0.0.0-20190426135247-a129542de9ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
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=
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ models:
|
|||
model: github.com/stashapp/stash/pkg/models.ScrapedItem
|
||||
Studio:
|
||||
model: github.com/stashapp/stash/pkg/models.Studio
|
||||
Movie:
|
||||
model: github.com/stashapp/stash/pkg/models.Movie
|
||||
Tag:
|
||||
model: github.com/stashapp/stash/pkg/models.Tag
|
||||
ScrapedPerformer:
|
||||
|
|
@ -36,6 +38,8 @@ models:
|
|||
model: github.com/stashapp/stash/pkg/models.ScrapedScenePerformer
|
||||
ScrapedSceneStudio:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedSceneStudio
|
||||
ScrapedSceneMovie:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedSceneMovie
|
||||
ScrapedSceneTag:
|
||||
model: github.com/stashapp/stash/pkg/models.ScrapedSceneTag
|
||||
SceneFileType:
|
||||
|
|
|
|||
|
|
@ -2,25 +2,32 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
|||
stashes
|
||||
databasePath
|
||||
generatedPath
|
||||
cachePath
|
||||
maxTranscodeSize
|
||||
maxStreamingTranscodeSize
|
||||
forceMkv
|
||||
forceHevc
|
||||
username
|
||||
password
|
||||
maxSessionAge
|
||||
logFile
|
||||
logOut
|
||||
logLevel
|
||||
logAccess
|
||||
excludes
|
||||
scraperUserAgent
|
||||
}
|
||||
|
||||
fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
soundOnPreview
|
||||
wallShowTitle
|
||||
wallPlayback
|
||||
maximumLoopDuration
|
||||
autostartVideo
|
||||
showStudioAsText
|
||||
css
|
||||
cssEnabled
|
||||
language
|
||||
}
|
||||
|
||||
fragment ConfigData on ConfigResult {
|
||||
|
|
|
|||
5
graphql/documents/data/movie-slim.graphql
Normal file
5
graphql/documents/data/movie-slim.graphql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
fragment SlimMovieData on Movie {
|
||||
id
|
||||
name
|
||||
front_image_path
|
||||
}
|
||||
20
graphql/documents/data/movie.graphql
Normal file
20
graphql/documents/data/movie.graphql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
fragment MovieData on Movie {
|
||||
id
|
||||
checksum
|
||||
name
|
||||
aliases
|
||||
duration
|
||||
date
|
||||
rating
|
||||
director
|
||||
|
||||
studio {
|
||||
...StudioData
|
||||
}
|
||||
|
||||
synopsis
|
||||
url
|
||||
front_image_path
|
||||
back_image_path
|
||||
scene_count
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
fragment SlimPerformerData on Performer {
|
||||
id
|
||||
name
|
||||
gender
|
||||
image_path
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ fragment PerformerData on Performer {
|
|||
checksum
|
||||
name
|
||||
url
|
||||
gender
|
||||
twitter
|
||||
instagram
|
||||
birthdate
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ fragment SlimSceneData on Scene {
|
|||
image_path
|
||||
}
|
||||
|
||||
movies {
|
||||
movie {
|
||||
id
|
||||
name
|
||||
front_image_path
|
||||
}
|
||||
scene_index
|
||||
}
|
||||
|
||||
tags {
|
||||
id
|
||||
name
|
||||
|
|
|
|||
|
|
@ -42,6 +42,13 @@ fragment SceneData on Scene {
|
|||
studio {
|
||||
...StudioData
|
||||
}
|
||||
|
||||
movies {
|
||||
movie {
|
||||
...MovieData
|
||||
}
|
||||
scene_index
|
||||
}
|
||||
|
||||
tags {
|
||||
...TagData
|
||||
|
|
|
|||
|
|
@ -1,5 +1,27 @@
|
|||
fragment ScrapedPerformerData on ScrapedPerformer {
|
||||
name
|
||||
gender
|
||||
url
|
||||
twitter
|
||||
instagram
|
||||
birthdate
|
||||
ethnicity
|
||||
country
|
||||
eye_color
|
||||
height
|
||||
measurements
|
||||
fake_tits
|
||||
career_length
|
||||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
image
|
||||
}
|
||||
|
||||
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||
id
|
||||
name
|
||||
gender
|
||||
url
|
||||
twitter
|
||||
instagram
|
||||
|
|
@ -16,23 +38,27 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
|||
aliases
|
||||
}
|
||||
|
||||
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||
fragment ScrapedMovieData on ScrapedMovie {
|
||||
name
|
||||
aliases
|
||||
duration
|
||||
date
|
||||
rating
|
||||
director
|
||||
url
|
||||
synopsis
|
||||
}
|
||||
|
||||
fragment ScrapedSceneMovieData on ScrapedSceneMovie {
|
||||
id
|
||||
name
|
||||
url
|
||||
twitter
|
||||
instagram
|
||||
birthdate
|
||||
ethnicity
|
||||
country
|
||||
eye_color
|
||||
height
|
||||
measurements
|
||||
fake_tits
|
||||
career_length
|
||||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
duration
|
||||
date
|
||||
rating
|
||||
director
|
||||
url
|
||||
synopsis
|
||||
}
|
||||
|
||||
fragment ScrapedSceneStudioData on ScrapedSceneStudio {
|
||||
|
|
@ -51,6 +77,7 @@ fragment ScrapedSceneData on ScrapedScene {
|
|||
details
|
||||
url
|
||||
date
|
||||
image
|
||||
|
||||
file {
|
||||
size
|
||||
|
|
@ -74,4 +101,8 @@ fragment ScrapedSceneData on ScrapedScene {
|
|||
performers {
|
||||
...ScrapedScenePerformerData
|
||||
}
|
||||
|
||||
movies {
|
||||
...ScrapedSceneMovieData
|
||||
}
|
||||
}
|
||||
27
graphql/documents/mutations/metadata.graphql
Normal file
27
graphql/documents/mutations/metadata.graphql
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
mutation MetadataImport {
|
||||
metadataImport
|
||||
}
|
||||
|
||||
mutation MetadataExport {
|
||||
metadataExport
|
||||
}
|
||||
|
||||
mutation MetadataScan($input: ScanMetadataInput!) {
|
||||
metadataScan(input: $input)
|
||||
}
|
||||
|
||||
mutation MetadataGenerate($input: GenerateMetadataInput!) {
|
||||
metadataGenerate(input: $input)
|
||||
}
|
||||
|
||||
mutation MetadataAutoTag($input: AutoTagMetadataInput!) {
|
||||
metadataAutoTag(input: $input)
|
||||
}
|
||||
|
||||
mutation MetadataClean {
|
||||
metadataClean
|
||||
}
|
||||
|
||||
mutation StopJob {
|
||||
stopJob
|
||||
}
|
||||
40
graphql/documents/mutations/movie.graphql
Normal file
40
graphql/documents/mutations/movie.graphql
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
mutation MovieCreate(
|
||||
$name: String!,
|
||||
$aliases: String,
|
||||
$duration: Int,
|
||||
$date: String,
|
||||
$rating: Int,
|
||||
$studio_id: ID,
|
||||
$director: String,
|
||||
$synopsis: String,
|
||||
$url: String,
|
||||
$front_image: String,
|
||||
$back_image: String) {
|
||||
|
||||
movieCreate(input: { name: $name, aliases: $aliases, duration: $duration, date: $date, rating: $rating, studio_id: $studio_id, director: $director, synopsis: $synopsis, url: $url, front_image: $front_image, back_image: $back_image }) {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
|
||||
mutation MovieUpdate(
|
||||
$id: ID!
|
||||
$name: String,
|
||||
$aliases: String,
|
||||
$duration: Int,
|
||||
$date: String,
|
||||
$rating: Int,
|
||||
$studio_id: ID,
|
||||
$director: String,
|
||||
$synopsis: String,
|
||||
$url: String,
|
||||
$front_image: String,
|
||||
$back_image: String) {
|
||||
|
||||
movieUpdate(input: { id: $id, name: $name, aliases: $aliases, duration: $duration, date: $date, rating: $rating, studio_id: $studio_id, director: $director, synopsis: $synopsis, url: $url, front_image: $front_image, back_image: $back_image }) {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
|
||||
mutation MovieDestroy($id: ID!) {
|
||||
movieDestroy(input: { id: $id })
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
mutation PerformerCreate(
|
||||
$name: String,
|
||||
$url: String,
|
||||
$gender: GenderEnum,
|
||||
$birthdate: String,
|
||||
$ethnicity: String,
|
||||
$country: String,
|
||||
|
|
@ -20,6 +21,7 @@ mutation PerformerCreate(
|
|||
performerCreate(input: {
|
||||
name: $name,
|
||||
url: $url,
|
||||
gender: $gender,
|
||||
birthdate: $birthdate,
|
||||
ethnicity: $ethnicity,
|
||||
country: $country,
|
||||
|
|
@ -44,6 +46,7 @@ mutation PerformerUpdate(
|
|||
$id: ID!,
|
||||
$name: String,
|
||||
$url: String,
|
||||
$gender: GenderEnum,
|
||||
$birthdate: String,
|
||||
$ethnicity: String,
|
||||
$country: String,
|
||||
|
|
@ -64,6 +67,7 @@ mutation PerformerUpdate(
|
|||
id: $id,
|
||||
name: $name,
|
||||
url: $url,
|
||||
gender: $gender,
|
||||
birthdate: $birthdate,
|
||||
ethnicity: $ethnicity,
|
||||
country: $country,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ mutation SceneUpdate(
|
|||
$studio_id: ID,
|
||||
$gallery_id: ID,
|
||||
$performer_ids: [ID!] = [],
|
||||
$movies: [SceneMovieInput!] = [],
|
||||
$tag_ids: [ID!] = [],
|
||||
$cover_image: String) {
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ mutation SceneUpdate(
|
|||
studio_id: $studio_id,
|
||||
gallery_id: $gallery_id,
|
||||
performer_ids: $performer_ids,
|
||||
movies: $movies,
|
||||
tag_ids: $tag_ids,
|
||||
cover_image: $cover_image
|
||||
}) {
|
||||
|
|
@ -37,8 +39,8 @@ mutation BulkSceneUpdate(
|
|||
$rating: Int,
|
||||
$studio_id: ID,
|
||||
$gallery_id: ID,
|
||||
$performer_ids: [ID!],
|
||||
$tag_ids: [ID!]) {
|
||||
$performer_ids: BulkUpdateIds,
|
||||
$tag_ids: BulkUpdateIds) {
|
||||
|
||||
bulkSceneUpdate(input: {
|
||||
ids: $ids,
|
||||
|
|
@ -76,4 +78,8 @@ mutation SceneResetO($id: ID!) {
|
|||
|
||||
mutation SceneDestroy($id: ID!, $delete_file: Boolean, $delete_generated : Boolean) {
|
||||
sceneDestroy(input: {id: $id, delete_file: $delete_file, delete_generated: $delete_generated})
|
||||
}
|
||||
}
|
||||
|
||||
mutation SceneGenerateScreenshot($id: ID!, $at: Float) {
|
||||
sceneGenerateScreenshot(id: $id, at: $at)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,19 +19,24 @@ query AllTags {
|
|||
}
|
||||
|
||||
query AllPerformersForFilter {
|
||||
allPerformers {
|
||||
allPerformersSlim {
|
||||
...SlimPerformerData
|
||||
}
|
||||
}
|
||||
|
||||
query AllStudiosForFilter {
|
||||
allStudios {
|
||||
allStudiosSlim {
|
||||
...SlimStudioData
|
||||
}
|
||||
}
|
||||
query AllMoviesForFilter {
|
||||
allMoviesSlim {
|
||||
...SlimMovieData
|
||||
}
|
||||
}
|
||||
|
||||
query AllTagsForFilter {
|
||||
allTags {
|
||||
allTagsSlim {
|
||||
id
|
||||
name
|
||||
}
|
||||
|
|
@ -47,9 +52,11 @@ query ValidGalleriesForScene($scene_id: ID!) {
|
|||
query Stats {
|
||||
stats {
|
||||
scene_count,
|
||||
scene_size_count,
|
||||
gallery_count,
|
||||
performer_count,
|
||||
studio_count,
|
||||
movie_count,
|
||||
tag_count
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
graphql/documents/queries/movie.graphql
Normal file
14
graphql/documents/queries/movie.graphql
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
query FindMovies($filter: FindFilterType, $movie_filter: MovieFilterType) {
|
||||
findMovies(filter: $filter, movie_filter: $movie_filter) {
|
||||
count
|
||||
movies {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query FindMovie($id: ID!) {
|
||||
findMovie(id: $id) {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,9 @@ query ParseSceneFilenames($filter: FindFilterType!, $config: SceneParserInput!)
|
|||
rating
|
||||
studio_id
|
||||
gallery_id
|
||||
movies {
|
||||
movie_id
|
||||
}
|
||||
performer_ids
|
||||
tag_ids
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ query Configuration {
|
|||
}
|
||||
}
|
||||
|
||||
query Directories($path: String) {
|
||||
directories(path: $path)
|
||||
}
|
||||
query Directory($path: String) {
|
||||
directory(path: $path) {
|
||||
path
|
||||
parent
|
||||
directories
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,3 @@
|
|||
query MetadataImport {
|
||||
metadataImport
|
||||
}
|
||||
|
||||
query MetadataExport {
|
||||
metadataExport
|
||||
}
|
||||
|
||||
query MetadataScan($input: ScanMetadataInput!) {
|
||||
metadataScan(input: $input)
|
||||
}
|
||||
|
||||
query MetadataGenerate($input: GenerateMetadataInput!) {
|
||||
metadataGenerate(input: $input)
|
||||
}
|
||||
|
||||
query MetadataAutoTag($input: AutoTagMetadataInput!) {
|
||||
metadataAutoTag(input: $input)
|
||||
}
|
||||
|
||||
query MetadataClean {
|
||||
metadataClean
|
||||
}
|
||||
|
||||
query JobStatus {
|
||||
jobStatus {
|
||||
progress
|
||||
|
|
@ -29,7 +5,3 @@ query JobStatus {
|
|||
message
|
||||
}
|
||||
}
|
||||
|
||||
query StopJob {
|
||||
stopJob
|
||||
}
|
||||
|
|
@ -22,6 +22,11 @@ type Query {
|
|||
"""A function which queries Studio objects"""
|
||||
findStudios(filter: FindFilterType): FindStudiosResultType!
|
||||
|
||||
"""Find a movie by ID"""
|
||||
findMovie(id: ID!): Movie
|
||||
"""A function which queries Movie objects"""
|
||||
findMovies(movie_filter: MovieFilterType, filter: FindFilterType): FindMoviesResultType!
|
||||
|
||||
findGallery(id: ID!): Gallery
|
||||
findGalleries(filter: FindFilterType): FindGalleriesResultType!
|
||||
|
||||
|
|
@ -68,32 +73,26 @@ type Query {
|
|||
"""Returns the current, complete configuration"""
|
||||
configuration: ConfigResult!
|
||||
"""Returns an array of paths for the given path"""
|
||||
directories(path: String): [String!]!
|
||||
directory(path: String): Directory!
|
||||
|
||||
# Metadata
|
||||
|
||||
"""Start an import. Returns the job ID"""
|
||||
metadataImport: String!
|
||||
"""Start an export. Returns the job ID"""
|
||||
metadataExport: String!
|
||||
"""Start a scan. Returns the job ID"""
|
||||
metadataScan(input: ScanMetadataInput!): String!
|
||||
"""Start generating content. Returns the job ID"""
|
||||
metadataGenerate(input: GenerateMetadataInput!): String!
|
||||
"""Start auto-tagging. Returns the job ID"""
|
||||
metadataAutoTag(input: AutoTagMetadataInput!): String!
|
||||
"""Clean metadata. Returns the job ID"""
|
||||
metadataClean: String!
|
||||
|
||||
jobStatus: MetadataUpdateStatus!
|
||||
stopJob: Boolean!
|
||||
|
||||
# Get everything
|
||||
|
||||
allPerformers: [Performer!]!
|
||||
allStudios: [Studio!]!
|
||||
allMovies: [Movie!]!
|
||||
allTags: [Tag!]!
|
||||
|
||||
# Get everything with minimal metadata
|
||||
|
||||
allPerformersSlim: [Performer!]!
|
||||
allStudiosSlim: [Studio!]!
|
||||
allMoviesSlim: [Movie!]!
|
||||
allTagsSlim: [Tag!]!
|
||||
|
||||
# Version
|
||||
version: Version!
|
||||
|
||||
|
|
@ -114,6 +113,9 @@ type Mutation {
|
|||
"""Resets the o-counter for a scene to 0. Returns the new value"""
|
||||
sceneResetO(id: ID!): Int!
|
||||
|
||||
"""Generates screenshot at specified time in seconds. Leave empty to generate default screenshot"""
|
||||
sceneGenerateScreenshot(id: ID!, at: Float): String!
|
||||
|
||||
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
|
||||
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
|
||||
sceneMarkerDestroy(id: ID!): Boolean!
|
||||
|
|
@ -126,6 +128,10 @@ type Mutation {
|
|||
studioUpdate(input: StudioUpdateInput!): Studio
|
||||
studioDestroy(input: StudioDestroyInput!): Boolean!
|
||||
|
||||
movieCreate(input: MovieCreateInput!): Movie
|
||||
movieUpdate(input: MovieUpdateInput!): Movie
|
||||
movieDestroy(input: MovieDestroyInput!): Boolean!
|
||||
|
||||
tagCreate(input: TagCreateInput!): Tag
|
||||
tagUpdate(input: TagUpdateInput!): Tag
|
||||
tagDestroy(input: TagDestroyInput!): Boolean!
|
||||
|
|
@ -133,6 +139,21 @@ type Mutation {
|
|||
"""Change general configuration options"""
|
||||
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
|
||||
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
|
||||
|
||||
"""Start an import. Returns the job ID"""
|
||||
metadataImport: String!
|
||||
"""Start an export. Returns the job ID"""
|
||||
metadataExport: String!
|
||||
"""Start a scan. Returns the job ID"""
|
||||
metadataScan(input: ScanMetadataInput!): String!
|
||||
"""Start generating content. Returns the job ID"""
|
||||
metadataGenerate(input: GenerateMetadataInput!): String!
|
||||
"""Start auto-tagging. Returns the job ID"""
|
||||
metadataAutoTag(input: AutoTagMetadataInput!): String!
|
||||
"""Clean metadata. Returns the job ID"""
|
||||
metadataClean: String!
|
||||
|
||||
stopJob: Boolean!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
|
|
|
|||
|
|
@ -14,14 +14,22 @@ input ConfigGeneralInput {
|
|||
databasePath: String
|
||||
"""Path to generated files"""
|
||||
generatedPath: String
|
||||
"""Path to cache"""
|
||||
cachePath: String
|
||||
"""Max generated transcode size"""
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"""Max streaming transcode size"""
|
||||
maxStreamingTranscodeSize: StreamingResolutionEnum
|
||||
"""Force MKV as supported format"""
|
||||
forceMkv: Boolean!
|
||||
"""Force HEVC as a supported codec"""
|
||||
forceHevc: Boolean!
|
||||
"""Username"""
|
||||
username: String
|
||||
"""Password"""
|
||||
password: String
|
||||
"""Maximum session cookie age"""
|
||||
maxSessionAge: Int
|
||||
"""Name of the log file"""
|
||||
logFile: String
|
||||
"""Whether to also output to stderr"""
|
||||
|
|
@ -32,6 +40,8 @@ input ConfigGeneralInput {
|
|||
logAccess: Boolean!
|
||||
"""Array of file regexp to exclude from Scan"""
|
||||
excludes: [String!]
|
||||
"""Scraper user agent string"""
|
||||
scraperUserAgent: String
|
||||
}
|
||||
|
||||
type ConfigGeneralResult {
|
||||
|
|
@ -41,14 +51,22 @@ type ConfigGeneralResult {
|
|||
databasePath: String!
|
||||
"""Path to generated files"""
|
||||
generatedPath: String!
|
||||
"""Max generated transcode size"""
|
||||
"""Path to cache"""
|
||||
cachePath: String!
|
||||
"""Max generated transcode size"""
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"""Max streaming transcode size"""
|
||||
maxStreamingTranscodeSize: StreamingResolutionEnum
|
||||
"""Force MKV as supported format"""
|
||||
forceMkv: Boolean!
|
||||
"""Force HEVC as a supported codec"""
|
||||
forceHevc: Boolean!
|
||||
"""Username"""
|
||||
username: String!
|
||||
"""Password"""
|
||||
password: String!
|
||||
"""Maximum session cookie age"""
|
||||
maxSessionAge: Int!
|
||||
"""Name of the log file"""
|
||||
logFile: String
|
||||
"""Whether to also output to stderr"""
|
||||
|
|
@ -59,6 +77,8 @@ type ConfigGeneralResult {
|
|||
logAccess: Boolean!
|
||||
"""Array of file regexp to exclude from Scan"""
|
||||
excludes: [String!]!
|
||||
"""Scraper user agent string"""
|
||||
scraperUserAgent: String
|
||||
}
|
||||
|
||||
input ConfigInterfaceInput {
|
||||
|
|
@ -66,6 +86,8 @@ input ConfigInterfaceInput {
|
|||
soundOnPreview: Boolean
|
||||
"""Show title and tags in wall view"""
|
||||
wallShowTitle: Boolean
|
||||
"""Wall playback type"""
|
||||
wallPlayback: String
|
||||
"""Maximum duration (in seconds) in which a scene video will loop in the scene player"""
|
||||
maximumLoopDuration: Int
|
||||
"""If true, video will autostart on load in the scene player"""
|
||||
|
|
@ -75,6 +97,7 @@ input ConfigInterfaceInput {
|
|||
"""Custom CSS"""
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
language: String
|
||||
}
|
||||
|
||||
type ConfigInterfaceResult {
|
||||
|
|
@ -82,6 +105,8 @@ type ConfigInterfaceResult {
|
|||
soundOnPreview: Boolean
|
||||
"""Show title and tags in wall view"""
|
||||
wallShowTitle: Boolean
|
||||
"""Wall playback type"""
|
||||
wallPlayback: String
|
||||
"""Maximum duration (in seconds) in which a scene video will loop in the scene player"""
|
||||
maximumLoopDuration: Int
|
||||
"""If true, video will autostart on load in the scene player"""
|
||||
|
|
@ -91,6 +116,8 @@ type ConfigInterfaceResult {
|
|||
"""Custom CSS"""
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
"""Interface language"""
|
||||
language: String
|
||||
}
|
||||
|
||||
"""All configuration settings"""
|
||||
|
|
@ -98,3 +125,10 @@ type ConfigResult {
|
|||
general: ConfigGeneralResult!
|
||||
interface: ConfigInterfaceResult!
|
||||
}
|
||||
|
||||
"""Directory structure of a path"""
|
||||
type Directory {
|
||||
path: String!
|
||||
parent: String
|
||||
directories: [String!]!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ input PerformerFilterType {
|
|||
piercings: StringCriterionInput
|
||||
"""Filter by aliases"""
|
||||
aliases: StringCriterionInput
|
||||
"""Filter by gender"""
|
||||
gender: GenderCriterionInput
|
||||
"""Filter to only include performers missing this property"""
|
||||
is_missing: String
|
||||
}
|
||||
|
||||
input SceneMarkerFilterType {
|
||||
|
|
@ -74,12 +78,19 @@ input SceneFilterType {
|
|||
is_missing: String
|
||||
"""Filter to only include scenes with this studio"""
|
||||
studios: MultiCriterionInput
|
||||
"""Filter to only include scenes with this movie"""
|
||||
movies: MultiCriterionInput
|
||||
"""Filter to only include scenes with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
"""Filter to only include scenes with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
}
|
||||
|
||||
input MovieFilterType {
|
||||
"""Filter to only include movies with this studio"""
|
||||
studios: MultiCriterionInput
|
||||
}
|
||||
|
||||
enum CriterionModifier {
|
||||
"""="""
|
||||
EQUALS,
|
||||
|
|
@ -112,4 +123,9 @@ input IntCriterionInput {
|
|||
input MultiCriterionInput {
|
||||
value: [ID!]
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input GenderCriterionInput {
|
||||
value: GenderEnum
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
input GenerateMetadataInput {
|
||||
sprites: Boolean!
|
||||
previews: Boolean!
|
||||
previewPreset: PreviewPreset
|
||||
imagePreviews: Boolean!
|
||||
markers: Boolean!
|
||||
transcodes: Boolean!
|
||||
"""gallery thumbnails for cache usage"""
|
||||
thumbnails: Boolean!
|
||||
}
|
||||
|
||||
input ScanMetadataInput {
|
||||
|
|
@ -20,6 +24,16 @@ input AutoTagMetadataInput {
|
|||
|
||||
type MetadataUpdateStatus {
|
||||
progress: Float!
|
||||
status: String!
|
||||
status: String!
|
||||
message: String!
|
||||
}
|
||||
}
|
||||
|
||||
enum PreviewPreset {
|
||||
"X264_ULTRAFAST", ultrafast
|
||||
"X264_VERYFAST", veryfast
|
||||
"X264_FAST", fast
|
||||
"X264_MEDIUM", medium
|
||||
"X264_SLOW", slow
|
||||
"X264_SLOWER", slower
|
||||
"X264_VERYSLOW", veryslow
|
||||
}
|
||||
|
|
|
|||
59
graphql/schema/types/movie.graphql
Normal file
59
graphql/schema/types/movie.graphql
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
type Movie {
|
||||
id: ID!
|
||||
checksum: String!
|
||||
name: String!
|
||||
aliases: String
|
||||
"""Duration in seconds"""
|
||||
duration: Int
|
||||
date: String
|
||||
rating: Int
|
||||
studio: Studio
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
|
||||
front_image_path: String # Resolver
|
||||
back_image_path: String # Resolver
|
||||
scene_count: Int # Resolver
|
||||
}
|
||||
|
||||
input MovieCreateInput {
|
||||
name: String!
|
||||
aliases: String
|
||||
"""Duration in seconds"""
|
||||
duration: Int
|
||||
date: String
|
||||
rating: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
"""This should be base64 encoded"""
|
||||
front_image: String
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input MovieUpdateInput {
|
||||
id: ID!
|
||||
name: String
|
||||
aliases: String
|
||||
duration: Int
|
||||
date: String
|
||||
rating: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
"""This should be base64 encoded"""
|
||||
front_image: String
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input MovieDestroyInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type FindMoviesResultType {
|
||||
count: Int!
|
||||
movies: [Movie!]!
|
||||
}
|
||||
|
|
@ -1,8 +1,17 @@
|
|||
enum GenderEnum {
|
||||
MALE
|
||||
FEMALE
|
||||
TRANSGENDER_MALE
|
||||
TRANSGENDER_FEMALE
|
||||
INTERSEX
|
||||
}
|
||||
|
||||
type Performer {
|
||||
id: ID!
|
||||
checksum: String!
|
||||
name: String
|
||||
url: String
|
||||
gender: GenderEnum
|
||||
twitter: String
|
||||
instagram: String
|
||||
birthdate: String
|
||||
|
|
@ -26,6 +35,7 @@ type Performer {
|
|||
input PerformerCreateInput {
|
||||
name: String
|
||||
url: String
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
|
|
@ -48,6 +58,7 @@ input PerformerUpdateInput {
|
|||
id: ID!
|
||||
name: String
|
||||
url: String
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ type ScenePathsType {
|
|||
chapters_vtt: String # Resolver
|
||||
}
|
||||
|
||||
type SceneMovie {
|
||||
movie: Movie!
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
type Scene {
|
||||
id: ID!
|
||||
checksum: String!
|
||||
|
|
@ -36,10 +41,16 @@ type Scene {
|
|||
scene_markers: [SceneMarker!]!
|
||||
gallery: Gallery
|
||||
studio: Studio
|
||||
movies: [SceneMovie!]!
|
||||
tags: [Tag!]!
|
||||
performers: [Performer!]!
|
||||
}
|
||||
|
||||
input SceneMovieInput {
|
||||
movie_id: ID!
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
input SceneUpdateInput {
|
||||
clientMutationId: String
|
||||
id: ID!
|
||||
|
|
@ -51,11 +62,23 @@ input SceneUpdateInput {
|
|||
studio_id: ID
|
||||
gallery_id: ID
|
||||
performer_ids: [ID!]
|
||||
movies: [SceneMovieInput!]
|
||||
tag_ids: [ID!]
|
||||
"""This should be base64 encoded"""
|
||||
cover_image: String
|
||||
}
|
||||
|
||||
enum BulkUpdateIdMode {
|
||||
SET
|
||||
ADD
|
||||
REMOVE
|
||||
}
|
||||
|
||||
input BulkUpdateIds {
|
||||
ids: [ID!]
|
||||
mode: BulkUpdateIdMode!
|
||||
}
|
||||
|
||||
input BulkSceneUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
|
|
@ -66,8 +89,8 @@ input BulkSceneUpdateInput {
|
|||
rating: Int
|
||||
studio_id: ID
|
||||
gallery_id: ID
|
||||
performer_ids: [ID!]
|
||||
tag_ids: [ID!]
|
||||
performer_ids: BulkUpdateIds
|
||||
tag_ids: BulkUpdateIds
|
||||
}
|
||||
|
||||
input SceneDestroyInput {
|
||||
|
|
@ -87,6 +110,11 @@ input SceneParserInput {
|
|||
capitalizeTitle: Boolean
|
||||
}
|
||||
|
||||
type SceneMovieID {
|
||||
movie_id: ID!
|
||||
scene_index: String
|
||||
}
|
||||
|
||||
type SceneParserResult {
|
||||
scene: Scene!
|
||||
title: String
|
||||
|
|
@ -97,6 +125,7 @@ type SceneParserResult {
|
|||
studio_id: ID
|
||||
gallery_id: ID
|
||||
performer_ids: [ID!]
|
||||
movies: [SceneMovieID!]
|
||||
tag_ids: [ID!]
|
||||
}
|
||||
|
||||
|
|
|
|||
22
graphql/schema/types/scraped-movie.graphql
Normal file
22
graphql/schema/types/scraped-movie.graphql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"""A movie from a scraping operation..."""
|
||||
type ScrapedMovie {
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
url: String
|
||||
synopsis: String
|
||||
}
|
||||
|
||||
input ScrapedMovieInput {
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
url: String
|
||||
synopsis: String
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"""A performer from a scraping operation..."""
|
||||
type ScrapedPerformer {
|
||||
name: String
|
||||
gender: String
|
||||
url: String
|
||||
twitter: String
|
||||
instagram: String
|
||||
|
|
@ -15,10 +16,14 @@ type ScrapedPerformer {
|
|||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
|
||||
"""This should be base64 encoded"""
|
||||
image: String
|
||||
}
|
||||
|
||||
input ScrapedPerformerInput {
|
||||
name: String
|
||||
gender: String
|
||||
url: String
|
||||
twitter: String
|
||||
instagram: String
|
||||
|
|
@ -33,4 +38,6 @@ input ScrapedPerformerInput {
|
|||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
|
||||
# not including image for the input
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ type ScrapedScenePerformer {
|
|||
"""Set if performer matched"""
|
||||
id: ID
|
||||
name: String!
|
||||
gender: String
|
||||
url: String
|
||||
twitter: String
|
||||
instagram: String
|
||||
|
|
@ -43,6 +44,19 @@ type ScrapedScenePerformer {
|
|||
aliases: String
|
||||
}
|
||||
|
||||
type ScrapedSceneMovie {
|
||||
"""Set if movie matched"""
|
||||
id: ID
|
||||
name: String!
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
}
|
||||
|
||||
type ScrapedSceneStudio {
|
||||
"""Set if studio matched"""
|
||||
id: ID
|
||||
|
|
@ -62,9 +76,13 @@ type ScrapedScene {
|
|||
url: String
|
||||
date: String
|
||||
|
||||
"""This should be base64 encoded"""
|
||||
image: String
|
||||
|
||||
file: SceneFileType # Resolver
|
||||
|
||||
studio: ScrapedSceneStudio
|
||||
tags: [ScrapedSceneTag!]
|
||||
performers: [ScrapedScenePerformer!]
|
||||
movies: [ScrapedSceneMovie!]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
type StatsResultType {
|
||||
scene_count: Int!
|
||||
scene_size_count: String!
|
||||
gallery_count: Int!
|
||||
performer_count: Int!
|
||||
studio_count: Int!
|
||||
movie_count: Int!
|
||||
tag_count: Int!
|
||||
}
|
||||
}
|
||||
|
|
|
|||
72
pkg/api/cache_thumbs.go
Normal file
72
pkg/api/cache_thumbs.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager/paths"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type thumbBuffer struct {
|
||||
path string
|
||||
dir string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func newCacheThumb(dir string, path string, data []byte) *thumbBuffer {
|
||||
t := thumbBuffer{dir: dir, path: path, data: data}
|
||||
return &t
|
||||
}
|
||||
|
||||
var writeChan chan *thumbBuffer
|
||||
var touchChan chan *string
|
||||
|
||||
func startThumbCache() { // TODO add extra wait, close chan code if/when stash gets a stop mode
|
||||
|
||||
writeChan = make(chan *thumbBuffer, 20)
|
||||
go thumbnailCacheWriter()
|
||||
}
|
||||
|
||||
//serialize file writes to avoid race conditions
|
||||
func thumbnailCacheWriter() {
|
||||
|
||||
for thumb := range writeChan {
|
||||
exists, _ := utils.FileExists(thumb.path)
|
||||
if !exists {
|
||||
err := utils.WriteFile(thumb.path, thumb.data)
|
||||
if err != nil {
|
||||
logger.Errorf("Write error for thumbnail %s: %s ", thumb.path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// get thumbnail from cache, otherwise create it and store to cache
|
||||
func cacheGthumb(gallery *models.Gallery, index int, width int) []byte {
|
||||
thumbPath := paths.GetGthumbPath(gallery.Checksum, index, width)
|
||||
exists, _ := utils.FileExists(thumbPath)
|
||||
if exists { // if thumbnail exists in cache return that
|
||||
content, err := ioutil.ReadFile(thumbPath)
|
||||
if err == nil {
|
||||
return content
|
||||
} else {
|
||||
logger.Errorf("Read Error for file %s : %s", thumbPath, err)
|
||||
}
|
||||
|
||||
}
|
||||
data := gallery.GetThumbnail(index, width)
|
||||
thumbDir := paths.GetGthumbDir(gallery.Checksum)
|
||||
t := newCacheThumb(thumbDir, thumbPath, data)
|
||||
writeChan <- t // write the file to cache
|
||||
return data
|
||||
}
|
||||
|
||||
// create all thumbs for a given gallery
|
||||
func CreateGthumbs(gallery *models.Gallery) {
|
||||
count := gallery.ImageCount()
|
||||
for i := 0; i < count; i++ {
|
||||
cacheGthumb(gallery, i, models.DefaultGthumbWidth)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
|
@ -18,6 +19,10 @@ const apiTags string = "https://api.github.com/repos/stashapp/stash/tags"
|
|||
const apiAcceptHeader string = "application/vnd.github.v3+json"
|
||||
const developmentTag string = "latest_develop"
|
||||
|
||||
// ErrNoVersion indicates that no version information has been embedded in the
|
||||
// stash binary
|
||||
var ErrNoVersion = errors.New("no stash version")
|
||||
|
||||
var stashReleases = func() map[string]string {
|
||||
return map[string]string{
|
||||
"windows/amd64": "stash-win.exe",
|
||||
|
|
@ -140,7 +145,7 @@ func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease strin
|
|||
|
||||
version, _, _ := GetVersion()
|
||||
if version == "" {
|
||||
return "", "", fmt.Errorf("Stash doesn't have a version. Version check not supported.")
|
||||
return "", "", ErrNoVersion
|
||||
}
|
||||
|
||||
// if the version is suffixed with -x-xxxx, then we are running a development build
|
||||
|
|
|
|||
|
|
@ -9,4 +9,6 @@ const (
|
|||
performerKey key = 1
|
||||
sceneKey key = 2
|
||||
studioKey key = 3
|
||||
movieKey key = 4
|
||||
ContextUser key = 5
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,18 +2,31 @@ package api
|
|||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/gobuffalo/packr/v2"
|
||||
)
|
||||
|
||||
var performerBox *packr.Box
|
||||
var performerBoxMale *packr.Box
|
||||
|
||||
func initialiseImages() {
|
||||
performerBox = packr.New("Performer Box", "../../static/performer")
|
||||
performerBoxMale = packr.New("Male Performer Box", "../../static/performer_male")
|
||||
}
|
||||
|
||||
func getRandomPerformerImage() ([]byte, error) {
|
||||
imageFiles := performerBox.List()
|
||||
func getRandomPerformerImage(gender string) ([]byte, error) {
|
||||
var box *packr.Box
|
||||
switch strings.ToUpper(gender) {
|
||||
case "FEMALE":
|
||||
box = performerBox
|
||||
case "MALE":
|
||||
box = performerBoxMale
|
||||
default:
|
||||
box = performerBox
|
||||
|
||||
}
|
||||
imageFiles := box.List()
|
||||
index := rand.Intn(len(imageFiles))
|
||||
return performerBox.Find(imageFiles[index])
|
||||
return box.Find(imageFiles[index])
|
||||
}
|
||||
|
|
|
|||
92
pkg/api/migrate.go
Normal file
92
pkg/api/migrate.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
type migrateData struct {
|
||||
ExistingVersion uint
|
||||
MigrateVersion uint
|
||||
BackupPath string
|
||||
}
|
||||
|
||||
func getMigrateData() migrateData {
|
||||
return migrateData{
|
||||
ExistingVersion: database.Version(),
|
||||
MigrateVersion: database.AppSchemaVersion(),
|
||||
BackupPath: database.DatabaseBackupPath(),
|
||||
}
|
||||
}
|
||||
|
||||
func getMigrateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !database.NeedsMigration() {
|
||||
http.Redirect(w, r, "/", 301)
|
||||
return
|
||||
}
|
||||
|
||||
data, _ := setupUIBox.Find("migrate.html")
|
||||
templ, err := template.New("Migrate").Parse(string(data))
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error: %s", err), 500)
|
||||
return
|
||||
}
|
||||
|
||||
err = templ.Execute(w, getMigrateData())
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error: %s", err), 500)
|
||||
}
|
||||
}
|
||||
|
||||
func doMigrateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error: %s", err), 500)
|
||||
}
|
||||
|
||||
formBackupPath := r.Form.Get("backuppath")
|
||||
|
||||
// always backup so that we can roll back to the previous version if
|
||||
// migration fails
|
||||
backupPath := formBackupPath
|
||||
if formBackupPath == "" {
|
||||
backupPath = database.DatabaseBackupPath()
|
||||
}
|
||||
|
||||
// perform database backup
|
||||
if err = database.Backup(backupPath); err != nil {
|
||||
http.Error(w, fmt.Sprintf("error backing up database: %s", err), 500)
|
||||
return
|
||||
}
|
||||
|
||||
err = database.RunMigrations()
|
||||
if 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
|
||||
}
|
||||
|
||||
http.Error(w, errStr, 500)
|
||||
return
|
||||
}
|
||||
|
||||
// if no backup path was provided, then delete the created backup
|
||||
if formBackupPath == "" {
|
||||
err = os.Remove(backupPath)
|
||||
if err != nil {
|
||||
logger.Warnf("error removing unwanted database backup (%s): %s", backupPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", 301)
|
||||
}
|
||||
|
|
@ -33,6 +33,9 @@ func (r *Resolver) SceneMarker() models.SceneMarkerResolver {
|
|||
func (r *Resolver) Studio() models.StudioResolver {
|
||||
return &studioResolver{r}
|
||||
}
|
||||
func (r *Resolver) Movie() models.MovieResolver {
|
||||
return &movieResolver{r}
|
||||
}
|
||||
func (r *Resolver) Subscription() models.SubscriptionResolver {
|
||||
return &subscriptionResolver{r}
|
||||
}
|
||||
|
|
@ -49,6 +52,7 @@ type performerResolver struct{ *Resolver }
|
|||
type sceneResolver struct{ *Resolver }
|
||||
type sceneMarkerResolver struct{ *Resolver }
|
||||
type studioResolver struct{ *Resolver }
|
||||
type movieResolver struct{ *Resolver }
|
||||
type tagResolver struct{ *Resolver }
|
||||
|
||||
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) ([]*models.SceneMarker, error) {
|
||||
|
|
@ -89,19 +93,24 @@ func (r *queryResolver) ValidGalleriesForScene(ctx context.Context, scene_id *st
|
|||
func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) {
|
||||
scenesQB := models.NewSceneQueryBuilder()
|
||||
scenesCount, _ := scenesQB.Count()
|
||||
scenesSizeCount, _ := scenesQB.SizeCount()
|
||||
galleryQB := models.NewGalleryQueryBuilder()
|
||||
galleryCount, _ := galleryQB.Count()
|
||||
performersQB := models.NewPerformerQueryBuilder()
|
||||
performersCount, _ := performersQB.Count()
|
||||
studiosQB := models.NewStudioQueryBuilder()
|
||||
studiosCount, _ := studiosQB.Count()
|
||||
moviesQB := models.NewMovieQueryBuilder()
|
||||
moviesCount, _ := moviesQB.Count()
|
||||
tagsQB := models.NewTagQueryBuilder()
|
||||
tagsCount, _ := tagsQB.Count()
|
||||
return &models.StatsResultType{
|
||||
SceneCount: scenesCount,
|
||||
SceneSizeCount: scenesSizeCount,
|
||||
GalleryCount: galleryCount,
|
||||
PerformerCount: performersCount,
|
||||
StudioCount: studiosCount,
|
||||
MovieCount: moviesCount,
|
||||
TagCount: tagsCount,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
95
pkg/api/resolver_model_movie.go
Normal file
95
pkg/api/resolver_model_movie.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *movieResolver) Name(ctx context.Context, obj *models.Movie) (string, error) {
|
||||
if obj.Name.Valid {
|
||||
return obj.Name.String, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) URL(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
if obj.URL.Valid {
|
||||
return &obj.URL.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Aliases(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
if obj.Aliases.Valid {
|
||||
return &obj.Aliases.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Duration(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
if obj.Duration.Valid {
|
||||
rating := int(obj.Duration.Int64)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
if obj.Date.Valid {
|
||||
result := utils.GetYMDFromDatabaseDate(obj.Date.String)
|
||||
return &result, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (*models.Studio, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
if obj.StudioID.Valid {
|
||||
return qb.Find(int(obj.StudioID.Int64), nil)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Director(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
if obj.Director.Valid {
|
||||
return &obj.Director.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Synopsis(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
if obj.Synopsis.Valid {
|
||||
return &obj.Synopsis.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
frontimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj.ID).GetMovieFrontImageURL()
|
||||
return &frontimagePath, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
backimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj.ID).GetMovieBackImageURL()
|
||||
return &backimagePath, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
res, err := qb.CountByMovieID(obj.ID)
|
||||
return &res, err
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
|
@ -20,6 +21,19 @@ func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*st
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Gender(ctx context.Context, obj *models.Performer) (*models.GenderEnum, error) {
|
||||
var ret models.GenderEnum
|
||||
|
||||
if obj.Gender.Valid {
|
||||
ret = models.GenderEnum(obj.Gender.String)
|
||||
if ret.IsValid() {
|
||||
return &ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Twitter.Valid {
|
||||
return &obj.Twitter.String, nil
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.S
|
|||
}
|
||||
|
||||
func (r *sceneResolver) IsStreamable(ctx context.Context, obj *models.Scene) (bool, error) {
|
||||
return manager.IsStreamable(obj)
|
||||
// ignore error
|
||||
ret, _ := manager.IsStreamable(obj)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) ([]*models.SceneMarker, error) {
|
||||
|
|
@ -100,6 +102,38 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (*models.
|
|||
return qb.FindBySceneID(obj.ID)
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) ([]*models.SceneMovie, error) {
|
||||
joinQB := models.NewJoinsQueryBuilder()
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
|
||||
sceneMovies, err := joinQB.GetSceneMovies(obj.ID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []*models.SceneMovie
|
||||
for _, sm := range sceneMovies {
|
||||
movie, err := qb.Find(sm.MovieID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sceneIdx := sm.SceneIndex
|
||||
sceneMovie := &models.SceneMovie{
|
||||
Movie: movie,
|
||||
}
|
||||
|
||||
if sceneIdx.Valid {
|
||||
var idx int
|
||||
idx = int(sceneIdx.Int64)
|
||||
sceneMovie.SceneIndex = &idx
|
||||
}
|
||||
|
||||
ret = append(ret, sceneMovie)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) ([]*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
return qb.FindBySceneID(obj.ID, nil)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,13 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
|||
config.Set(config.Generated, input.GeneratedPath)
|
||||
}
|
||||
|
||||
if input.CachePath != nil {
|
||||
if err := utils.EnsureDir(*input.CachePath); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
config.Set(config.Cache, input.CachePath)
|
||||
}
|
||||
|
||||
if input.MaxTranscodeSize != nil {
|
||||
config.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||
}
|
||||
|
|
@ -45,6 +52,8 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
|||
if input.MaxStreamingTranscodeSize != nil {
|
||||
config.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
|
||||
}
|
||||
config.Set(config.ForceMKV, input.ForceMkv)
|
||||
config.Set(config.ForceHEVC, input.ForceHevc)
|
||||
|
||||
if input.Username != nil {
|
||||
config.Set(config.Username, input.Username)
|
||||
|
|
@ -60,6 +69,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
|||
}
|
||||
}
|
||||
|
||||
if input.MaxSessionAge != nil {
|
||||
config.Set(config.MaxSessionAge, *input.MaxSessionAge)
|
||||
}
|
||||
|
||||
if input.LogFile != nil {
|
||||
config.Set(config.LogFile, input.LogFile)
|
||||
}
|
||||
|
|
@ -76,6 +89,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
|||
config.Set(config.Exclude, input.Excludes)
|
||||
}
|
||||
|
||||
if input.ScraperUserAgent != nil {
|
||||
config.Set(config.ScraperUserAgent, input.ScraperUserAgent)
|
||||
}
|
||||
|
||||
if err := config.Write(); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
|
@ -94,6 +111,10 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
|||
config.Set(config.WallShowTitle, *input.WallShowTitle)
|
||||
}
|
||||
|
||||
if input.WallPlayback != nil {
|
||||
config.Set(config.WallPlayback, *input.WallPlayback)
|
||||
}
|
||||
|
||||
if input.MaximumLoopDuration != nil {
|
||||
config.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
|
||||
}
|
||||
|
|
@ -106,6 +127,10 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
|||
config.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
|
||||
}
|
||||
|
||||
if input.Language != nil {
|
||||
config.Set(config.Language, *input.Language)
|
||||
}
|
||||
|
||||
css := ""
|
||||
|
||||
if input.CSS != nil {
|
||||
|
|
|
|||
53
pkg/api/resolver_mutation_metadata.go
Normal file
53
pkg/api/resolver_mutation_metadata.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
|
||||
manager.GetInstance().Scan(input.UseFileMetadata)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataImport(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Import()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataExport(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Export()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
|
||||
manager.GetInstance().Generate(input.Sprites, input.Previews, input.PreviewPreset, input.ImagePreviews, input.Markers, input.Transcodes, input.Thumbnails)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
|
||||
manager.GetInstance().AutoTag(input.Performers, input.Studios, input.Tags)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataClean(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Clean()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateStatus, error) {
|
||||
status := manager.GetInstance().Status
|
||||
ret := models.MetadataUpdateStatus{
|
||||
Progress: status.Progress,
|
||||
Status: status.Status.String(),
|
||||
Message: "",
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StopJob(ctx context.Context) (bool, error) {
|
||||
return manager.GetInstance().Status.Stop(), nil
|
||||
}
|
||||
199
pkg/api/resolver_mutation_movie.go
Normal file
199
pkg/api/resolver_mutation_movie.go
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCreateInput) (*models.Movie, error) {
|
||||
// generate checksum from movie name rather than image
|
||||
checksum := utils.MD5FromString(input.Name)
|
||||
|
||||
var frontimageData []byte
|
||||
var backimageData []byte
|
||||
var err error
|
||||
|
||||
if input.FrontImage == nil {
|
||||
input.FrontImage = &models.DefaultMovieImage
|
||||
}
|
||||
if input.BackImage == nil {
|
||||
input.BackImage = &models.DefaultMovieImage
|
||||
}
|
||||
// Process the base 64 encoded image string
|
||||
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Process the base 64 encoded image string
|
||||
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Populate a new movie from the input
|
||||
currentTime := time.Now()
|
||||
newMovie := models.Movie{
|
||||
BackImage: backimageData,
|
||||
FrontImage: frontimageData,
|
||||
Checksum: checksum,
|
||||
Name: sql.NullString{String: input.Name, Valid: true},
|
||||
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
}
|
||||
|
||||
if input.Aliases != nil {
|
||||
newMovie.Aliases = sql.NullString{String: *input.Aliases, Valid: true}
|
||||
}
|
||||
if input.Duration != nil {
|
||||
duration := int64(*input.Duration)
|
||||
newMovie.Duration = sql.NullInt64{Int64: duration, Valid: true}
|
||||
}
|
||||
|
||||
if input.Date != nil {
|
||||
newMovie.Date = models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
rating := int64(*input.Rating)
|
||||
newMovie.Rating = sql.NullInt64{Int64: rating, Valid: true}
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
newMovie.StudioID = sql.NullInt64{Int64: studioID, Valid: true}
|
||||
}
|
||||
|
||||
if input.Director != nil {
|
||||
newMovie.Director = sql.NullString{String: *input.Director, Valid: true}
|
||||
}
|
||||
|
||||
if input.Synopsis != nil {
|
||||
newMovie.Synopsis = sql.NullString{String: *input.Synopsis, Valid: true}
|
||||
}
|
||||
|
||||
if input.URL != nil {
|
||||
newMovie.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
|
||||
// Start the transaction and save the movie
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
movie, err := qb.Create(newMovie, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return movie, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUpdateInput) (*models.Movie, error) {
|
||||
// Populate movie from the input
|
||||
movieID, _ := strconv.Atoi(input.ID)
|
||||
|
||||
updatedMovie := models.MoviePartial{
|
||||
ID: movieID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
|
||||
}
|
||||
if input.FrontImage != nil {
|
||||
_, frontimageData, err := utils.ProcessBase64Image(*input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updatedMovie.FrontImage = &frontimageData
|
||||
}
|
||||
if input.BackImage != nil {
|
||||
_, backimageData, err := utils.ProcessBase64Image(*input.BackImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updatedMovie.BackImage = &backimageData
|
||||
}
|
||||
|
||||
if input.Name != nil {
|
||||
// generate checksum from movie name rather than image
|
||||
checksum := utils.MD5FromString(*input.Name)
|
||||
updatedMovie.Name = &sql.NullString{String: *input.Name, Valid: true}
|
||||
updatedMovie.Checksum = &checksum
|
||||
}
|
||||
|
||||
if input.Aliases != nil {
|
||||
updatedMovie.Aliases = &sql.NullString{String: *input.Aliases, Valid: true}
|
||||
}
|
||||
if input.Duration != nil {
|
||||
duration := int64(*input.Duration)
|
||||
updatedMovie.Duration = &sql.NullInt64{Int64: duration, Valid: true}
|
||||
}
|
||||
|
||||
if input.Date != nil {
|
||||
updatedMovie.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
rating := int64(*input.Rating)
|
||||
updatedMovie.Rating = &sql.NullInt64{Int64: rating, Valid: true}
|
||||
} else {
|
||||
// rating must be nullable
|
||||
updatedMovie.Rating = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedMovie.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
updatedMovie.StudioID = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
if input.Director != nil {
|
||||
updatedMovie.Director = &sql.NullString{String: *input.Director, Valid: true}
|
||||
}
|
||||
|
||||
if input.Synopsis != nil {
|
||||
updatedMovie.Synopsis = &sql.NullString{String: *input.Synopsis, Valid: true}
|
||||
}
|
||||
|
||||
if input.URL != nil {
|
||||
updatedMovie.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
|
||||
// Start the transaction and save the movie
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
movie, err := qb.Update(updatedMovie, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return movie, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MovieDestroy(ctx context.Context, input models.MovieDestroyInput) (bool, error) {
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
if err := qb.Destroy(input.ID, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
@ -19,7 +19,11 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
|||
var err error
|
||||
|
||||
if input.Image == nil {
|
||||
imageData, err = getRandomPerformerImage()
|
||||
gender := ""
|
||||
if input.Gender != nil {
|
||||
gender = input.Gender.String()
|
||||
}
|
||||
imageData, err = getRandomPerformerImage(gender)
|
||||
} else {
|
||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||
}
|
||||
|
|
@ -42,6 +46,9 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
|||
if input.URL != nil {
|
||||
newPerformer.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Gender != nil {
|
||||
newPerformer.Gender = sql.NullString{String: input.Gender.String(), Valid: true}
|
||||
}
|
||||
if input.Birthdate != nil {
|
||||
newPerformer.Birthdate = models.SQLiteDate{String: *input.Birthdate, Valid: true}
|
||||
}
|
||||
|
|
@ -128,6 +135,9 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
|||
if input.URL != nil {
|
||||
updatedPerformer.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Gender != nil {
|
||||
updatedPerformer.Gender = sql.NullString{String: input.Gender.String(), Valid: true}
|
||||
}
|
||||
if input.Birthdate != nil {
|
||||
updatedPerformer.Birthdate = models.SQLiteDate{String: *input.Birthdate, Valid: true}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,31 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.T
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Save the movies
|
||||
var movieJoins []models.MoviesScenes
|
||||
|
||||
for _, movie := range input.Movies {
|
||||
|
||||
movieID, _ := strconv.Atoi(movie.MovieID)
|
||||
|
||||
movieJoin := models.MoviesScenes{
|
||||
MovieID: movieID,
|
||||
SceneID: sceneID,
|
||||
}
|
||||
|
||||
if movie.SceneIndex != nil {
|
||||
movieJoin.SceneIndex = sql.NullInt64{
|
||||
Int64: int64(*movie.SceneIndex),
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
movieJoins = append(movieJoins, movieJoin)
|
||||
}
|
||||
if err := jqb.UpdateMoviesScenes(sceneID, movieJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
var tagJoins []models.ScenesTags
|
||||
for _, tid := range input.TagIds {
|
||||
|
|
@ -247,9 +272,14 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
|
|||
|
||||
// Save the performers
|
||||
if wasFieldIncluded(ctx, "performer_ids") {
|
||||
performerIDs, err := adjustScenePerformerIDs(tx, sceneID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var performerJoins []models.PerformersScenes
|
||||
for _, pid := range input.PerformerIds {
|
||||
performerID, _ := strconv.Atoi(pid)
|
||||
for _, performerID := range performerIDs {
|
||||
performerJoin := models.PerformersScenes{
|
||||
PerformerID: performerID,
|
||||
SceneID: sceneID,
|
||||
|
|
@ -264,9 +294,14 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
|
|||
|
||||
// Save the tags
|
||||
if wasFieldIncluded(ctx, "tag_ids") {
|
||||
tagIDs, err := adjustSceneTagIDs(tx, sceneID, *input.TagIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tagJoins []models.ScenesTags
|
||||
for _, tid := range input.TagIds {
|
||||
tagID, _ := strconv.Atoi(tid)
|
||||
for _, tagID := range tagIDs {
|
||||
tagJoin := models.ScenesTags{
|
||||
SceneID: sceneID,
|
||||
TagID: tagID,
|
||||
|
|
@ -288,6 +323,72 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
|
|||
return ret, nil
|
||||
}
|
||||
|
||||
func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
|
||||
for _, idStr := range updateIDs.Ids {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
|
||||
// look for the id in the list
|
||||
foundExisting := false
|
||||
for idx, existingID := range existingIDs {
|
||||
if existingID == id {
|
||||
if updateIDs.Mode == models.BulkUpdateIDModeRemove {
|
||||
// remove from the list
|
||||
existingIDs = append(existingIDs[:idx], existingIDs[idx+1:]...)
|
||||
}
|
||||
|
||||
foundExisting = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundExisting && updateIDs.Mode != models.BulkUpdateIDModeRemove {
|
||||
existingIDs = append(existingIDs, id)
|
||||
}
|
||||
}
|
||||
|
||||
return existingIDs
|
||||
}
|
||||
|
||||
func adjustScenePerformerIDs(tx *sqlx.Tx, sceneID int, ids models.BulkUpdateIds) ([]int, error) {
|
||||
var ret []int
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
|
||||
// adding to the joins
|
||||
performerJoins, err := jqb.GetScenePerformers(sceneID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range performerJoins {
|
||||
ret = append(ret, join.PerformerID)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustSceneTagIDs(tx *sqlx.Tx, sceneID int, ids models.BulkUpdateIds) ([]int, error) {
|
||||
var ret []int
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
|
||||
// adding to the joins
|
||||
tagJoins, err := jqb.GetSceneTags(sceneID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range tagJoins {
|
||||
ret = append(ret, join.TagID)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
|
@ -356,6 +457,14 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input models.S
|
|||
func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) {
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
markerID, _ := strconv.Atoi(id)
|
||||
marker, err := qb.Find(markerID)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := qb.Destroy(id, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return false, err
|
||||
|
|
@ -363,6 +472,16 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
|
|||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// delete the preview for the marker
|
||||
sqb := models.NewSceneQueryBuilder()
|
||||
scene, _ := sqb.Find(int(marker.SceneID.Int64))
|
||||
|
||||
if scene != nil {
|
||||
seconds := int(marker.Seconds)
|
||||
manager.DeleteSceneMarkerFiles(scene, seconds)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
|
@ -372,13 +491,18 @@ func changeMarker(ctx context.Context, changeType int, changedMarker models.Scen
|
|||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
|
||||
var existingMarker *models.SceneMarker
|
||||
var sceneMarker *models.SceneMarker
|
||||
var err error
|
||||
switch changeType {
|
||||
case create:
|
||||
sceneMarker, err = qb.Create(changedMarker, tx)
|
||||
case update:
|
||||
sceneMarker, err = qb.Update(changedMarker, tx)
|
||||
// check to see if timestamp was changed
|
||||
existingMarker, err = qb.Find(changedMarker.ID)
|
||||
if err == nil {
|
||||
sceneMarker, err = qb.Update(changedMarker, tx)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
|
|
@ -416,6 +540,18 @@ func changeMarker(ctx context.Context, changeType int, changedMarker models.Scen
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// remove the marker preview if the timestamp was changed
|
||||
if existingMarker != nil && existingMarker.Seconds != changedMarker.Seconds {
|
||||
sqb := models.NewSceneQueryBuilder()
|
||||
|
||||
scene, _ := sqb.Find(int(existingMarker.SceneID.Int64))
|
||||
|
||||
if scene != nil {
|
||||
seconds := int(existingMarker.Seconds)
|
||||
manager.DeleteSceneMarkerFiles(scene, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
return sceneMarker, nil
|
||||
}
|
||||
|
||||
|
|
@ -478,3 +614,13 @@ func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (int, err
|
|||
|
||||
return newVal, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneGenerateScreenshot(ctx context.Context, id string, at *float64) (string, error) {
|
||||
if at != nil {
|
||||
manager.GetInstance().GenerateScreenshot(id, *at)
|
||||
} else {
|
||||
manager.GetInstance().GenerateDefaultScreenshot(id)
|
||||
}
|
||||
|
||||
return "todo", nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,18 @@ func (r *queryResolver) Configuration(ctx context.Context) (*models.ConfigResult
|
|||
return makeConfigResult(), nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Directories(ctx context.Context, path *string) ([]string, error) {
|
||||
func (r *queryResolver) Directory(ctx context.Context, path *string) (*models.Directory, error) {
|
||||
var dirPath = ""
|
||||
if path != nil {
|
||||
dirPath = *path
|
||||
}
|
||||
return utils.ListDir(dirPath), nil
|
||||
currentDir := utils.GetDir(dirPath)
|
||||
|
||||
return &models.Directory{
|
||||
Path: currentDir,
|
||||
Parent: utils.GetParent(currentDir),
|
||||
Directories: utils.ListDir(currentDir),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func makeConfigResult() *models.ConfigResult {
|
||||
|
|
@ -33,38 +39,49 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
|||
maxTranscodeSize := config.GetMaxTranscodeSize()
|
||||
maxStreamingTranscodeSize := config.GetMaxStreamingTranscodeSize()
|
||||
|
||||
scraperUserAgent := config.GetScraperUserAgent()
|
||||
|
||||
return &models.ConfigGeneralResult{
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
MaxTranscodeSize: &maxTranscodeSize,
|
||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||
ForceMkv: config.GetForceMKV(),
|
||||
ForceHevc: config.GetForceHEVC(),
|
||||
Username: config.GetUsername(),
|
||||
Password: config.GetPasswordHash(),
|
||||
MaxSessionAge: config.GetMaxSessionAge(),
|
||||
LogFile: &logFile,
|
||||
LogOut: config.GetLogOut(),
|
||||
LogLevel: config.GetLogLevel(),
|
||||
LogAccess: config.GetLogAccess(),
|
||||
Excludes: config.GetExcludes(),
|
||||
ScraperUserAgent: &scraperUserAgent,
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
soundOnPreview := config.GetSoundOnPreview()
|
||||
wallShowTitle := config.GetWallShowTitle()
|
||||
wallPlayback := config.GetWallPlayback()
|
||||
maximumLoopDuration := config.GetMaximumLoopDuration()
|
||||
autostartVideo := config.GetAutostartVideo()
|
||||
showStudioAsText := config.GetShowStudioAsText()
|
||||
css := config.GetCSS()
|
||||
cssEnabled := config.GetCSSEnabled()
|
||||
language := config.GetLanguage()
|
||||
|
||||
return &models.ConfigInterfaceResult{
|
||||
SoundOnPreview: &soundOnPreview,
|
||||
WallShowTitle: &wallShowTitle,
|
||||
WallPlayback: &wallPlayback,
|
||||
MaximumLoopDuration: &maximumLoopDuration,
|
||||
AutostartVideo: &autostartVideo,
|
||||
ShowStudioAsText: &showStudioAsText,
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
Language: &language,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
33
pkg/api/resolver_query_find_movie.go
Normal file
33
pkg/api/resolver_query_find_movie.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindMovie(ctx context.Context, id string) (*models.Movie, error) {
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
idInt, _ := strconv.Atoi(id)
|
||||
return qb.Find(idInt, nil)
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (*models.FindMoviesResultType, error) {
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
movies, total := qb.Query(movieFilter, filter)
|
||||
return &models.FindMoviesResultType{
|
||||
Count: total,
|
||||
Movies: movies,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllMovies(ctx context.Context) ([]*models.Movie, error) {
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
return qb.All()
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllMoviesSlim(ctx context.Context) ([]*models.Movie, error) {
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
return qb.AllSlim()
|
||||
}
|
||||
|
|
@ -25,3 +25,8 @@ func (r *queryResolver) AllPerformers(ctx context.Context) ([]*models.Performer,
|
|||
qb := models.NewPerformerQueryBuilder()
|
||||
return qb.All()
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllPerformersSlim(ctx context.Context) ([]*models.Performer, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
return qb.AllSlim()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,3 +25,8 @@ func (r *queryResolver) AllStudios(ctx context.Context) ([]*models.Studio, error
|
|||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.All()
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllStudiosSlim(ctx context.Context) ([]*models.Studio, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.AllSlim()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,3 +16,8 @@ func (r *queryResolver) AllTags(ctx context.Context) ([]*models.Tag, error) {
|
|||
qb := models.NewTagQueryBuilder()
|
||||
return qb.All()
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllTagsSlim(ctx context.Context) ([]*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
return qb.AllSlim()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,36 +7,6 @@ import (
|
|||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
|
||||
manager.GetInstance().Scan(input.UseFileMetadata)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MetadataImport(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Import()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MetadataExport(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Export()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
|
||||
manager.GetInstance().Generate(input.Sprites, input.Previews, input.Markers, input.Transcodes)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
|
||||
manager.GetInstance().AutoTag(input.Performers, input.Studios, input.Tags)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MetadataClean(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Clean()
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateStatus, error) {
|
||||
status := manager.GetInstance().Status
|
||||
ret := models.MetadataUpdateStatus{
|
||||
|
|
@ -47,7 +17,3 @@ func (r *queryResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateSt
|
|||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) StopJob(ctx context.Context) (bool, error) {
|
||||
return manager.GetInstance().Status.Stop(), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,15 @@ func (rs galleryRoutes) Routes() chi.Router {
|
|||
|
||||
func (rs galleryRoutes) File(w http.ResponseWriter, r *http.Request) {
|
||||
gallery := r.Context().Value(galleryKey).(*models.Gallery)
|
||||
if gallery == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
fileIndex, _ := strconv.Atoi(chi.URLParam(r, "fileIndex"))
|
||||
thumb := r.URL.Query().Get("thumb")
|
||||
w.Header().Add("Cache-Control", "max-age=604800000") // 1 Week
|
||||
if thumb == "true" {
|
||||
_, _ = w.Write(gallery.GetThumbnail(fileIndex, 200))
|
||||
_, _ = w.Write(cacheGthumb(gallery, fileIndex, models.DefaultGthumbWidth))
|
||||
} else if thumb == "" {
|
||||
_, _ = w.Write(gallery.GetImage(fileIndex))
|
||||
} else {
|
||||
|
|
@ -36,7 +40,7 @@ func (rs galleryRoutes) File(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, http.StatusText(400), 400)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(gallery.GetThumbnail(fileIndex, int(width)))
|
||||
_, _ = w.Write(cacheGthumb(gallery, fileIndex, int(width)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
54
pkg/api/routes_movie.go
Normal file
54
pkg/api/routes_movie.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type movieRoutes struct{}
|
||||
|
||||
func (rs movieRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{movieId}", func(r chi.Router) {
|
||||
r.Use(MovieCtx)
|
||||
r.Get("/frontimage", rs.FrontImage)
|
||||
r.Get("/backimage", rs.BackImage)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
|
||||
movie := r.Context().Value(movieKey).(*models.Movie)
|
||||
_, _ = w.Write(movie.FrontImage)
|
||||
}
|
||||
|
||||
func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
|
||||
movie := r.Context().Value(movieKey).(*models.Movie)
|
||||
_, _ = w.Write(movie.BackImage)
|
||||
}
|
||||
|
||||
func MovieCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
movieID, err := strconv.Atoi(chi.URLParam(r, "movieId"))
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
movie, err := qb.Find(movieID, nil)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), movieKey, movie)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -42,13 +43,32 @@ func (rs sceneRoutes) Routes() chi.Router {
|
|||
// region Handlers
|
||||
|
||||
func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
|
||||
container := ""
|
||||
if scene.Format.Valid {
|
||||
container = scene.Format.String
|
||||
} else { // container isn't in the DB
|
||||
// shouldn't happen, fallback to ffprobe
|
||||
tmpVideoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path)
|
||||
if err != nil {
|
||||
logger.Errorf("[transcode] error reading video file: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
container = string(ffmpeg.MatchContainer(tmpVideoFile.Container, scene.Path))
|
||||
}
|
||||
|
||||
// detect if not a streamable file and try to transcode it instead
|
||||
filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.Checksum)
|
||||
videoCodec := scene.VideoCodec.String
|
||||
audioCodec := ffmpeg.MissingUnsupported
|
||||
if scene.AudioCodec.Valid {
|
||||
audioCodec = ffmpeg.AudioCodec(scene.AudioCodec.String)
|
||||
}
|
||||
hasTranscode, _ := manager.HasTranscode(scene)
|
||||
if ffmpeg.IsValidCodec(videoCodec) || hasTranscode {
|
||||
if ffmpeg.IsValidCodec(videoCodec) && ffmpeg.IsValidCombo(videoCodec, ffmpeg.Container(container)) && ffmpeg.IsValidAudioForContainer(audioCodec, ffmpeg.Container(container)) || hasTranscode {
|
||||
manager.RegisterStream(filepath, &w)
|
||||
http.ServeFile(w, r, filepath)
|
||||
manager.WaitAndDeregisterStream(filepath, &w, r)
|
||||
|
|
@ -69,16 +89,50 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
|
||||
|
||||
stream, process, err := encoder.StreamTranscode(*videoFile, startTime, config.GetMaxStreamingTranscodeSize())
|
||||
var stream io.ReadCloser
|
||||
var process *os.Process
|
||||
mimeType := ffmpeg.MimeWebm
|
||||
|
||||
if audioCodec == ffmpeg.MissingUnsupported {
|
||||
//ffmpeg fails if it trys to transcode a non supported audio codec
|
||||
stream, process, err = encoder.StreamTranscodeVideo(*videoFile, startTime, config.GetMaxStreamingTranscodeSize())
|
||||
} else {
|
||||
copyVideo := false // try to be smart if the video to be transcoded is in a Matroska container
|
||||
// mp4 has always supported audio so it doesn't need to be checked
|
||||
// while mpeg_ts has seeking issues if we don't reencode the video
|
||||
|
||||
if config.GetForceMKV() { // If MKV is forced as supported and video codec is also supported then only transcode audio
|
||||
if ffmpeg.Container(container) == ffmpeg.Matroska {
|
||||
switch videoCodec {
|
||||
case ffmpeg.H264, ffmpeg.Vp9, ffmpeg.Vp8:
|
||||
copyVideo = true
|
||||
case ffmpeg.Hevc:
|
||||
if config.GetForceHEVC() {
|
||||
copyVideo = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if copyVideo { // copy video stream instead of transcoding it
|
||||
stream, process, err = encoder.StreamMkvTranscodeAudio(*videoFile, startTime, config.GetMaxStreamingTranscodeSize())
|
||||
mimeType = ffmpeg.MimeMkv
|
||||
|
||||
} else {
|
||||
stream, process, err = encoder.StreamTranscode(*videoFile, startTime, config.GetMaxStreamingTranscodeSize())
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("[stream] error transcoding video file: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "video/webm")
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
|
||||
logger.Info("[stream] transcoding video file")
|
||||
logger.Infof("[stream] transcoding video file to %s", mimeType)
|
||||
|
||||
// handle if client closes the connection
|
||||
notify := r.Context().Done()
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type studioRoutes struct{}
|
||||
|
|
@ -23,6 +26,21 @@ func (rs studioRoutes) Routes() chi.Router {
|
|||
|
||||
func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
studio := r.Context().Value(studioKey).(*models.Studio)
|
||||
etag := fmt.Sprintf("%x", md5.Sum(studio.Image))
|
||||
if match := r.Header.Get("If-None-Match"); match != "" {
|
||||
if strings.Contains(match, etag) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
contentType := http.DetectContentType(studio.Image)
|
||||
if contentType == "text/xml; charset=utf-8" || contentType == "text/plain; charset=utf-8" {
|
||||
contentType = "image/svg+xml"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Add("Etag", etag)
|
||||
_, _ = w.Write(studio.Image)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
|
@ -20,6 +21,7 @@ import (
|
|||
"github.com/gobuffalo/packr/v2"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/cors"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
|
|
@ -28,46 +30,81 @@ import (
|
|||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
var version string = ""
|
||||
var buildstamp string = ""
|
||||
var githash string = ""
|
||||
var version string
|
||||
var buildstamp string
|
||||
var githash string
|
||||
|
||||
var uiBox *packr.Box
|
||||
|
||||
//var legacyUiBox *packr.Box
|
||||
var setupUIBox *packr.Box
|
||||
var loginUIBox *packr.Box
|
||||
|
||||
func allowUnauthenticated(r *http.Request) bool {
|
||||
return strings.HasPrefix(r.URL.Path, "/login") || r.URL.Path == "/css"
|
||||
}
|
||||
|
||||
func authenticateHandler() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// only do this if credentials have been configured
|
||||
if !config.HasCredentials() {
|
||||
next.ServeHTTP(w, r)
|
||||
ctx := r.Context()
|
||||
|
||||
// translate api key into current user, if present
|
||||
userID := ""
|
||||
var err error
|
||||
|
||||
// handle session
|
||||
userID, err = getSessionUserID(w, r)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
authUser, authPW, ok := r.BasicAuth()
|
||||
// handle redirect if no user and user is required
|
||||
if userID == "" && config.HasCredentials() && !allowUnauthenticated(r) {
|
||||
// always allow
|
||||
|
||||
if !ok || !config.ValidateCredentials(authUser, authPW) {
|
||||
unauthorized(w)
|
||||
// if we don't have a userID, then redirect
|
||||
// if graphql was requested, we just return a forbidden error
|
||||
if r.URL.Path == "/graphql" {
|
||||
w.Header().Add("WWW-Authenticate", `FormBased`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise redirect to the login page
|
||||
u := url.URL{
|
||||
Path: "/login",
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set(returnURLParam, r.URL.Path)
|
||||
u.RawQuery = q.Encode()
|
||||
http.Redirect(w, r, u.String(), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, ContextUser, userID)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func unauthorized(w http.ResponseWriter) {
|
||||
w.Header().Add("WWW-Authenticate", `Basic realm=\"Stash\"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
const setupEndPoint = "/setup"
|
||||
const migrateEndPoint = "/migrate"
|
||||
const loginEndPoint = "/login"
|
||||
|
||||
func Start() {
|
||||
uiBox = packr.New("UI Box", "../../ui/v2/build")
|
||||
uiBox = packr.New("UI Box", "../../ui/v2.5/build")
|
||||
//legacyUiBox = packr.New("UI Box", "../../ui/v1/dist/stash-frontend")
|
||||
setupUIBox = packr.New("Setup UI Box", "../../ui/setup")
|
||||
loginUIBox = packr.New("Login UI Box", "../../ui/login")
|
||||
|
||||
initSessionStore()
|
||||
initialiseImages()
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
|
@ -83,6 +120,7 @@ func Start() {
|
|||
r.Use(cors.AllowAll().Handler)
|
||||
r.Use(BaseURLMiddleware)
|
||||
r.Use(ConfigCheckMiddleware)
|
||||
r.Use(DatabaseCheckMiddleware)
|
||||
|
||||
recoverFunc := handler.RecoverFunc(func(ctx context.Context, err interface{}) error {
|
||||
logger.Error(err)
|
||||
|
|
@ -105,12 +143,20 @@ func Start() {
|
|||
r.Handle("/graphql", gqlHandler)
|
||||
r.Handle("/playground", handler.Playground("GraphQL playground", "/graphql"))
|
||||
|
||||
// session handlers
|
||||
r.Post(loginEndPoint, handleLogin)
|
||||
r.Get("/logout", handleLogout)
|
||||
|
||||
r.Get(loginEndPoint, getLoginHandler)
|
||||
|
||||
r.Mount("/gallery", galleryRoutes{}.Routes())
|
||||
r.Mount("/performer", performerRoutes{}.Routes())
|
||||
r.Mount("/scene", sceneRoutes{}.Routes())
|
||||
r.Mount("/studio", studioRoutes{}.Routes())
|
||||
r.Mount("/movie", movieRoutes{}.Routes())
|
||||
|
||||
r.HandleFunc("/css", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
if !config.GetCSSEnabled() {
|
||||
return
|
||||
}
|
||||
|
|
@ -125,6 +171,10 @@ func Start() {
|
|||
http.ServeFile(w, r, fn)
|
||||
})
|
||||
|
||||
// Serve the migration UI
|
||||
r.Get("/migrate", getMigrateHandler)
|
||||
r.Post("/migrate", doMigrateHandler)
|
||||
|
||||
// Serve the setup UI
|
||||
r.HandleFunc("/setup*", func(w http.ResponseWriter, r *http.Request) {
|
||||
ext := path.Ext(r.URL.Path)
|
||||
|
|
@ -136,6 +186,16 @@ func Start() {
|
|||
http.FileServer(setupUIBox).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
r.HandleFunc("/login*", func(w http.ResponseWriter, r *http.Request) {
|
||||
ext := path.Ext(r.URL.Path)
|
||||
if ext == ".html" || ext == "" {
|
||||
data, _ := loginUIBox.Find("login.html")
|
||||
_, _ = w.Write(data)
|
||||
} else {
|
||||
r.URL.Path = strings.Replace(r.URL.Path, loginEndPoint, "", 1)
|
||||
http.FileServer(loginUIBox).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
r.Post("/init", func(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
|
|
@ -174,7 +234,8 @@ func Start() {
|
|||
|
||||
_ = os.Mkdir(downloads, 0755)
|
||||
|
||||
config.Set(config.Stash, stash)
|
||||
// #536 - set stash as slice of strings
|
||||
config.Set(config.Stash, []string{stash})
|
||||
config.Set(config.Generated, generated)
|
||||
config.Set(config.Metadata, metadata)
|
||||
config.Set(config.Cache, cache)
|
||||
|
|
@ -189,6 +250,7 @@ func Start() {
|
|||
http.Redirect(w, r, "/", 301)
|
||||
})
|
||||
|
||||
startThumbCache()
|
||||
// Serve the web app
|
||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
ext := path.Ext(r.URL.Path)
|
||||
|
|
@ -311,10 +373,27 @@ func BaseURLMiddleware(next http.Handler) http.Handler {
|
|||
func ConfigCheckMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ext := path.Ext(r.URL.Path)
|
||||
shouldRedirect := ext == "" && r.Method == "GET" && r.URL.Path != "/init"
|
||||
shouldRedirect := ext == "" && r.Method == "GET"
|
||||
if !config.IsValid() && shouldRedirect {
|
||||
if !strings.HasPrefix(r.URL.Path, "/setup") {
|
||||
http.Redirect(w, r, "/setup", 301)
|
||||
// #539 - don't redirect if loading login page
|
||||
if !strings.HasPrefix(r.URL.Path, setupEndPoint) && !strings.HasPrefix(r.URL.Path, loginEndPoint) {
|
||||
http.Redirect(w, r, setupEndPoint, 301)
|
||||
return
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func DatabaseCheckMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ext := path.Ext(r.URL.Path)
|
||||
shouldRedirect := ext == "" && r.Method == "GET"
|
||||
if shouldRedirect && database.NeedsMigration() {
|
||||
// #451 - don't redirect if loading login page
|
||||
// #539 - or setup page
|
||||
if !strings.HasPrefix(r.URL.Path, migrateEndPoint) && !strings.HasPrefix(r.URL.Path, loginEndPoint) && !strings.HasPrefix(r.URL.Path, setupEndPoint) {
|
||||
http.Redirect(w, r, migrateEndPoint, 301)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
127
pkg/api/session.go
Normal file
127
pkg/api/session.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
const cookieName = "session"
|
||||
const usernameFormKey = "username"
|
||||
const passwordFormKey = "password"
|
||||
const userIDKey = "userID"
|
||||
|
||||
const returnURLParam = "returnURL"
|
||||
|
||||
var sessionStore = sessions.NewCookieStore(config.GetSessionStoreKey())
|
||||
|
||||
type loginTemplateData struct {
|
||||
URL string
|
||||
Error string
|
||||
}
|
||||
|
||||
func initSessionStore() {
|
||||
sessionStore.MaxAge(config.GetMaxSessionAge())
|
||||
}
|
||||
|
||||
func redirectToLogin(w http.ResponseWriter, returnURL string, loginError string) {
|
||||
data, _ := loginUIBox.Find("login.html")
|
||||
templ, err := template.New("Login").Parse(string(data))
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error: %s", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = templ.Execute(w, loginTemplateData{URL: returnURL, Error: loginError})
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error: %s", err), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func getLoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !config.HasCredentials() {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
redirectToLogin(w, r.URL.Query().Get(returnURLParam), "")
|
||||
}
|
||||
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.FormValue(returnURLParam)
|
||||
if url == "" {
|
||||
url = "/"
|
||||
}
|
||||
|
||||
// ignore error - we want a new session regardless
|
||||
newSession, _ := sessionStore.Get(r, cookieName)
|
||||
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
// authenticate the user
|
||||
if !config.ValidateCredentials(username, password) {
|
||||
// redirect back to the login page with an error
|
||||
redirectToLogin(w, url, "Username or password is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
newSession.Values[userIDKey] = username
|
||||
|
||||
err := newSession.Save(r, w)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := sessionStore.Get(r, cookieName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
delete(session.Values, userIDKey)
|
||||
session.Options.MaxAge = -1
|
||||
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect to the login page if credentials are required
|
||||
getLoginHandler(w, r)
|
||||
}
|
||||
|
||||
func getSessionUserID(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
session, err := sessionStore.Get(r, cookieName)
|
||||
// ignore errors and treat as an empty user id, so that we handle expired
|
||||
// cookie
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !session.IsNew {
|
||||
val := session.Values[userIDKey]
|
||||
|
||||
// refresh the cookie
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ret, _ := val.(string)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
24
pkg/api/urlbuilders/movie.go
Normal file
24
pkg/api/urlbuilders/movie.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package urlbuilders
|
||||
|
||||
import "strconv"
|
||||
|
||||
type MovieURLBuilder struct {
|
||||
BaseURL string
|
||||
MovieID string
|
||||
}
|
||||
|
||||
func NewMovieURLBuilder(baseURL string, movieID int) MovieURLBuilder {
|
||||
return MovieURLBuilder{
|
||||
BaseURL: baseURL,
|
||||
MovieID: strconv.Itoa(movieID),
|
||||
}
|
||||
}
|
||||
|
||||
func (b MovieURLBuilder) GetMovieFrontImageURL() string {
|
||||
return b.BaseURL + "/movie/" + b.MovieID + "/frontimage"
|
||||
}
|
||||
|
||||
func (b MovieURLBuilder) GetMovieBackImageURL() string {
|
||||
return b.BaseURL + "/movie/" + b.MovieID + "/backimage"
|
||||
}
|
||||
|
||||
|
|
@ -5,10 +5,11 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/gobuffalo/packr/v2"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
sqlite3mig "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
"github.com/golang-migrate/migrate/v4/source"
|
||||
"github.com/jmoiron/sqlx"
|
||||
sqlite3 "github.com/mattn/go-sqlite3"
|
||||
|
|
@ -17,26 +18,62 @@ import (
|
|||
)
|
||||
|
||||
var DB *sqlx.DB
|
||||
var appSchemaVersion uint = 3
|
||||
var dbPath string
|
||||
var appSchemaVersion uint = 8
|
||||
var databaseSchemaVersion uint
|
||||
|
||||
const sqlite3Driver = "sqlite3_regexp"
|
||||
const sqlite3Driver = "sqlite3ex"
|
||||
|
||||
func init() {
|
||||
// register custom driver with regexp function
|
||||
registerRegexpFunc()
|
||||
registerCustomDriver()
|
||||
}
|
||||
|
||||
func Initialize(databasePath string) {
|
||||
runMigrations(databasePath)
|
||||
dbPath = databasePath
|
||||
|
||||
if err := getDatabaseSchemaVersion(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if databaseSchemaVersion == 0 {
|
||||
// new database, just run the migrations
|
||||
if err := RunMigrations(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// RunMigrations calls Initialise. Just return
|
||||
return
|
||||
} else {
|
||||
if databaseSchemaVersion > appSchemaVersion {
|
||||
panic(fmt.Sprintf("Database schema version %d is incompatible with required schema version %d", databaseSchemaVersion, appSchemaVersion))
|
||||
}
|
||||
|
||||
// if migration is needed, then don't open the connection
|
||||
if NeedsMigration() {
|
||||
logger.Warnf("Database schema version %d does not match required schema version %d.", databaseSchemaVersion, appSchemaVersion)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const disableForeignKeys = false
|
||||
DB = open(databasePath, disableForeignKeys)
|
||||
}
|
||||
|
||||
func open(databasePath string, disableForeignKeys bool) *sqlx.DB {
|
||||
// https://github.com/mattn/go-sqlite3
|
||||
conn, err := sqlx.Open(sqlite3Driver, "file:"+databasePath+"?_fk=true")
|
||||
url := "file:" + databasePath
|
||||
if !disableForeignKeys {
|
||||
url += "?_fk=true"
|
||||
}
|
||||
|
||||
conn, err := sqlx.Open(sqlite3Driver, url)
|
||||
conn.SetMaxOpenConns(25)
|
||||
conn.SetMaxIdleConns(4)
|
||||
if err != nil {
|
||||
logger.Fatalf("db.Open(): %q\n", err)
|
||||
}
|
||||
DB = conn
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func Reset(databasePath string) error {
|
||||
|
|
@ -55,45 +92,123 @@ func Reset(databasePath string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Backup the database
|
||||
func Backup(backupPath string) error {
|
||||
db, err := sqlx.Connect(sqlite3Driver, "file:"+dbPath+"?_fk=true")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Open database %s failed:%s", dbPath, err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec(`VACUUM INTO "` + backupPath + `"`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Vacuum failed: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RestoreFromBackup(backupPath string) error {
|
||||
return os.Rename(backupPath, dbPath)
|
||||
}
|
||||
|
||||
// Migrate the database
|
||||
func runMigrations(databasePath string) {
|
||||
func NeedsMigration() bool {
|
||||
return databaseSchemaVersion != appSchemaVersion
|
||||
}
|
||||
|
||||
func AppSchemaVersion() uint {
|
||||
return appSchemaVersion
|
||||
}
|
||||
|
||||
func DatabaseBackupPath() string {
|
||||
return fmt.Sprintf("%s.%d.%s", dbPath, databaseSchemaVersion, time.Now().Format("20060102_150405"))
|
||||
}
|
||||
|
||||
func Version() uint {
|
||||
return databaseSchemaVersion
|
||||
}
|
||||
|
||||
func getMigrate() (*migrate.Migrate, error) {
|
||||
migrationsBox := packr.New("Migrations Box", "./migrations")
|
||||
packrSource := &Packr2Source{
|
||||
Box: migrationsBox,
|
||||
Migrations: source.NewMigrations(),
|
||||
}
|
||||
|
||||
databasePath = utils.FixWindowsPath(databasePath)
|
||||
databasePath := utils.FixWindowsPath(dbPath)
|
||||
s, _ := WithInstance(packrSource)
|
||||
m, err := migrate.NewWithSourceInstance(
|
||||
|
||||
const disableForeignKeys = true
|
||||
conn := open(databasePath, disableForeignKeys)
|
||||
|
||||
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(
|
||||
"packr2",
|
||||
s,
|
||||
fmt.Sprintf("sqlite3://%s", "file:"+databasePath),
|
||||
databasePath,
|
||||
driver,
|
||||
)
|
||||
}
|
||||
|
||||
func getDatabaseSchemaVersion() error {
|
||||
m, err := getMigrate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
databaseSchemaVersion, _, _ = m.Version()
|
||||
m.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate the database
|
||||
func RunMigrations() error {
|
||||
m, err := getMigrate()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
databaseSchemaVersion, _, _ := m.Version()
|
||||
databaseSchemaVersion, _, _ = m.Version()
|
||||
stepNumber := appSchemaVersion - databaseSchemaVersion
|
||||
if stepNumber != 0 {
|
||||
err = m.Steps(int(stepNumber))
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
// migration failed
|
||||
m.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.Close()
|
||||
|
||||
// re-initialise the database
|
||||
Initialize(dbPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerRegexpFunc() {
|
||||
regexFn := func(re, s string) (bool, error) {
|
||||
return regexp.MatchString(re, s)
|
||||
}
|
||||
|
||||
func registerCustomDriver() {
|
||||
sql.Register(sqlite3Driver,
|
||||
&sqlite3.SQLiteDriver{
|
||||
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
||||
return conn.RegisterFunc("regexp", regexFn, true)
|
||||
funcs := map[string]interface{}{
|
||||
"regexp": regexFn,
|
||||
"durationToTinyInt": durationToTinyIntFn,
|
||||
}
|
||||
|
||||
for name, fn := range funcs {
|
||||
if err := conn.RegisterFunc(name, fn, true); err != nil {
|
||||
return fmt.Errorf("Error registering function %s: %s", name, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
37
pkg/database/functions.go
Normal file
37
pkg/database/functions.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func regexFn(re, s string) (bool, error) {
|
||||
return regexp.MatchString(re, s)
|
||||
}
|
||||
|
||||
func durationToTinyIntFn(str string) (int64, error) {
|
||||
splits := strings.Split(str, ":")
|
||||
|
||||
if len(splits) > 3 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
seconds := 0
|
||||
factor := 1
|
||||
for len(splits) > 0 {
|
||||
// pop the last split
|
||||
var thisSplit string
|
||||
thisSplit, splits = splits[len(splits)-1], splits[:len(splits)-1]
|
||||
|
||||
thisInt, err := strconv.Atoi(thisSplit)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
seconds += factor * thisInt
|
||||
factor *= 60
|
||||
}
|
||||
|
||||
return int64(seconds), nil
|
||||
}
|
||||
32
pkg/database/migrations/4_movie.up.sql
Normal file
32
pkg/database/migrations/4_movie.up.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
CREATE TABLE `movies` (
|
||||
`id` integer not null primary key autoincrement,
|
||||
`name` varchar(255),
|
||||
`aliases` varchar(255),
|
||||
`duration` varchar(6),
|
||||
`date` date,
|
||||
`rating` varchar(1),
|
||||
`director` varchar(255),
|
||||
`synopsis` text,
|
||||
`front_image` blob not null,
|
||||
`back_image` blob,
|
||||
`checksum` varchar(255) not null,
|
||||
`url` varchar(255),
|
||||
`created_at` datetime not null,
|
||||
`updated_at` datetime not null
|
||||
);
|
||||
CREATE TABLE `movies_scenes` (
|
||||
`movie_id` integer,
|
||||
`scene_id` integer,
|
||||
`scene_index` varchar(2),
|
||||
foreign key(`movie_id`) references `movies`(`id`),
|
||||
foreign key(`scene_id`) references `scenes`(`id`)
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE `scraped_items` ADD COLUMN `movie_id` integer;
|
||||
CREATE UNIQUE INDEX `movies_checksum_unique` on `movies` (`checksum`);
|
||||
CREATE UNIQUE INDEX `index_movie_id_scene_index_unique` ON `movies_scenes` ( `movie_id`, `scene_index` );
|
||||
CREATE INDEX `index_movies_scenes_on_movie_id` on `movies_scenes` (`movie_id`);
|
||||
CREATE INDEX `index_movies_scenes_on_scene_id` on `movies_scenes` (`scene_id`);
|
||||
|
||||
|
||||
89
pkg/database/migrations/5_performer_gender.down.sql
Normal file
89
pkg/database/migrations/5_performer_gender.down.sql
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
|
||||
PRAGMA foreign_keys=off;
|
||||
|
||||
-- need to re-create the performers table without the added column.
|
||||
-- also need re-create the performers_scenes table due to the foreign key
|
||||
|
||||
-- rename existing performers table
|
||||
ALTER TABLE `performers` RENAME TO `performers_old`;
|
||||
ALTER TABLE `performers_scenes` RENAME TO `performers_scenes_old`;
|
||||
|
||||
-- drop the indexes
|
||||
DROP INDEX IF EXISTS `index_performers_on_name`;
|
||||
DROP INDEX IF EXISTS `index_performers_on_checksum`;
|
||||
DROP INDEX IF EXISTS `index_performers_scenes_on_scene_id`;
|
||||
DROP INDEX IF EXISTS `index_performers_scenes_on_performer_id`;
|
||||
|
||||
-- recreate the tables
|
||||
CREATE TABLE `performers` (
|
||||
`id` integer not null primary key autoincrement,
|
||||
`image` blob not null,
|
||||
`checksum` varchar(255) not null,
|
||||
`name` varchar(255),
|
||||
`url` varchar(255),
|
||||
`twitter` varchar(255),
|
||||
`instagram` varchar(255),
|
||||
`birthdate` date,
|
||||
`ethnicity` varchar(255),
|
||||
`country` varchar(255),
|
||||
`eye_color` varchar(255),
|
||||
`height` varchar(255),
|
||||
`measurements` varchar(255),
|
||||
`fake_tits` varchar(255),
|
||||
`career_length` varchar(255),
|
||||
`tattoos` varchar(255),
|
||||
`piercings` varchar(255),
|
||||
`aliases` varchar(255),
|
||||
`favorite` boolean not null default '0',
|
||||
`created_at` datetime not null,
|
||||
`updated_at` datetime not null
|
||||
);
|
||||
|
||||
CREATE TABLE `performers_scenes` (
|
||||
`performer_id` integer,
|
||||
`scene_id` integer,
|
||||
foreign key(`performer_id`) references `performers`(`id`),
|
||||
foreign key(`scene_id`) references `scenes`(`id`)
|
||||
);
|
||||
|
||||
INSERT INTO `performers`
|
||||
SELECT
|
||||
`id`,
|
||||
`image`,
|
||||
`checksum`,
|
||||
`name`,
|
||||
`url`,
|
||||
`twitter`,
|
||||
`instagram`,
|
||||
`birthdate`,
|
||||
`ethnicity`,
|
||||
`country`,
|
||||
`eye_color`,
|
||||
`height`,
|
||||
`measurements`,
|
||||
`fake_tits`,
|
||||
`career_length`,
|
||||
`tattoos`,
|
||||
`piercings`,
|
||||
`aliases`,
|
||||
`favorite`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
FROM `performers_old`;
|
||||
|
||||
INSERT INTO `performers_scenes`
|
||||
SELECT
|
||||
`performer_id`,
|
||||
`scene_id`
|
||||
FROM `performers_scenes_old`;
|
||||
|
||||
DROP TABLE `performers_scenes_old`;
|
||||
DROP TABLE `performers_old`;
|
||||
|
||||
-- re-create the indexes after removing the old tables
|
||||
CREATE INDEX `index_performers_on_name` on `performers` (`name`);
|
||||
CREATE INDEX `index_performers_on_checksum` on `performers` (`checksum`);
|
||||
CREATE INDEX `index_performers_scenes_on_scene_id` on `performers_scenes` (`scene_id`);
|
||||
CREATE INDEX `index_performers_scenes_on_performer_id` on `performers_scenes` (`performer_id`);
|
||||
|
||||
PRAGMA foreign_keys=on;
|
||||
1
pkg/database/migrations/5_performer_gender.up.sql
Normal file
1
pkg/database/migrations/5_performer_gender.up.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `performers` ADD COLUMN `gender` varchar(20);
|
||||
1
pkg/database/migrations/6_scenes_format.up.sql
Normal file
1
pkg/database/migrations/6_scenes_format.up.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `scenes` ADD COLUMN `format` varchar(255);
|
||||
101
pkg/database/migrations/7_performer_optimization.up.sql
Normal file
101
pkg/database/migrations/7_performer_optimization.up.sql
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
DROP INDEX `performers_checksum_unique`;
|
||||
DROP INDEX `index_performers_on_name`;
|
||||
DROP INDEX `index_performers_on_checksum`;
|
||||
ALTER TABLE `performers` RENAME TO `temp_old_performers`;
|
||||
CREATE TABLE `performers` (
|
||||
`id` integer not null primary key autoincrement,
|
||||
`checksum` varchar(255) not null,
|
||||
`name` varchar(255),
|
||||
`gender` varchar(20),
|
||||
`url` varchar(255),
|
||||
`twitter` varchar(255),
|
||||
`instagram` varchar(255),
|
||||
`birthdate` date,
|
||||
`ethnicity` varchar(255),
|
||||
`country` varchar(255),
|
||||
`eye_color` varchar(255),
|
||||
`height` varchar(255),
|
||||
`measurements` varchar(255),
|
||||
`fake_tits` varchar(255),
|
||||
`career_length` varchar(255),
|
||||
`tattoos` varchar(255),
|
||||
`piercings` varchar(255),
|
||||
`aliases` varchar(255),
|
||||
`favorite` boolean not null default '0',
|
||||
`created_at` datetime not null,
|
||||
`updated_at` datetime not null,
|
||||
`image` blob not null
|
||||
);
|
||||
CREATE UNIQUE INDEX `performers_checksum_unique` on `performers` (`checksum`);
|
||||
CREATE INDEX `index_performers_on_name` on `performers` (`name`);
|
||||
INSERT INTO `performers` (
|
||||
`id`,
|
||||
`checksum`,
|
||||
`name`,
|
||||
`gender`,
|
||||
`url`,
|
||||
`twitter`,
|
||||
`instagram`,
|
||||
`birthdate`,
|
||||
`ethnicity`,
|
||||
`country`,
|
||||
`eye_color`,
|
||||
`height`,
|
||||
`measurements`,
|
||||
`fake_tits`,
|
||||
`career_length`,
|
||||
`tattoos`,
|
||||
`piercings`,
|
||||
`aliases`,
|
||||
`favorite`,
|
||||
`created_at`,
|
||||
`updated_at`,
|
||||
`image`
|
||||
)
|
||||
SELECT
|
||||
`id`,
|
||||
`checksum`,
|
||||
`name`,
|
||||
`gender`,
|
||||
`url`,
|
||||
`twitter`,
|
||||
`instagram`,
|
||||
`birthdate`,
|
||||
`ethnicity`,
|
||||
`country`,
|
||||
`eye_color`,
|
||||
`height`,
|
||||
`measurements`,
|
||||
`fake_tits`,
|
||||
`career_length`,
|
||||
`tattoos`,
|
||||
`piercings`,
|
||||
`aliases`,
|
||||
`favorite`,
|
||||
`created_at`,
|
||||
`updated_at`,
|
||||
`image`
|
||||
FROM `temp_old_performers`;
|
||||
|
||||
DROP INDEX `index_performers_scenes_on_scene_id`;
|
||||
DROP INDEX `index_performers_scenes_on_performer_id`;
|
||||
ALTER TABLE performers_scenes RENAME TO temp_old_performers_scenes;
|
||||
CREATE TABLE `performers_scenes` (
|
||||
`performer_id` integer,
|
||||
`scene_id` integer,
|
||||
foreign key(`performer_id`) references `performers`(`id`),
|
||||
foreign key(`scene_id`) references `scenes`(`id`)
|
||||
);
|
||||
CREATE INDEX `index_performers_scenes_on_scene_id` on `performers_scenes` (`scene_id`);
|
||||
CREATE INDEX `index_performers_scenes_on_performer_id` on `performers_scenes` (`performer_id`);
|
||||
INSERT INTO `performers_scenes` (
|
||||
`performer_id`,
|
||||
`scene_id`
|
||||
)
|
||||
SELECT
|
||||
`performer_id`,
|
||||
`scene_id`
|
||||
FROM `temp_old_performers_scenes`;
|
||||
|
||||
DROP TABLE `temp_old_performers`;
|
||||
DROP TABLE `temp_old_performers_scenes`;
|
||||
106
pkg/database/migrations/8_movie_fix.up.sql
Normal file
106
pkg/database/migrations/8_movie_fix.up.sql
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
ALTER TABLE `movies` rename to `_movies_old`;
|
||||
ALTER TABLE `movies_scenes` rename to `_movies_scenes_old`;
|
||||
|
||||
DROP INDEX IF EXISTS `movies_checksum_unique`;
|
||||
DROP INDEX IF EXISTS `index_movie_id_scene_index_unique`;
|
||||
DROP INDEX IF EXISTS `index_movies_scenes_on_movie_id`;
|
||||
DROP INDEX IF EXISTS `index_movies_scenes_on_scene_id`;
|
||||
|
||||
-- recreate the movies table with fixed column types and constraints
|
||||
CREATE TABLE `movies` (
|
||||
`id` integer not null primary key autoincrement,
|
||||
-- add not null
|
||||
`name` varchar(255) not null,
|
||||
`aliases` varchar(255),
|
||||
-- varchar(6) -> integer
|
||||
`duration` integer,
|
||||
`date` date,
|
||||
-- varchar(1) -> tinyint
|
||||
`rating` tinyint,
|
||||
`studio_id` integer,
|
||||
`director` varchar(255),
|
||||
`synopsis` text,
|
||||
`checksum` varchar(255) not null,
|
||||
`url` varchar(255),
|
||||
`created_at` datetime not null,
|
||||
`updated_at` datetime not null,
|
||||
`front_image` blob not null,
|
||||
`back_image` blob,
|
||||
foreign key(`studio_id`) references `studios`(`id`) on delete set null
|
||||
);
|
||||
CREATE TABLE `movies_scenes` (
|
||||
`movie_id` integer,
|
||||
`scene_id` integer,
|
||||
-- varchar(2) -> tinyint
|
||||
`scene_index` tinyint,
|
||||
foreign key(`movie_id`) references `movies`(`id`) on delete cascade,
|
||||
foreign key(`scene_id`) references `scenes`(`id`) on delete cascade
|
||||
);
|
||||
|
||||
-- add unique index on movie name
|
||||
CREATE UNIQUE INDEX `movies_name_unique` on `movies` (`name`);
|
||||
CREATE UNIQUE INDEX `movies_checksum_unique` on `movies` (`checksum`);
|
||||
-- remove unique index on movies_scenes
|
||||
CREATE INDEX `index_movies_scenes_on_movie_id` on `movies_scenes` (`movie_id`);
|
||||
CREATE INDEX `index_movies_scenes_on_scene_id` on `movies_scenes` (`scene_id`);
|
||||
CREATE INDEX `index_movies_on_studio_id` on `movies` (`studio_id`);
|
||||
|
||||
-- custom functions cannot accept NULL values, so massage the old data
|
||||
UPDATE `_movies_old` set `duration` = 0 WHERE `duration` IS NULL;
|
||||
|
||||
-- now populate from the old tables
|
||||
INSERT INTO `movies`
|
||||
(
|
||||
`id`,
|
||||
`name`,
|
||||
`aliases`,
|
||||
`duration`,
|
||||
`date`,
|
||||
`rating`,
|
||||
`director`,
|
||||
`synopsis`,
|
||||
`front_image`,
|
||||
`back_image`,
|
||||
`checksum`,
|
||||
`url`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
)
|
||||
SELECT
|
||||
`id`,
|
||||
`name`,
|
||||
`aliases`,
|
||||
durationToTinyInt(`duration`),
|
||||
`date`,
|
||||
CAST(`rating` as tinyint),
|
||||
`director`,
|
||||
`synopsis`,
|
||||
`front_image`,
|
||||
`back_image`,
|
||||
`checksum`,
|
||||
`url`,
|
||||
`created_at`,
|
||||
`updated_at`
|
||||
FROM `_movies_old`
|
||||
-- ignore null named movies
|
||||
WHERE `name` is not null;
|
||||
|
||||
-- durationToTinyInt returns 0 if it cannot parse the string
|
||||
-- set these values to null instead
|
||||
UPDATE `movies` SET `duration` = NULL WHERE `duration` = 0;
|
||||
|
||||
INSERT INTO `movies_scenes`
|
||||
(
|
||||
`movie_id`,
|
||||
`scene_id`,
|
||||
`scene_index`
|
||||
)
|
||||
SELECT
|
||||
`movie_id`,
|
||||
`scene_id`,
|
||||
CAST(`scene_index` as tinyint)
|
||||
FROM `_movies_scenes_old`;
|
||||
|
||||
-- drop old tables
|
||||
DROP TABLE `_movies_scenes_old`;
|
||||
DROP TABLE `_movies_old`;
|
||||
|
|
@ -18,8 +18,8 @@ type Encoder struct {
|
|||
}
|
||||
|
||||
var (
|
||||
runningEncoders map[string][]*os.Process = make(map[string][]*os.Process)
|
||||
runningEncodersMutex = sync.RWMutex{}
|
||||
runningEncoders = make(map[string][]*os.Process)
|
||||
runningEncodersMutex = sync.RWMutex{}
|
||||
)
|
||||
|
||||
func NewEncoder(ffmpegPath string) Encoder {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ type ScenePreviewChunkOptions struct {
|
|||
OutputPath string
|
||||
}
|
||||
|
||||
func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePreviewChunkOptions) {
|
||||
func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePreviewChunkOptions, preset string) {
|
||||
args := []string{
|
||||
"-v", "error",
|
||||
"-xerror",
|
||||
"-ss", strconv.Itoa(options.Time),
|
||||
"-i", probeResult.Path,
|
||||
"-t", "0.75",
|
||||
|
|
@ -25,7 +26,7 @@ func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePre
|
|||
"-pix_fmt", "yuv420p",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.2",
|
||||
"-preset", "veryslow",
|
||||
"-preset", preset,
|
||||
"-crf", "21",
|
||||
"-threads", "4",
|
||||
"-vf", fmt.Sprintf("scale=%v:-2", options.Width),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ type ScreenshotOptions struct {
|
|||
Verbosity string
|
||||
}
|
||||
|
||||
func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) {
|
||||
func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) error {
|
||||
if options.Verbosity == "" {
|
||||
options.Verbosity = "error"
|
||||
}
|
||||
|
|
@ -28,5 +28,7 @@ func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) {
|
|||
"-f", "image2",
|
||||
options.OutputPath,
|
||||
}
|
||||
_, _ = e.run(probeResult, args)
|
||||
_, err := e.run(probeResult, args)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,49 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
|
|||
_, _ = e.run(probeResult, args)
|
||||
}
|
||||
|
||||
//transcode the video, remove the audio
|
||||
//in some videos where the audio codec is not supported by ffmpeg
|
||||
//ffmpeg fails if you try to transcode the audio
|
||||
func (e *Encoder) TranscodeVideo(probeResult VideoFile, options TranscodeOptions) {
|
||||
scale := calculateTranscodeScale(probeResult, options.MaxTranscodeSize)
|
||||
args := []string{
|
||||
"-i", probeResult.Path,
|
||||
"-an",
|
||||
"-c:v", "libx264",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.2",
|
||||
"-preset", "superfast",
|
||||
"-crf", "23",
|
||||
"-vf", "scale=" + scale,
|
||||
options.OutputPath,
|
||||
}
|
||||
_, _ = e.run(probeResult, args)
|
||||
}
|
||||
|
||||
//copy the video stream as is, transcode audio
|
||||
func (e *Encoder) TranscodeAudio(probeResult VideoFile, options TranscodeOptions) {
|
||||
args := []string{
|
||||
"-i", probeResult.Path,
|
||||
"-c:v", "copy",
|
||||
"-c:a", "aac",
|
||||
"-strict", "-2",
|
||||
options.OutputPath,
|
||||
}
|
||||
_, _ = e.run(probeResult, args)
|
||||
}
|
||||
|
||||
//copy the video stream as is, drop audio
|
||||
func (e *Encoder) CopyVideo(probeResult VideoFile, options TranscodeOptions) {
|
||||
args := []string{
|
||||
"-i", probeResult.Path,
|
||||
"-an",
|
||||
"-c:v", "copy",
|
||||
options.OutputPath,
|
||||
}
|
||||
_, _ = e.run(probeResult, args)
|
||||
}
|
||||
|
||||
func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string, maxTranscodeSize models.StreamingResolutionEnum) (io.ReadCloser, *os.Process, error) {
|
||||
scale := calculateTranscodeScale(probeResult, maxTranscodeSize)
|
||||
args := []string{}
|
||||
|
|
@ -92,3 +135,53 @@ func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string, maxTr
|
|||
|
||||
return e.stream(probeResult, args)
|
||||
}
|
||||
|
||||
//transcode the video, remove the audio
|
||||
//in some videos where the audio codec is not supported by ffmpeg
|
||||
//ffmpeg fails if you try to transcode the audio
|
||||
func (e *Encoder) StreamTranscodeVideo(probeResult VideoFile, startTime string, maxTranscodeSize models.StreamingResolutionEnum) (io.ReadCloser, *os.Process, error) {
|
||||
scale := calculateTranscodeScale(probeResult, maxTranscodeSize)
|
||||
args := []string{}
|
||||
|
||||
if startTime != "" {
|
||||
args = append(args, "-ss", startTime)
|
||||
}
|
||||
|
||||
args = append(args,
|
||||
"-i", probeResult.Path,
|
||||
"-an",
|
||||
"-c:v", "libvpx-vp9",
|
||||
"-vf", "scale="+scale,
|
||||
"-deadline", "realtime",
|
||||
"-cpu-used", "5",
|
||||
"-row-mt", "1",
|
||||
"-crf", "30",
|
||||
"-b:v", "0",
|
||||
"-f", "webm",
|
||||
"pipe:",
|
||||
)
|
||||
|
||||
return e.stream(probeResult, args)
|
||||
}
|
||||
|
||||
//it is very common in MKVs to have just the audio codec unsupported
|
||||
//copy the video stream, transcode the audio and serve as Matroska
|
||||
func (e *Encoder) StreamMkvTranscodeAudio(probeResult VideoFile, startTime string, maxTranscodeSize models.StreamingResolutionEnum) (io.ReadCloser, *os.Process, error) {
|
||||
args := []string{}
|
||||
|
||||
if startTime != "" {
|
||||
args = append(args, "-ss", startTime)
|
||||
}
|
||||
|
||||
args = append(args,
|
||||
"-i", probeResult.Path,
|
||||
"-c:v", "copy",
|
||||
"-c:a", "libopus",
|
||||
"-b:a", "96k",
|
||||
"-vbr", "on",
|
||||
"-f", "matroska",
|
||||
"pipe:",
|
||||
)
|
||||
|
||||
return e.stream(probeResult, args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,106 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
)
|
||||
|
||||
var ValidCodecs = []string{"h264", "h265", "vp8", "vp9"}
|
||||
type Container string
|
||||
type AudioCodec string
|
||||
|
||||
const (
|
||||
Mp4 Container = "mp4"
|
||||
M4v Container = "m4v"
|
||||
Mov Container = "mov"
|
||||
Wmv Container = "wmv"
|
||||
Webm Container = "webm"
|
||||
Matroska Container = "matroska"
|
||||
Avi Container = "avi"
|
||||
Flv Container = "flv"
|
||||
Mpegts Container = "mpegts"
|
||||
Aac AudioCodec = "aac"
|
||||
Mp3 AudioCodec = "mp3"
|
||||
Opus AudioCodec = "opus"
|
||||
Vorbis AudioCodec = "vorbis"
|
||||
MissingUnsupported AudioCodec = ""
|
||||
Mp4Ffmpeg string = "mov,mp4,m4a,3gp,3g2,mj2" // browsers support all of them
|
||||
M4vFfmpeg string = "mov,mp4,m4a,3gp,3g2,mj2" // so we don't care that ffmpeg
|
||||
MovFfmpeg string = "mov,mp4,m4a,3gp,3g2,mj2" // can't differentiate between them
|
||||
WmvFfmpeg string = "asf"
|
||||
WebmFfmpeg string = "matroska,webm"
|
||||
MatroskaFfmpeg string = "matroska,webm"
|
||||
AviFfmpeg string = "avi"
|
||||
FlvFfmpeg string = "flv"
|
||||
MpegtsFfmpeg string = "mpegts"
|
||||
H264 string = "h264"
|
||||
H265 string = "h265" // found in rare cases from a faulty encoder
|
||||
Hevc string = "hevc"
|
||||
Vp8 string = "vp8"
|
||||
Vp9 string = "vp9"
|
||||
MimeWebm string = "video/webm"
|
||||
MimeMkv string = "video/x-matroska"
|
||||
)
|
||||
|
||||
var ValidCodecs = []string{H264, H265, Vp8, Vp9}
|
||||
|
||||
var validForH264Mkv = []Container{Mp4, Matroska}
|
||||
var validForH264 = []Container{Mp4}
|
||||
var validForH265Mkv = []Container{Mp4, Matroska}
|
||||
var validForH265 = []Container{Mp4}
|
||||
var validForVp8 = []Container{Webm}
|
||||
var validForVp9Mkv = []Container{Webm, Matroska}
|
||||
var validForVp9 = []Container{Webm}
|
||||
var validForHevcMkv = []Container{Mp4, Matroska}
|
||||
var validForHevc = []Container{Mp4}
|
||||
|
||||
var validAudioForMkv = []AudioCodec{Aac, Mp3, Vorbis, Opus}
|
||||
var validAudioForWebm = []AudioCodec{Vorbis, Opus}
|
||||
var validAudioForMp4 = []AudioCodec{Aac, Mp3}
|
||||
|
||||
//maps user readable container strings to ffprobe's format_name
|
||||
//on some formats ffprobe can't differentiate
|
||||
var ContainerToFfprobe = map[Container]string{
|
||||
Mp4: Mp4Ffmpeg,
|
||||
M4v: M4vFfmpeg,
|
||||
Mov: MovFfmpeg,
|
||||
Wmv: WmvFfmpeg,
|
||||
Webm: WebmFfmpeg,
|
||||
Matroska: MatroskaFfmpeg,
|
||||
Avi: AviFfmpeg,
|
||||
Flv: FlvFfmpeg,
|
||||
Mpegts: MpegtsFfmpeg,
|
||||
}
|
||||
|
||||
var FfprobeToContainer = map[string]Container{
|
||||
Mp4Ffmpeg: Mp4,
|
||||
WmvFfmpeg: Wmv,
|
||||
AviFfmpeg: Avi,
|
||||
FlvFfmpeg: Flv,
|
||||
MpegtsFfmpeg: Mpegts,
|
||||
MatroskaFfmpeg: Matroska,
|
||||
}
|
||||
|
||||
func MatchContainer(format string, filePath string) Container { // match ffprobe string to our Container
|
||||
|
||||
container := FfprobeToContainer[format]
|
||||
if container == Matroska {
|
||||
container = MagicContainer(filePath) // use magic number instead of ffprobe for matroska,webm
|
||||
}
|
||||
if container == "" { // if format is not in our Container list leave it as ffprobes reported format_name
|
||||
container = Container(format)
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
func IsValidCodec(codecName string) bool {
|
||||
forceHEVC := config.GetForceHEVC()
|
||||
if forceHEVC {
|
||||
if codecName == Hevc {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range ValidCodecs {
|
||||
if c == codecName {
|
||||
return true
|
||||
|
|
@ -23,6 +118,78 @@ func IsValidCodec(codecName string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func IsValidAudio(audio AudioCodec, ValidCodecs []AudioCodec) bool {
|
||||
|
||||
// if audio codec is missing or unsupported by ffmpeg we can't do anything about it
|
||||
// report it as valid so that the file can at least be streamed directly if the video codec is supported
|
||||
if audio == MissingUnsupported {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, c := range ValidCodecs {
|
||||
if c == audio {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func IsValidAudioForContainer(audio AudioCodec, format Container) bool {
|
||||
switch format {
|
||||
case Matroska:
|
||||
return IsValidAudio(audio, validAudioForMkv)
|
||||
case Webm:
|
||||
return IsValidAudio(audio, validAudioForWebm)
|
||||
case Mp4:
|
||||
return IsValidAudio(audio, validAudioForMp4)
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func IsValidForContainer(format Container, validContainers []Container) bool {
|
||||
for _, fmt := range validContainers {
|
||||
if fmt == format {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//extend stream validation check to take into account container
|
||||
func IsValidCombo(codecName string, format Container) bool {
|
||||
forceMKV := config.GetForceMKV()
|
||||
forceHEVC := config.GetForceHEVC()
|
||||
switch codecName {
|
||||
case H264:
|
||||
if forceMKV {
|
||||
return IsValidForContainer(format, validForH264Mkv)
|
||||
}
|
||||
return IsValidForContainer(format, validForH264)
|
||||
case H265:
|
||||
if forceMKV {
|
||||
return IsValidForContainer(format, validForH265Mkv)
|
||||
}
|
||||
return IsValidForContainer(format, validForH265)
|
||||
case Vp8:
|
||||
return IsValidForContainer(format, validForVp8)
|
||||
case Vp9:
|
||||
if forceMKV {
|
||||
return IsValidForContainer(format, validForVp9Mkv)
|
||||
}
|
||||
return IsValidForContainer(format, validForVp9)
|
||||
case Hevc:
|
||||
if forceHEVC {
|
||||
if forceMKV {
|
||||
return IsValidForContainer(format, validForHevcMkv)
|
||||
}
|
||||
return IsValidForContainer(format, validForHevc)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type VideoFile struct {
|
||||
JSON FFProbeJSON
|
||||
AudioStream *FFProbeStream
|
||||
|
|
@ -98,7 +265,11 @@ func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
|
|||
result.Container = probeJSON.Format.FormatName
|
||||
duration, _ := strconv.ParseFloat(probeJSON.Format.Duration, 64)
|
||||
result.Duration = math.Round(duration*100) / 100
|
||||
fileStat, _ := os.Stat(filePath)
|
||||
fileStat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
logger.Errorf("Error statting file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
result.Size = fileStat.Size()
|
||||
result.StartTime, _ = strconv.ParseFloat(probeJSON.Format.StartTime, 64)
|
||||
result.CreationTime = probeJSON.Format.Tags.CreationTime.Time
|
||||
|
|
|
|||
66
pkg/ffmpeg/media_detection.go
Normal file
66
pkg/ffmpeg/media_detection.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"os"
|
||||
)
|
||||
|
||||
// detect file format from magic file number
|
||||
// https://github.com/lex-r/filetype/blob/73c10ad714e3b8ecf5cd1564c882ed6d440d5c2d/matchers/video.go
|
||||
|
||||
func mkv(buf []byte) bool {
|
||||
return len(buf) > 3 &&
|
||||
buf[0] == 0x1A && buf[1] == 0x45 &&
|
||||
buf[2] == 0xDF && buf[3] == 0xA3 &&
|
||||
containsMatroskaSignature(buf, []byte{'m', 'a', 't', 'r', 'o', 's', 'k', 'a'})
|
||||
}
|
||||
|
||||
func webm(buf []byte) bool {
|
||||
return len(buf) > 3 &&
|
||||
buf[0] == 0x1A && buf[1] == 0x45 &&
|
||||
buf[2] == 0xDF && buf[3] == 0xA3 &&
|
||||
containsMatroskaSignature(buf, []byte{'w', 'e', 'b', 'm'})
|
||||
}
|
||||
|
||||
func containsMatroskaSignature(buf, subType []byte) bool {
|
||||
limit := 4096
|
||||
if len(buf) < limit {
|
||||
limit = len(buf)
|
||||
}
|
||||
|
||||
index := bytes.Index(buf[:limit], subType)
|
||||
if index < 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
return buf[index-3] == 0x42 && buf[index-2] == 0x82
|
||||
}
|
||||
|
||||
//returns container as string ("" on error or no match)
|
||||
//implements only mkv or webm as ffprobe can't distinguish between them
|
||||
//and not all browsers support mkv
|
||||
func MagicContainer(file_path string) Container {
|
||||
file, err := os.Open(file_path)
|
||||
if err != nil {
|
||||
logger.Errorf("[magicfile] %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
_, err = file.Read(buf)
|
||||
if err != nil {
|
||||
logger.Errorf("[magicfile] %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
if webm(buf) {
|
||||
return Webm
|
||||
}
|
||||
if mkv(buf) {
|
||||
return Matroska
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -19,10 +19,12 @@ const Metadata = "metadata"
|
|||
const Downloads = "downloads"
|
||||
const Username = "username"
|
||||
const Password = "password"
|
||||
const MaxSessionAge = "max_session_age"
|
||||
|
||||
const DefaultMaxSessionAge = 60 * 60 * 1 // 1 hours
|
||||
|
||||
const Database = "database"
|
||||
|
||||
const ScrapersPath = "scrapers_path"
|
||||
const Exclude = "exclude"
|
||||
|
||||
const MaxTranscodeSize = "max_transcode_size"
|
||||
|
|
@ -32,6 +34,19 @@ const Host = "host"
|
|||
const Port = "port"
|
||||
const ExternalHost = "external_host"
|
||||
|
||||
// key used to sign JWT tokens
|
||||
const JWTSignKey = "jwt_secret_key"
|
||||
|
||||
// key used for session store
|
||||
const SessionStoreKey = "session_store_key"
|
||||
|
||||
// scraping options
|
||||
const ScrapersPath = "scrapers_path"
|
||||
const ScraperUserAgent = "scraper_user_agent"
|
||||
|
||||
// i18n
|
||||
const Language = "language"
|
||||
|
||||
// Interface options
|
||||
const SoundOnPreview = "sound_on_preview"
|
||||
const WallShowTitle = "wall_show_title"
|
||||
|
|
@ -39,6 +54,11 @@ const MaximumLoopDuration = "maximum_loop_duration"
|
|||
const AutostartVideo = "autostart_video"
|
||||
const ShowStudioAsText = "show_studio_as_text"
|
||||
const CSSEnabled = "cssEnabled"
|
||||
const WallPlayback = "wall_playback"
|
||||
|
||||
// Playback force codec,container
|
||||
const ForceMKV = "forceMKV"
|
||||
const ForceHEVC = "forceHEVC"
|
||||
|
||||
// Logging options
|
||||
const LogFile = "logFile"
|
||||
|
|
@ -83,6 +103,14 @@ func GetDatabasePath() string {
|
|||
return viper.GetString(Database)
|
||||
}
|
||||
|
||||
func GetJWTSignKey() []byte {
|
||||
return []byte(viper.GetString(JWTSignKey))
|
||||
}
|
||||
|
||||
func GetSessionStoreKey() []byte {
|
||||
return []byte(viper.GetString(SessionStoreKey))
|
||||
}
|
||||
|
||||
func GetDefaultScrapersPath() string {
|
||||
// default to the same directory as the config file
|
||||
configFileUsed := viper.ConfigFileUsed()
|
||||
|
|
@ -97,10 +125,25 @@ func GetExcludes() []string {
|
|||
return viper.GetStringSlice(Exclude)
|
||||
}
|
||||
|
||||
func GetLanguage() string {
|
||||
ret := viper.GetString(Language)
|
||||
|
||||
// default to English
|
||||
if ret == "" {
|
||||
return "en-US"
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func GetScrapersPath() string {
|
||||
return viper.GetString(ScrapersPath)
|
||||
}
|
||||
|
||||
func GetScraperUserAgent() string {
|
||||
return viper.GetString(ScraperUserAgent)
|
||||
}
|
||||
|
||||
func GetHost() string {
|
||||
return viper.GetString(Host)
|
||||
}
|
||||
|
|
@ -181,6 +224,13 @@ func ValidateCredentials(username string, password string) bool {
|
|||
return username == authUser && err == nil
|
||||
}
|
||||
|
||||
// GetMaxSessionAge gets the maximum age for session cookies, in seconds.
|
||||
// Session cookie expiry times are refreshed every request.
|
||||
func GetMaxSessionAge() int {
|
||||
viper.SetDefault(MaxSessionAge, DefaultMaxSessionAge)
|
||||
return viper.GetInt(MaxSessionAge)
|
||||
}
|
||||
|
||||
// Interface options
|
||||
func GetSoundOnPreview() bool {
|
||||
viper.SetDefault(SoundOnPreview, true)
|
||||
|
|
@ -192,6 +242,11 @@ func GetWallShowTitle() bool {
|
|||
return viper.GetBool(WallShowTitle)
|
||||
}
|
||||
|
||||
func GetWallPlayback() string {
|
||||
viper.SetDefault(WallPlayback, "video")
|
||||
return viper.GetString(WallPlayback)
|
||||
}
|
||||
|
||||
func GetMaximumLoopDuration() int {
|
||||
viper.SetDefault(MaximumLoopDuration, 0)
|
||||
return viper.GetInt(MaximumLoopDuration)
|
||||
|
|
@ -246,6 +301,15 @@ func GetCSSEnabled() bool {
|
|||
return viper.GetBool(CSSEnabled)
|
||||
}
|
||||
|
||||
// force codec,container
|
||||
func GetForceMKV() bool {
|
||||
return viper.GetBool(ForceMKV)
|
||||
}
|
||||
|
||||
func GetForceHEVC() bool {
|
||||
return viper.GetBool(ForceHEVC)
|
||||
}
|
||||
|
||||
// GetLogFile returns the filename of the file to output logs to.
|
||||
// An empty string means that file logging will be disabled.
|
||||
func GetLogFile() string {
|
||||
|
|
@ -294,3 +358,21 @@ func IsValid() bool {
|
|||
// TODO: check valid paths
|
||||
return setPaths
|
||||
}
|
||||
|
||||
// SetInitialConfig fills in missing required config fields
|
||||
func SetInitialConfig() error {
|
||||
// generate some api keys
|
||||
const apiKeyLength = 32
|
||||
|
||||
if string(GetJWTSignKey()) == "" {
|
||||
signKey := utils.GenerateRandomKey(apiKeyLength)
|
||||
Set(JWTSignKey, signKey)
|
||||
}
|
||||
|
||||
if string(GetSessionStoreKey()) == "" {
|
||||
sessionStoreKey := utils.GenerateRandomKey(apiKeyLength)
|
||||
Set(SessionStoreKey, sessionStoreKey)
|
||||
}
|
||||
|
||||
return Write()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
func excludeFiles(files []string, patterns []string) ([]string, int) {
|
||||
|
|
@ -37,21 +38,13 @@ func excludeFiles(files []string, patterns []string) ([]string, int) {
|
|||
}
|
||||
|
||||
func matchFile(file string, patterns []string) bool {
|
||||
if patterns == nil {
|
||||
logger.Infof("No exclude patterns in config.")
|
||||
|
||||
} else {
|
||||
if patterns != nil {
|
||||
fileRegexps := generateRegexps(patterns)
|
||||
|
||||
if len(fileRegexps) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, regPattern := range fileRegexps {
|
||||
if regPattern.MatchString(strings.ToLower(file)) {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
|
||||
|
|
@ -87,8 +88,10 @@ func initParserFields() {
|
|||
//I = new ParserField("i", undefined, "Matches any ignored word", false);
|
||||
|
||||
ret["d"] = newParserField("d", `(?:\.|-|_)`, false)
|
||||
ret["rating"] = newParserField("rating", `\d`, true)
|
||||
ret["performer"] = newParserField("performer", ".*", true)
|
||||
ret["studio"] = newParserField("studio", ".*", true)
|
||||
ret["movie"] = newParserField("movie", ".*", true)
|
||||
ret["tag"] = newParserField("tag", ".*", true)
|
||||
|
||||
// date fields
|
||||
|
|
@ -96,6 +99,7 @@ func initParserFields() {
|
|||
ret["yyyy"] = newParserField("yyyy", `\d{4}`, true)
|
||||
ret["yy"] = newParserField("yy", `\d{2}`, true)
|
||||
ret["mm"] = newParserField("mm", `\d{2}`, true)
|
||||
ret["mmm"] = newParserField("mmm", `\w{3}`, true)
|
||||
ret["dd"] = newParserField("dd", `\d{2}`, true)
|
||||
ret["yyyymmdd"] = newFullDateParserField("yyyymmdd", `\d{8}`)
|
||||
ret["yymmdd"] = newFullDateParserField("yymmdd", `\d{6}`)
|
||||
|
|
@ -204,6 +208,7 @@ type sceneHolder struct {
|
|||
mm string
|
||||
dd string
|
||||
performers []string
|
||||
movies []string
|
||||
studio string
|
||||
tags []string
|
||||
}
|
||||
|
|
@ -222,6 +227,10 @@ func newSceneHolder(scene *models.Scene) *sceneHolder {
|
|||
return &ret
|
||||
}
|
||||
|
||||
func validateRating(rating int) bool {
|
||||
return rating >= 1 && rating <= 5
|
||||
}
|
||||
|
||||
func validateDate(dateStr string) bool {
|
||||
splits := strings.Split(dateStr, "-")
|
||||
if len(splits) != 3 {
|
||||
|
|
@ -283,6 +292,20 @@ func (h *sceneHolder) setDate(field *parserField, value string) {
|
|||
}
|
||||
}
|
||||
|
||||
func mmmToMonth(mmm string) string {
|
||||
format := "02-Jan-2006"
|
||||
dateStr := "01-" + mmm + "-2000"
|
||||
t, err := time.Parse(format, dateStr)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// expect month in two-digit format
|
||||
format = "01-02-2006"
|
||||
return t.Format(format)[0:2]
|
||||
}
|
||||
|
||||
func (h *sceneHolder) setField(field parserField, value interface{}) {
|
||||
if field.isFullDateField {
|
||||
h.setDate(&field, value.(string))
|
||||
|
|
@ -302,27 +325,35 @@ func (h *sceneHolder) setField(field parserField, value interface{}) {
|
|||
Valid: true,
|
||||
}
|
||||
}
|
||||
case "rating":
|
||||
rating, _ := strconv.Atoi(value.(string))
|
||||
if validateRating(rating) {
|
||||
h.result.Rating = sql.NullInt64{
|
||||
Int64: int64(rating),
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
case "performer":
|
||||
// add performer to list
|
||||
h.performers = append(h.performers, value.(string))
|
||||
case "studio":
|
||||
h.studio = value.(string)
|
||||
case "movie":
|
||||
h.movies = append(h.movies, value.(string))
|
||||
case "tag":
|
||||
h.tags = append(h.tags, value.(string))
|
||||
case "yyyy":
|
||||
h.yyyy = value.(string)
|
||||
break
|
||||
case "yy":
|
||||
v := value.(string)
|
||||
v = "20" + v
|
||||
h.yyyy = v
|
||||
break
|
||||
case "mmm":
|
||||
h.mm = mmmToMonth(value.(string))
|
||||
case "mm":
|
||||
h.mm = value.(string)
|
||||
break
|
||||
case "dd":
|
||||
h.dd = value.(string)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +405,7 @@ func (m parseMapper) parse(scene *models.Scene) *sceneHolder {
|
|||
}
|
||||
|
||||
type performerQueryer interface {
|
||||
FindByNames(names []string, tx *sqlx.Tx) ([]*models.Performer, error)
|
||||
FindByNames(names []string, tx *sqlx.Tx, nocase bool) ([]*models.Performer, error)
|
||||
}
|
||||
|
||||
type sceneQueryer interface {
|
||||
|
|
@ -382,11 +413,15 @@ type sceneQueryer interface {
|
|||
}
|
||||
|
||||
type tagQueryer interface {
|
||||
FindByName(name string, tx *sqlx.Tx) (*models.Tag, error)
|
||||
FindByName(name string, tx *sqlx.Tx, nocase bool) (*models.Tag, error)
|
||||
}
|
||||
|
||||
type studioQueryer interface {
|
||||
FindByName(name string, tx *sqlx.Tx) (*models.Studio, error)
|
||||
FindByName(name string, tx *sqlx.Tx, nocase bool) (*models.Studio, error)
|
||||
}
|
||||
|
||||
type movieQueryer interface {
|
||||
FindByName(name string, tx *sqlx.Tx, nocase bool) (*models.Movie, error)
|
||||
}
|
||||
|
||||
type SceneFilenameParser struct {
|
||||
|
|
@ -396,12 +431,14 @@ type SceneFilenameParser struct {
|
|||
whitespaceRE *regexp.Regexp
|
||||
performerCache map[string]*models.Performer
|
||||
studioCache map[string]*models.Studio
|
||||
movieCache map[string]*models.Movie
|
||||
tagCache map[string]*models.Tag
|
||||
|
||||
performerQuery performerQueryer
|
||||
sceneQuery sceneQueryer
|
||||
tagQuery tagQueryer
|
||||
studioQuery studioQueryer
|
||||
movieQuery movieQueryer
|
||||
}
|
||||
|
||||
func NewSceneFilenameParser(filter *models.FindFilterType, config models.SceneParserInput) *SceneFilenameParser {
|
||||
|
|
@ -413,6 +450,7 @@ func NewSceneFilenameParser(filter *models.FindFilterType, config models.ScenePa
|
|||
|
||||
p.performerCache = make(map[string]*models.Performer)
|
||||
p.studioCache = make(map[string]*models.Studio)
|
||||
p.movieCache = make(map[string]*models.Movie)
|
||||
p.tagCache = make(map[string]*models.Tag)
|
||||
|
||||
p.initWhiteSpaceRegex()
|
||||
|
|
@ -429,6 +467,9 @@ func NewSceneFilenameParser(filter *models.FindFilterType, config models.ScenePa
|
|||
studioQuery := models.NewStudioQueryBuilder()
|
||||
p.studioQuery = &studioQuery
|
||||
|
||||
movieQuery := models.NewMovieQueryBuilder()
|
||||
p.movieQuery = &movieQuery
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
|
|
@ -505,7 +546,7 @@ func (p *SceneFilenameParser) queryPerformer(performerName string) *models.Perfo
|
|||
}
|
||||
|
||||
// perform an exact match and grab the first
|
||||
performers, _ := p.performerQuery.FindByNames([]string{performerName}, nil)
|
||||
performers, _ := p.performerQuery.FindByNames([]string{performerName}, nil, true)
|
||||
|
||||
var ret *models.Performer
|
||||
if len(performers) > 0 {
|
||||
|
|
@ -527,7 +568,7 @@ func (p *SceneFilenameParser) queryStudio(studioName string) *models.Studio {
|
|||
return ret
|
||||
}
|
||||
|
||||
ret, _ := p.studioQuery.FindByName(studioName, nil)
|
||||
ret, _ := p.studioQuery.FindByName(studioName, nil, true)
|
||||
|
||||
// add result to cache
|
||||
p.studioCache[studioName] = ret
|
||||
|
|
@ -535,6 +576,23 @@ func (p *SceneFilenameParser) queryStudio(studioName string) *models.Studio {
|
|||
return ret
|
||||
}
|
||||
|
||||
func (p *SceneFilenameParser) queryMovie(movieName string) *models.Movie {
|
||||
// massage the movie name
|
||||
movieName = delimiterRE.ReplaceAllString(movieName, " ")
|
||||
|
||||
// check cache first
|
||||
if ret, found := p.movieCache[movieName]; found {
|
||||
return ret
|
||||
}
|
||||
|
||||
ret, _ := p.movieQuery.FindByName(movieName, nil, true)
|
||||
|
||||
// add result to cache
|
||||
p.movieCache[movieName] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *SceneFilenameParser) queryTag(tagName string) *models.Tag {
|
||||
// massage the performer name
|
||||
tagName = delimiterRE.ReplaceAllString(tagName, " ")
|
||||
|
|
@ -545,7 +603,7 @@ func (p *SceneFilenameParser) queryTag(tagName string) *models.Tag {
|
|||
}
|
||||
|
||||
// match tag name exactly
|
||||
ret, _ := p.tagQuery.FindByName(tagName, nil)
|
||||
ret, _ := p.tagQuery.FindByName(tagName, nil, true)
|
||||
|
||||
// add result to cache
|
||||
p.tagCache[tagName] = ret
|
||||
|
|
@ -596,6 +654,24 @@ func (p *SceneFilenameParser) setStudio(h sceneHolder, result *models.SceneParse
|
|||
}
|
||||
}
|
||||
|
||||
func (p *SceneFilenameParser) setMovies(h sceneHolder, result *models.SceneParserResult) {
|
||||
// query for each movie
|
||||
moviesSet := make(map[int]bool)
|
||||
for _, movieName := range h.movies {
|
||||
if movieName != "" {
|
||||
movie := p.queryMovie(movieName)
|
||||
if movie != nil {
|
||||
if _, found := moviesSet[movie.ID]; !found {
|
||||
result.Movies = append(result.Movies, &models.SceneMovieID{
|
||||
MovieID: strconv.Itoa(movie.ID),
|
||||
})
|
||||
moviesSet[movie.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SceneFilenameParser) setParserResult(h sceneHolder, result *models.SceneParserResult) {
|
||||
if h.result.Title.Valid {
|
||||
title := h.result.Title.String
|
||||
|
|
@ -612,6 +688,11 @@ func (p *SceneFilenameParser) setParserResult(h sceneHolder, result *models.Scen
|
|||
result.Date = &h.result.Date.String
|
||||
}
|
||||
|
||||
if h.result.Rating.Valid {
|
||||
rating := int(h.result.Rating.Int64)
|
||||
result.Rating = &rating
|
||||
}
|
||||
|
||||
if len(h.performers) > 0 {
|
||||
p.setPerformers(h, result)
|
||||
}
|
||||
|
|
@ -619,4 +700,9 @@ func (p *SceneFilenameParser) setParserResult(h sceneHolder, result *models.Scen
|
|||
p.setTags(h, result)
|
||||
}
|
||||
p.setStudio(h, result)
|
||||
|
||||
if len(h.movies) > 0 {
|
||||
p.setMovies(h, result)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,14 @@ type PreviewGenerator struct {
|
|||
VideoFilename string
|
||||
ImageFilename string
|
||||
OutputDirectory string
|
||||
|
||||
GenerateVideo bool
|
||||
GenerateImage bool
|
||||
|
||||
PreviewPreset string
|
||||
}
|
||||
|
||||
func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, imageFilename string, outputDirectory string) (*PreviewGenerator, error) {
|
||||
func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, imageFilename string, outputDirectory string, generateVideo bool, generateImage bool, previewPreset string) (*PreviewGenerator, error) {
|
||||
exists, err := utils.FileExists(videoFile.Path)
|
||||
if !exists {
|
||||
return nil, err
|
||||
|
|
@ -37,6 +42,9 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image
|
|||
VideoFilename: videoFilename,
|
||||
ImageFilename: imageFilename,
|
||||
OutputDirectory: outputDirectory,
|
||||
GenerateVideo: generateVideo,
|
||||
GenerateImage: generateImage,
|
||||
PreviewPreset: previewPreset,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -47,11 +55,16 @@ func (g *PreviewGenerator) Generate() error {
|
|||
if err := g.generateConcatFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.generateVideo(&encoder); err != nil {
|
||||
return err
|
||||
|
||||
if g.GenerateVideo {
|
||||
if err := g.generateVideo(&encoder); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := g.generateImage(&encoder); err != nil {
|
||||
return err
|
||||
if g.GenerateImage {
|
||||
if err := g.generateImage(&encoder); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -91,7 +104,7 @@ func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
|
|||
Width: 640,
|
||||
OutputPath: chunkOutputPath,
|
||||
}
|
||||
encoder.ScenePreviewVideoChunk(g.Info.VideoFile, options)
|
||||
encoder.ScenePreviewVideoChunk(g.Info.VideoFile, options, g.PreviewPreset)
|
||||
}
|
||||
|
||||
videoOutputPath := filepath.Join(g.OutputDirectory, g.VideoFilename)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ func (jp *jsonUtils) saveStudio(checksum string, studio *jsonschema.Studio) erro
|
|||
return jsonschema.SaveStudioFile(instance.Paths.JSON.StudioJSONPath(checksum), studio)
|
||||
}
|
||||
|
||||
func (jp *jsonUtils) getMovie(checksum string) (*jsonschema.Movie, error) {
|
||||
return jsonschema.LoadMovieFile(instance.Paths.JSON.MovieJSONPath(checksum))
|
||||
}
|
||||
|
||||
func (jp *jsonUtils) saveMovie(checksum string, movie *jsonschema.Movie) error {
|
||||
return jsonschema.SaveMovieFile(instance.Paths.JSON.MovieJSONPath(checksum), movie)
|
||||
}
|
||||
|
||||
func (jp *jsonUtils) getScene(checksum string) (*jsonschema.Scene, error) {
|
||||
return jsonschema.LoadSceneFile(instance.Paths.JSON.SceneJSONPath(checksum))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/json-iterator/go"
|
||||
"os"
|
||||
)
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ type PathMapping struct {
|
|||
type Mappings struct {
|
||||
Performers []NameMapping `json:"performers"`
|
||||
Studios []NameMapping `json:"studios"`
|
||||
Movies []NameMapping `json:"movies"`
|
||||
Galleries []PathMapping `json:"galleries"`
|
||||
Scenes []PathMapping `json:"scenes"`
|
||||
}
|
||||
|
|
@ -30,6 +31,7 @@ func LoadMappingsFile(filePath string) (*Mappings, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
jsonParser := json.NewDecoder(file)
|
||||
err = jsonParser.Decode(&mappings)
|
||||
if err != nil {
|
||||
|
|
|
|||
47
pkg/manager/jsonschema/movie.go
Normal file
47
pkg/manager/jsonschema/movie.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/json-iterator/go"
|
||||
"os"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type Movie struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Aliases string `json:"aliases,omitempty"`
|
||||
Duration int `json:"duration,omitempty"`
|
||||
Date string `json:"date,omitempty"`
|
||||
Rating int `json:"rating,omitempty"`
|
||||
Director string `json:"director,omitempty"`
|
||||
Synopsis string `json:"sypnopsis,omitempty"`
|
||||
FrontImage string `json:"front_image,omitempty"`
|
||||
BackImage string `json:"back_image,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
||||
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
func LoadMovieFile(filePath string) (*Movie, error) {
|
||||
var movie Movie
|
||||
file, err := os.Open(filePath)
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
jsonParser := json.NewDecoder(file)
|
||||
err = jsonParser.Decode(&movie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &movie, nil
|
||||
}
|
||||
|
||||
func SaveMovieFile(filePath string, movie *Movie) error {
|
||||
if movie == nil {
|
||||
return fmt.Errorf("movie must not be nil")
|
||||
}
|
||||
return marshalToFile(filePath, movie)
|
||||
}
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/json-iterator/go"
|
||||
"os"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type Performer struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Gender string `json:"gender,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Twitter string `json:"twitter,omitempty"`
|
||||
Instagram string `json:"instagram,omitempty"`
|
||||
|
|
@ -36,6 +38,7 @@ func LoadPerformerFile(filePath string) (*Performer, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
jsonParser := json.NewDecoder(file)
|
||||
err = jsonParser.Decode(&performer)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/json-iterator/go"
|
||||
"os"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type SceneMarker struct {
|
||||
|
|
@ -21,21 +22,29 @@ type SceneFile struct {
|
|||
Duration string `json:"duration"`
|
||||
VideoCodec string `json:"video_codec"`
|
||||
AudioCodec string `json:"audio_codec"`
|
||||
Format string `json:"format"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Framerate string `json:"framerate"`
|
||||
Bitrate int `json:"bitrate"`
|
||||
}
|
||||
|
||||
type SceneMovie struct {
|
||||
MovieName string `json:"movieName,omitempty"`
|
||||
SceneIndex int `json:"scene_index,omitempty"`
|
||||
}
|
||||
|
||||
type Scene struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Studio string `json:"studio,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Date string `json:"date,omitempty"`
|
||||
Rating int `json:"rating,omitempty"`
|
||||
OCounter int `json:"o_counter,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Gallery string `json:"gallery,omitempty"`
|
||||
Performers []string `json:"performers,omitempty"`
|
||||
Movies []SceneMovie `json:"movies,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Markers []SceneMarker `json:"markers,omitempty"`
|
||||
File *SceneFile `json:"file,omitempty"`
|
||||
|
|
@ -51,6 +60,7 @@ func LoadSceneFile(filePath string) (*Scene, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
jsonParser := json.NewDecoder(file)
|
||||
err = jsonParser.Decode(&scene)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/json-iterator/go"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"os"
|
||||
)
|
||||
|
|
@ -31,6 +31,7 @@ func LoadScrapedFile(filePath string) ([]ScrapedItem, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
jsonParser := json.NewDecoder(file)
|
||||
err = jsonParser.Decode(&scraped)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/json-iterator/go"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"os"
|
||||
)
|
||||
|
|
@ -22,6 +22,7 @@ func LoadStudioFile(filePath string) (*Studio, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
jsonParser := json.NewDecoder(file)
|
||||
err = jsonParser.Decode(&studio)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ package jsonschema
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/json-iterator/go"
|
||||
|
||||
"io/ioutil"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -25,6 +26,7 @@ func marshalToFile(filePath string, j interface{}) error {
|
|||
|
||||
func encode(j interface{}) ([]byte, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent("", " ")
|
||||
|
|
|
|||
|
|
@ -154,9 +154,6 @@ func (s *singleton) RefreshConfig() {
|
|||
_ = utils.EnsureDir(s.Paths.Generated.Markers)
|
||||
_ = utils.EnsureDir(s.Paths.Generated.Transcodes)
|
||||
|
||||
_ = utils.EnsureDir(s.Paths.JSON.Performers)
|
||||
_ = utils.EnsureDir(s.Paths.JSON.Scenes)
|
||||
_ = utils.EnsureDir(s.Paths.JSON.Galleries)
|
||||
_ = utils.EnsureDir(s.Paths.JSON.Studios)
|
||||
paths.EnsureJSONDirs()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,44 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bmatcuk/doublestar"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var extensionsToScan = []string{"zip", "m4v", "mp4", "mov", "wmv", "avi", "mpg", "mpeg", "rmvb", "rm", "flv", "asf", "mkv", "webm"}
|
||||
var extensionsGallery = []string{"zip"}
|
||||
|
||||
func constructGlob() string { // create a sequence for glob doublestar from our extensions
|
||||
extLen := len(extensionsToScan)
|
||||
glb := "{"
|
||||
for i := 0; i < extLen-1; i++ { // append extensions and commas
|
||||
glb += extensionsToScan[i] + ","
|
||||
}
|
||||
if extLen >= 1 { // append last extension without comma
|
||||
glb += extensionsToScan[extLen-1]
|
||||
}
|
||||
glb += "}"
|
||||
return glb
|
||||
|
||||
}
|
||||
|
||||
func isGallery(pathname string) bool {
|
||||
for _, ext := range extensionsGallery {
|
||||
if filepath.Ext(pathname) == "."+ext {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type TaskStatus struct {
|
||||
Status JobStatus
|
||||
Progress float64
|
||||
|
|
@ -67,7 +94,7 @@ func (s *singleton) Scan(useFileMetadata bool) {
|
|||
|
||||
var results []string
|
||||
for _, path := range config.GetStashPaths() {
|
||||
globPath := filepath.Join(path, "**/*.{zip,m4v,mp4,mov,wmv,avi,mpg,mpeg,rmvb,rm,flv,asf,mkv,webm}") // TODO: Make this configurable
|
||||
globPath := filepath.Join(path, "**/*."+constructGlob())
|
||||
globResults, _ := doublestar.Glob(globPath)
|
||||
results = append(results, globResults...)
|
||||
}
|
||||
|
|
@ -96,6 +123,15 @@ func (s *singleton) Scan(useFileMetadata bool) {
|
|||
}
|
||||
|
||||
logger.Info("Finished scan")
|
||||
for _, path := range results {
|
||||
if isGallery(path) {
|
||||
wg.Add(1)
|
||||
task := ScanTask{FilePath: path, UseFileMetadata: false}
|
||||
go task.associateGallery(&wg)
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
logger.Info("Finished gallery association")
|
||||
}()
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +171,7 @@ func (s *singleton) Export() {
|
|||
}()
|
||||
}
|
||||
|
||||
func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcodes bool) {
|
||||
func (s *singleton) Generate(sprites bool, previews bool, previewPreset *models.PreviewPreset, imagePreviews bool, markers bool, transcodes bool, thumbnails bool) {
|
||||
if s.Status.Status != Idle {
|
||||
return
|
||||
}
|
||||
|
|
@ -143,13 +179,21 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
|||
s.Status.indefiniteProgress()
|
||||
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
qg := models.NewGalleryQueryBuilder()
|
||||
//this.job.total = await ObjectionUtils.getCount(Scene);
|
||||
instance.Paths.Generated.EnsureTmpDir()
|
||||
|
||||
preset := string(models.PreviewPresetSlow)
|
||||
if previewPreset != nil && previewPreset.IsValid() {
|
||||
preset = string(*previewPreset)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer s.returnToIdleState()
|
||||
|
||||
scenes, err := qb.All()
|
||||
var galleries []*models.Gallery
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("failed to get scenes for generate")
|
||||
return
|
||||
|
|
@ -158,18 +202,27 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
|||
delta := utils.Btoi(sprites) + utils.Btoi(previews) + utils.Btoi(markers) + utils.Btoi(transcodes)
|
||||
var wg sync.WaitGroup
|
||||
s.Status.Progress = 0
|
||||
total := len(scenes)
|
||||
lenScenes := len(scenes)
|
||||
total := lenScenes
|
||||
if thumbnails {
|
||||
galleries, err = qg.All()
|
||||
if err != nil {
|
||||
logger.Errorf("failed to get galleries for generate")
|
||||
return
|
||||
}
|
||||
total += len(galleries)
|
||||
}
|
||||
|
||||
if s.Status.stopping {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
}
|
||||
totalsNeeded := s.neededGenerate(scenes, sprites, previews, markers, transcodes)
|
||||
totalsNeeded := s.neededGenerate(scenes, sprites, previews, imagePreviews, markers, transcodes)
|
||||
if totalsNeeded == nil {
|
||||
logger.Infof("Taking too long to count content. Skipping...")
|
||||
logger.Infof("Generating content")
|
||||
} else {
|
||||
logger.Infof("Generating %d sprites %d previews %d markers %d transcodes", totalsNeeded.sprites, totalsNeeded.previews, totalsNeeded.markers, totalsNeeded.transcodes)
|
||||
logger.Infof("Generating %d sprites %d previews %d image previews %d markers %d transcodes", totalsNeeded.sprites, totalsNeeded.previews, totalsNeeded.imagePreviews, totalsNeeded.markers, totalsNeeded.transcodes)
|
||||
}
|
||||
for i, scene := range scenes {
|
||||
s.Status.setProgress(i, total)
|
||||
|
|
@ -196,7 +249,7 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
|||
}
|
||||
|
||||
if previews {
|
||||
task := GeneratePreviewTask{Scene: *scene}
|
||||
task := GeneratePreviewTask{Scene: *scene, ImagePreview: imagePreviews, PreviewPreset: preset}
|
||||
go task.Start(&wg)
|
||||
}
|
||||
|
||||
|
|
@ -212,6 +265,77 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
|||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
if thumbnails {
|
||||
logger.Infof("Generating thumbnails for the galleries")
|
||||
for i, gallery := range galleries {
|
||||
s.Status.setProgress(lenScenes+i, total)
|
||||
if s.Status.stopping {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
logger.Errorf("nil gallery, skipping generate")
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
task := GenerateGthumbsTask{Gallery: *gallery}
|
||||
go task.Start(&wg)
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("Generate finished")
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *singleton) GenerateDefaultScreenshot(sceneId string) {
|
||||
s.generateScreenshot(sceneId, nil)
|
||||
}
|
||||
|
||||
func (s *singleton) GenerateScreenshot(sceneId string, at float64) {
|
||||
s.generateScreenshot(sceneId, &at)
|
||||
}
|
||||
|
||||
// generate default screenshot if at is nil
|
||||
func (s *singleton) generateScreenshot(sceneId string, at *float64) {
|
||||
if s.Status.Status != Idle {
|
||||
return
|
||||
}
|
||||
s.Status.SetStatus(Generate)
|
||||
s.Status.indefiniteProgress()
|
||||
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
instance.Paths.Generated.EnsureTmpDir()
|
||||
|
||||
go func() {
|
||||
defer s.returnToIdleState()
|
||||
|
||||
sceneIdInt, err := strconv.Atoi(sceneId)
|
||||
if err != nil {
|
||||
logger.Errorf("Error parsing scene id %s: %s", sceneId, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
scene, err := qb.Find(sceneIdInt)
|
||||
if err != nil || scene == nil {
|
||||
logger.Errorf("failed to get scene for generate")
|
||||
return
|
||||
}
|
||||
|
||||
task := GenerateScreenshotTask{
|
||||
Scene: *scene,
|
||||
ScreenshotAt: at,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go task.Start(&wg)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
logger.Infof("Generate finished")
|
||||
}()
|
||||
}
|
||||
|
|
@ -390,6 +514,7 @@ func (s *singleton) Clean() {
|
|||
s.Status.indefiniteProgress()
|
||||
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
gqb := models.NewGalleryQueryBuilder()
|
||||
go func() {
|
||||
defer s.returnToIdleState()
|
||||
|
||||
|
|
@ -400,6 +525,12 @@ func (s *singleton) Clean() {
|
|||
return
|
||||
}
|
||||
|
||||
galleries, err := gqb.All()
|
||||
if err != nil {
|
||||
logger.Errorf("failed to fetch list of galleries for cleaning")
|
||||
return
|
||||
}
|
||||
|
||||
if s.Status.stopping {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
|
|
@ -407,7 +538,7 @@ func (s *singleton) Clean() {
|
|||
|
||||
var wg sync.WaitGroup
|
||||
s.Status.Progress = 0
|
||||
total := len(scenes)
|
||||
total := len(scenes) + len(galleries)
|
||||
for i, scene := range scenes {
|
||||
s.Status.setProgress(i, total)
|
||||
if s.Status.stopping {
|
||||
|
|
@ -422,7 +553,26 @@ func (s *singleton) Clean() {
|
|||
|
||||
wg.Add(1)
|
||||
|
||||
task := CleanTask{Scene: *scene}
|
||||
task := CleanTask{Scene: scene}
|
||||
go task.Start(&wg)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
for i, gallery := range galleries {
|
||||
s.Status.setProgress(len(scenes)+i, total)
|
||||
if s.Status.stopping {
|
||||
logger.Info("Stopping due to user request")
|
||||
return
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
logger.Errorf("nil gallery, skipping Clean")
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
task := CleanTask{Gallery: gallery}
|
||||
go task.Start(&wg)
|
||||
wg.Wait()
|
||||
}
|
||||
|
|
@ -445,7 +595,7 @@ func (s *singleton) returnToIdleState() {
|
|||
}
|
||||
|
||||
func (s *singleton) neededScan(paths []string) int64 {
|
||||
var neededScans int64 = 0
|
||||
var neededScans int64
|
||||
|
||||
for _, path := range paths {
|
||||
task := ScanTask{FilePath: path}
|
||||
|
|
@ -457,23 +607,24 @@ func (s *singleton) neededScan(paths []string) int64 {
|
|||
}
|
||||
|
||||
type totalsGenerate struct {
|
||||
sprites int64
|
||||
previews int64
|
||||
markers int64
|
||||
transcodes int64
|
||||
sprites int64
|
||||
previews int64
|
||||
imagePreviews int64
|
||||
markers int64
|
||||
transcodes int64
|
||||
}
|
||||
|
||||
func (s *singleton) neededGenerate(scenes []*models.Scene, sprites, previews, markers, transcodes bool) *totalsGenerate {
|
||||
func (s *singleton) neededGenerate(scenes []*models.Scene, sprites, previews, imagePreviews, markers, transcodes bool) *totalsGenerate {
|
||||
|
||||
var totals totalsGenerate
|
||||
const timeoutSecs = 90 * time.Second
|
||||
const timeout = 90 * time.Second
|
||||
|
||||
// create a control channel through which to signal the counting loop when the timeout is reached
|
||||
chTimeout := make(chan struct{})
|
||||
|
||||
//run the timeout function in a separate thread
|
||||
go func() {
|
||||
time.Sleep(timeoutSecs)
|
||||
time.Sleep(timeout)
|
||||
chTimeout <- struct{}{}
|
||||
}()
|
||||
|
||||
|
|
@ -488,10 +639,13 @@ func (s *singleton) neededGenerate(scenes []*models.Scene, sprites, previews, ma
|
|||
}
|
||||
|
||||
if previews {
|
||||
task := GeneratePreviewTask{Scene: *scene}
|
||||
if !task.doesPreviewExist(task.Scene.Checksum) {
|
||||
task := GeneratePreviewTask{Scene: *scene, ImagePreview: imagePreviews}
|
||||
if !task.doesVideoPreviewExist(task.Scene.Checksum) {
|
||||
totals.previews++
|
||||
}
|
||||
if imagePreviews && !task.doesImagePreviewExist(task.Scene.Checksum) {
|
||||
totals.imagePreviews++
|
||||
}
|
||||
}
|
||||
|
||||
if markers {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type galleryPaths struct{}
|
||||
|
||||
const thumbDir = "gthumbs"
|
||||
const thumbDirDepth int = 2
|
||||
const thumbDirLength int = 2 // thumbDirDepth * thumbDirLength must be smaller than the length of checksum
|
||||
|
||||
func newGalleryPaths() *galleryPaths {
|
||||
return &galleryPaths{}
|
||||
}
|
||||
|
|
@ -15,6 +21,19 @@ func (gp *galleryPaths) GetExtractedPath(checksum string) string {
|
|||
return filepath.Join(config.GetCachePath(), checksum)
|
||||
}
|
||||
|
||||
func GetGthumbCache() string {
|
||||
return filepath.Join(config.GetCachePath(), thumbDir)
|
||||
}
|
||||
|
||||
func GetGthumbDir(checksum string) string {
|
||||
return filepath.Join(config.GetCachePath(), thumbDir, utils.GetIntraDir(checksum, thumbDirDepth, thumbDirLength), checksum)
|
||||
}
|
||||
|
||||
func GetGthumbPath(checksum string, index int, width int) string {
|
||||
fname := fmt.Sprintf("%s_%d_%d.jpg", checksum, index, width)
|
||||
return filepath.Join(config.GetCachePath(), thumbDir, utils.GetIntraDir(checksum, thumbDirDepth, thumbDirLength), checksum, fname)
|
||||
}
|
||||
|
||||
func (gp *galleryPaths) GetExtractedFilePath(checksum string, fileName string) string {
|
||||
return filepath.Join(config.GetCachePath(), checksum, fileName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ package paths
|
|||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type jsonPaths struct {
|
||||
Metadata string
|
||||
|
||||
MappingsFile string
|
||||
ScrapedFile string
|
||||
|
||||
|
|
@ -13,19 +16,37 @@ type jsonPaths struct {
|
|||
Scenes string
|
||||
Galleries string
|
||||
Studios string
|
||||
Movies string
|
||||
}
|
||||
|
||||
func newJSONPaths() *jsonPaths {
|
||||
jp := jsonPaths{}
|
||||
jp.Metadata = config.GetMetadataPath()
|
||||
jp.MappingsFile = filepath.Join(config.GetMetadataPath(), "mappings.json")
|
||||
jp.ScrapedFile = filepath.Join(config.GetMetadataPath(), "scraped.json")
|
||||
jp.Performers = filepath.Join(config.GetMetadataPath(), "performers")
|
||||
jp.Scenes = filepath.Join(config.GetMetadataPath(), "scenes")
|
||||
jp.Galleries = filepath.Join(config.GetMetadataPath(), "galleries")
|
||||
jp.Studios = filepath.Join(config.GetMetadataPath(), "studios")
|
||||
jp.Movies = filepath.Join(config.GetMetadataPath(), "movies")
|
||||
return &jp
|
||||
}
|
||||
|
||||
func GetJSONPaths() *jsonPaths {
|
||||
jp := newJSONPaths()
|
||||
return jp
|
||||
}
|
||||
|
||||
func EnsureJSONDirs() {
|
||||
jsonPaths := GetJSONPaths()
|
||||
utils.EnsureDir(jsonPaths.Metadata)
|
||||
utils.EnsureDir(jsonPaths.Scenes)
|
||||
utils.EnsureDir(jsonPaths.Galleries)
|
||||
utils.EnsureDir(jsonPaths.Performers)
|
||||
utils.EnsureDir(jsonPaths.Studios)
|
||||
utils.EnsureDir(jsonPaths.Movies)
|
||||
}
|
||||
|
||||
func (jp *jsonPaths) PerformerJSONPath(checksum string) string {
|
||||
return filepath.Join(jp.Performers, checksum+".json")
|
||||
}
|
||||
|
|
@ -37,3 +58,7 @@ func (jp *jsonPaths) SceneJSONPath(checksum string) string {
|
|||
func (jp *jsonPaths) StudioJSONPath(checksum string) string {
|
||||
return filepath.Join(jp.Studios, checksum+".json")
|
||||
}
|
||||
|
||||
func (jp *jsonPaths) MovieJSONPath(checksum string) string {
|
||||
return filepath.Join(jp.Movies, checksum+".json")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,27 @@ func DeleteGeneratedSceneFiles(scene *models.Scene) {
|
|||
}
|
||||
}
|
||||
|
||||
func DeleteSceneMarkerFiles(scene *models.Scene, seconds int) {
|
||||
videoPath := GetInstance().Paths.SceneMarkers.GetStreamPath(scene.Checksum, seconds)
|
||||
imagePath := GetInstance().Paths.SceneMarkers.GetStreamPreviewImagePath(scene.Checksum, seconds)
|
||||
|
||||
exists, _ := utils.FileExists(videoPath)
|
||||
if exists {
|
||||
err := os.Remove(videoPath)
|
||||
if err != nil {
|
||||
logger.Warnf("Could not delete file %s: %s", videoPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
exists, _ = utils.FileExists(imagePath)
|
||||
if exists {
|
||||
err := os.Remove(imagePath)
|
||||
if err != nil {
|
||||
logger.Warnf("Could not delete file %s: %s", videoPath, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteSceneFile(scene *models.Scene) {
|
||||
// kill any running encoders
|
||||
KillRunningStreams(scene.Path)
|
||||
|
|
|
|||
16
pkg/manager/screenshot.go
Normal file
16
pkg/manager/screenshot.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
)
|
||||
|
||||
func makeScreenshot(probeResult ffmpeg.VideoFile, outputPath string, quality int, width int, time float64) {
|
||||
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
|
||||
options := ffmpeg.ScreenshotOptions{
|
||||
OutputPath: outputPath,
|
||||
Quality: quality,
|
||||
Time: time,
|
||||
Width: width,
|
||||
}
|
||||
encoder.Screenshot(probeResult, options)
|
||||
}
|
||||
|
|
@ -24,12 +24,10 @@ func (t *AutoTagPerformerTask) Start(wg *sync.WaitGroup) {
|
|||
func getQueryRegex(name string) string {
|
||||
const separatorChars = `.\-_ `
|
||||
// handle path separators
|
||||
const endSeparatorChars = separatorChars + `\\/`
|
||||
const separator = `[` + separatorChars + `]`
|
||||
const endSeparator = `[` + endSeparatorChars + `]`
|
||||
|
||||
ret := strings.Replace(name, " ", separator+"*", -1)
|
||||
ret = "(?:^|" + endSeparator + "+)" + ret + "(?:$|" + endSeparator + "+)"
|
||||
ret = `(?:^|_|[^\w\d])` + ret + `(?:$|_|[^\w\d])`
|
||||
return ret
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,15 @@ var testSeparators = []string{
|
|||
" ",
|
||||
}
|
||||
|
||||
func generateNamePatterns(name string, separator string) []string {
|
||||
var testEndSeparators = []string{
|
||||
"{",
|
||||
"}",
|
||||
"(",
|
||||
")",
|
||||
",",
|
||||
}
|
||||
|
||||
func generateNamePatterns(name, separator string) []string {
|
||||
var ret []string
|
||||
ret = append(ret, fmt.Sprintf("%s%saaa"+testExtension, name, separator))
|
||||
ret = append(ret, fmt.Sprintf("aaa%s%s"+testExtension, separator, name))
|
||||
|
|
@ -152,13 +160,20 @@ func createScenes(tx *sqlx.Tx) error {
|
|||
// create the scenes
|
||||
var scenePatterns []string
|
||||
var falseScenePatterns []string
|
||||
for _, separator := range testSeparators {
|
||||
|
||||
separators := append(testSeparators, testEndSeparators...)
|
||||
|
||||
for _, separator := range separators {
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(testName, separator)...)
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ToLower(testName), separator)...)
|
||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePattern(testName, separator))
|
||||
}
|
||||
|
||||
// add test cases for intra-name separators
|
||||
for _, separator := range testSeparators {
|
||||
if separator != " " {
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.Replace(testName, " ", separator, -1), separator)...)
|
||||
}
|
||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePattern(testName, separator))
|
||||
}
|
||||
|
||||
for _, fn := range scenePatterns {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue