mirror of
https://github.com/stashapp/stash.git
synced 2026-06-10 21:42:03 -05:00
Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3acb21d4e1 | ||
|
|
e0623eb302 | ||
|
|
5a37e6cf52 | ||
|
|
3f97b3a1cb | ||
|
|
81cf3d3337 | ||
|
|
bdac352250 | ||
|
|
31981d4116 | ||
|
|
08c294414d | ||
|
|
2ab42e9cd3 | ||
|
|
896c3874af | ||
|
|
a3609079bb | ||
|
|
2c52fd711b | ||
|
|
d7a04ced00 | ||
|
|
3f0c965400 | ||
|
|
4a04dfe4a2 | ||
|
|
597576f5e6 | ||
|
|
502d99de1b | ||
|
|
4d13e8d7f7 | ||
|
|
210feb4034 | ||
|
|
fe0c5615a6 | ||
|
|
70b66d91a0 | ||
|
|
eefc628cf0 | ||
|
|
aedadc3857 | ||
|
|
2eb2d865dc | ||
|
|
f66010a367 | ||
|
|
7836a37d6e | ||
|
|
bf3f658091 | ||
|
|
1767390e0d | ||
|
|
79a180ba73 | ||
|
|
8705f78591 | ||
|
|
39512e1452 | ||
|
|
9200f167bf | ||
|
|
1759a99f65 | ||
|
|
cd0a9a1d62 | ||
|
|
e3fa8f7b24 | ||
|
|
a5e9e7abce | ||
|
|
d673c4ce03 | ||
|
|
cd6b6b74eb | ||
|
|
0b40017b09 | ||
|
|
e59018acfb | ||
|
|
ea54a67798 | ||
|
|
e6aaa196f3 | ||
|
|
34f114faff | ||
|
|
f443223d16 | ||
|
|
6a4421f8e1 | ||
|
|
f5dc654f6b | ||
|
|
f6ffda7504 | ||
|
|
c38660d209 | ||
|
|
a2582047ca | ||
|
|
6a0c73b3a1 | ||
|
|
d042ec42ee | ||
|
|
25311247ed | ||
|
|
60af076fff | ||
|
|
4462b3cc8e | ||
|
|
2edcdeaeb9 | ||
|
|
d8ba4a08c0 | ||
|
|
72b027a887 | ||
|
|
7671465334 | ||
|
|
2c1300cae0 | ||
|
|
35718ce59a | ||
|
|
1412b554a0 | ||
|
|
2c2e56d33a | ||
|
|
ccb96c3795 | ||
|
|
d5e9030768 | ||
|
|
496900df42 | ||
|
|
7acae34ed4 | ||
|
|
d30a68567e | ||
|
|
8a3d2e8e06 | ||
|
|
cad96b7872 | ||
|
|
de538be79c | ||
|
|
4299f113e0 | ||
|
|
b39fe3ed2b | ||
|
|
68d4a4fe42 | ||
|
|
73a8bad1bc | ||
|
|
960f843259 | ||
|
|
d93011a828 | ||
|
|
215737d6c5 | ||
|
|
6369a500b3 | ||
|
|
58243cded0 | ||
|
|
7e6127975d | ||
|
|
f7cd9cb00d | ||
|
|
ecac7a8013 | ||
|
|
a619b9dd48 | ||
|
|
b63e8ef929 | ||
|
|
23d85655a8 | ||
|
|
53cb9a1b7b | ||
|
|
a3a531d122 | ||
|
|
55aee21cff | ||
|
|
b3966b3c76 | ||
|
|
b647a75151 | ||
|
|
baeeb2d649 | ||
|
|
f794c6ae45 | ||
|
|
a0676d5c30 | ||
|
|
698e21a04f | ||
|
|
9d1b716f48 | ||
|
|
c2c06d8f8d | ||
|
|
e5c5cde974 | ||
|
|
16da483674 | ||
|
|
bde5d07afb | ||
|
|
1850a2b533 | ||
|
|
117e6326db | ||
|
|
fe990e00c1 | ||
|
|
4825de7d35 | ||
|
|
7cfff46d02 | ||
|
|
44ea777019 | ||
|
|
711496e9f4 | ||
|
|
01da28010d | ||
|
|
7e0db2aad4 | ||
|
|
144cd6e4f2 | ||
|
|
a9ac176e91 | ||
|
|
af6b21a428 | ||
|
|
8ec25ef161 | ||
|
|
777956f0ab | ||
|
|
acbdee76de | ||
|
|
14230d7b52 | ||
|
|
f7a8899d90 | ||
|
|
7fbb92d071 | ||
|
|
2ec595dedf | ||
|
|
f9b2a4be5e | ||
|
|
ac117c2934 | ||
|
|
dd03c63da2 | ||
|
|
3f9b395b89 | ||
|
|
deebeab9e8 | ||
|
|
647ae2d7f9 | ||
|
|
758eccc659 | ||
|
|
bbc34bd1bf | ||
|
|
7609969491 | ||
|
|
8d8a8530e8 | ||
|
|
bcbbd1474c | ||
|
|
984a0c9247 | ||
|
|
714ae541d4 | ||
|
|
9da11603c2 | ||
|
|
918b739b6c | ||
|
|
25b600f768 | ||
|
|
0dd2e269ee | ||
|
|
6d48cd1c97 | ||
|
|
4e9ebe055b | ||
|
|
e4d91a0226 | ||
|
|
4fd022a93b | ||
|
|
86bfb64a0d | ||
|
|
6114caa938 | ||
|
|
23d2668b38 | ||
|
|
89fcd6d775 | ||
|
|
df8675c2e7 | ||
|
|
203fc3e4b2 | ||
|
|
0d7663c13d | ||
|
|
d1274d559d | ||
|
|
088f32a116 | ||
|
|
3b41894dbd | ||
|
|
093b997eb1 | ||
|
|
1e04deb3d4 | ||
|
|
7bae990c67 | ||
|
|
defb23aaa2 | ||
|
|
aad4ddc46d | ||
|
|
8a3d940aa7 | ||
|
|
3d83fa449d | ||
|
|
0a123548a0 | ||
|
|
03a9d65cfe | ||
|
|
1882b44951 | ||
|
|
4fc0d47087 | ||
|
|
6b1d229a6d | ||
|
|
d84d48bc29 | ||
|
|
d50238cf41 | ||
|
|
5c10712aab | ||
|
|
232a69c518 | ||
|
|
c8bcaaf27d | ||
|
|
e84c92355e | ||
|
|
e883e5fe27 | ||
|
|
dd2086a912 | ||
|
|
aadbcaeec2 | ||
|
|
99bd7bc750 | ||
|
|
f264baa330 | ||
|
|
931d3a0974 | ||
|
|
4a08bd351a | ||
|
|
fad64ba126 | ||
|
|
938559ca11 | ||
|
|
86747acc78 | ||
|
|
6eea33aec9 | ||
|
|
c45780dc59 | ||
|
|
a96ab9ce6f | ||
|
|
aecbd236bc | ||
|
|
a7d333786f | ||
|
|
a45c1111be | ||
|
|
54c9f167ba | ||
|
|
1d910419d1 | ||
|
|
89277f1e25 | ||
|
|
7d37e3e564 | ||
|
|
df37ddcc2c | ||
|
|
794ea00d37 | ||
|
|
e3eb550a7d | ||
|
|
502e9297ad | ||
|
|
56b47ff9be | ||
|
|
93068e1ded |
144
.github/workflows/build.yml
vendored
Normal file
144
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
pull_request:
|
||||
branches: [ develop ]
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:4
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Checkout
|
||||
run: git fetch --prune --unshallow --tags
|
||||
|
||||
- name: Pull compiler image
|
||||
run: docker pull $COMPILER_IMAGE
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node_modules
|
||||
with:
|
||||
path: ui/v2.5/node_modules
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock') }}
|
||||
|
||||
- name: Pre-install
|
||||
run: docker run --rm --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated -w /stash $COMPILER_IMAGE /bin/bash -c "make pre-ui"
|
||||
|
||||
- name: Generate
|
||||
run: docker run --rm --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated -w /stash $COMPILER_IMAGE /bin/bash -c "make generate"
|
||||
|
||||
# TODO: Replace with `make validate` once `revive` is bundled in COMPILER_IMAGE
|
||||
- name: Validate
|
||||
run: docker run --rm --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated -w /stash $COMPILER_IMAGE /bin/bash -c "make ui-validate fmt-check vet it"
|
||||
|
||||
- name: Build UI
|
||||
run: docker run --rm --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated -w /stash $COMPILER_IMAGE /bin/bash -c "make ui-only"
|
||||
|
||||
- name: Compile for all supported platforms
|
||||
run: ./scripts/cross-compile.sh
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
|
||||
sha1sum dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
|
||||
echo "STASH_VERSION=$(git describe --tags --exclude latest_develop)" >> $GITHUB_ENV
|
||||
echo "RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Windows binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: stash-win.exe
|
||||
path: dist/stash-win.exe
|
||||
|
||||
- name: Upload OSX binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: stash-osx
|
||||
path: dist/stash-osx
|
||||
|
||||
- name: Upload Linux binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: stash-linux
|
||||
path: dist/stash-linux
|
||||
|
||||
- name: Update latest_develop tag
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
run : git tag -f latest_develop; git push -f --tags
|
||||
|
||||
- name: Development Release
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
uses: marvinpinto/action-automatic-releases@v1.1.2
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
prerelease: true
|
||||
automatic_release_tag: latest_develop
|
||||
title: "${{ env.STASH_VERSION }}: Latest development build"
|
||||
files: |
|
||||
dist/stash-osx
|
||||
dist/stash-win.exe
|
||||
dist/stash-linux
|
||||
dist/stash-linux-arm64v8
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-pi
|
||||
CHECKSUMS_SHA1
|
||||
|
||||
- name: Master release
|
||||
if: ${{ github.event_name == 'release' && github.ref != 'refs/tags/latest_develop' }}
|
||||
uses: meeDamian/github-release@2.0
|
||||
with:
|
||||
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
allow_override: true
|
||||
files: |
|
||||
dist/stash-osx
|
||||
dist/stash-win.exe
|
||||
dist/stash-linux
|
||||
dist/stash-linux-arm64v8
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-pi
|
||||
CHECKSUMS_SHA1
|
||||
gzip: false
|
||||
|
||||
- name: Development Docker
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: |
|
||||
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
|
||||
docker info
|
||||
docker buildx create --name builder --use
|
||||
docker buildx inspect --bootstrap
|
||||
docker buildx ls
|
||||
bash ./docker/ci/x86_64/docker_push.sh development
|
||||
|
||||
- name: Release Docker
|
||||
if: ${{ github.event_name == 'release' && github.ref != 'refs/tags/latest_develop' }}
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: |
|
||||
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
|
||||
docker info
|
||||
docker buildx create --name builder --use
|
||||
docker buildx inspect --bootstrap
|
||||
docker buildx ls
|
||||
bash ./docker/ci/x86_64/docker_push.sh latest
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,6 +35,7 @@ ui/v2.5/src/core/generated-*.tsx
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
.vscode
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
@@ -12,6 +12,24 @@ env:
|
||||
global:
|
||||
- GO111MODULE=on
|
||||
before_install:
|
||||
- set -e
|
||||
# Configure environment so changes are picked up when the Docker daemon is restarted after upgrading
|
||||
- echo '{"experimental":true}' | sudo tee /etc/docker/daemon.json
|
||||
- export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
# Upgrade to Docker CE 19.03 for BuildKit support
|
||||
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||
- sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
|
||||
- sudo apt-get update
|
||||
- sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
|
||||
# install binfmt docker container, this container uses qemu to run arm programs transparently allowng docker to build arm 6,7,8 containers.
|
||||
- docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
|
||||
# Show info to simplify debugging and create a builder that can build the platforms we need
|
||||
- docker info
|
||||
- docker buildx create --name builder --use
|
||||
- docker buildx inspect --bootstrap
|
||||
- docker buildx ls
|
||||
|
||||
install:
|
||||
- echo -e "machine github.com\n login $CI_USER_TOKEN" > ~/.netrc
|
||||
- nvm install 12
|
||||
- travis_retry make pre-ui
|
||||
@@ -25,6 +43,8 @@ script:
|
||||
after_success:
|
||||
- docker pull stashapp/compiler:4
|
||||
- sh ./scripts/cross-compile.sh
|
||||
- git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
|
||||
- sha1sum dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
|
||||
- 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then sh ./scripts/upload-pull-request.sh; fi'
|
||||
before_deploy:
|
||||
# push the latest tag when on the develop branch
|
||||
@@ -47,6 +67,7 @@ deploy:
|
||||
- dist/stash-linux-arm64v8
|
||||
- dist/stash-linux-arm32v7
|
||||
- dist/stash-pi
|
||||
- CHECKSUMS_SHA1
|
||||
skip_cleanup: true
|
||||
overwrite: true
|
||||
name: "${STASH_VERSION}: Latest development build"
|
||||
@@ -58,7 +79,7 @@ deploy:
|
||||
# docker image build for develop release
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: bash ./docker/ci/x86_64/docker_push.sh development-x86_64
|
||||
script: bash ./docker/ci/x86_64/docker_push.sh development
|
||||
on:
|
||||
repo: stashapp/stash
|
||||
branch: develop
|
||||
@@ -73,6 +94,7 @@ deploy:
|
||||
- dist/stash-linux-arm64v8
|
||||
- dist/stash-linux-arm32v7
|
||||
- dist/stash-pi
|
||||
- CHECKSUMS_SHA1
|
||||
# make the release a draft so the maintainers can confirm before releasing
|
||||
draft: true
|
||||
skip_cleanup: true
|
||||
74
README.md
74
README.md
@@ -6,45 +6,61 @@
|
||||
|
||||
https://stashapp.cc
|
||||
|
||||
**Stash is a Go app which organizes and serves your porn.**
|
||||
**Stash is a locally hosted web-based app written in Go which organizes and serves your porn.**
|
||||
|
||||
See a demo [here](https://vimeo.com/275537038) (password is stashapp).
|
||||
* It can gather information about videos in your collection from the internet, and is extensible through the use of community-built plugins for a large number of content producers.
|
||||
* It supports a wide variety of both video and image formats.
|
||||
* You can tag videos and find them later.
|
||||
* It provides statistics about performers, tags, studios and other things.
|
||||
|
||||
An in-app manual is available, and the manual pages can be viewed [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en).
|
||||
You can [watch a SFW demo video](https://vimeo.com/545323354) to see it in action.
|
||||
|
||||
# Docker install
|
||||
For further information you can [read the in-app manual](ui/v2.5/src/docs/en).
|
||||
|
||||
# Installing stash
|
||||
|
||||
## via Docker
|
||||
|
||||
Follow [this README.md in the docker directory.](docker/production/README.md)
|
||||
|
||||
# Bare-metal Install
|
||||
## Pre-Compiled Binaries
|
||||
|
||||
Stash supports macOS, Windows, and Linux. Download the [latest release here](https://github.com/stashapp/stash/releases).
|
||||
The Stash server runs on macOS, Windows, and Linux. Download the [latest release here](https://github.com/stashapp/stash/releases).
|
||||
|
||||
Run the executable (double click the exe on windows or run `./stash-osx` / `./stash-linux` from the terminal on macOS / Linux) and navigate to either https://localhost:9999 or http://localhost:9999 to get started.
|
||||
|
||||
*Note for Windows users:* Running the app might present a security prompt since the binary isn't signed yet. Just click more info and then the "run anyway" button.
|
||||
*Note for Windows users:* Running the app might present a security prompt since the binary isn't yet signed. Bypass this by clicking "more info" and then the "run anyway" button.
|
||||
|
||||
#### FFMPEG
|
||||
|
||||
If stash is unable to find or download FFMPEG then download it yourself from the link for your platform:
|
||||
|
||||
* [macOS](https://ffmpeg.zeranoe.com/builds/macos64/static/ffmpeg-4.0-macos64-static.zip)
|
||||
* [Windows](https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-4.0-win64-static.zip)
|
||||
* [Linux](https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz)
|
||||
* [macOS ffmpeg](https://evermeet.cx/ffmpeg/ffmpeg-4.3.1.zip), [macOS ffprobe](https://evermeet.cx/ffmpeg/ffprobe-4.3.1.zip)
|
||||
* [Windows](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip)
|
||||
* [Linux](https://www.johnvansickle.com/ffmpeg/)
|
||||
|
||||
The `ffmpeg(.exe)` and `ffprobe(.exe)` files should be placed in `~/.stash` on macOS / Linux or `C:\Users\YourUsername\.stash` on Windows.
|
||||
|
||||
# Usage
|
||||
|
||||
## Quickstart Guide
|
||||
1) Download and install Stash and its dependencies
|
||||
2) Run Stash. It will prompt you for some configuration options and a directory to index (you can also do this step afterward)
|
||||
3) After configuration, launch your web browser and navigate to the URL shown within the Stash app.
|
||||
|
||||
**Note that Stash does not currently retrieve and organize information about your entire library automatically.** You will need to help it along through the use of [scrapers](blob/develop/ui/v2.5/src/docs/en/Scraping.md). The Stash community has developed scrapers for many popular data sources which can be downloaded and installed from [this repository](https://github.com/stashapp/CommunityScrapers).
|
||||
|
||||
The simplest way to tag a large number of files is by using the [Tagger](https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/docs/en/Tagger.md) which uses filename keywords to help identify the file and pull in scene and performer information from our stash-box database. Note that this data source is not comprehensive and you may need to use the scrapers to identify some of your media.
|
||||
|
||||
## CLI
|
||||
|
||||
Stash provides some command line options. See what is currently available by running `stash --help`.
|
||||
Stash runs as a command-line app and local web server. There are some command-line options available, which you can see by running `stash --help`.
|
||||
|
||||
For example, to run stash locally on port 80 run it like this (OSX / Linux) `stash --host 127.0.0.1 --port 80`
|
||||
|
||||
## SSL (HTTPS)
|
||||
|
||||
Stash supports HTTPS with some additional work. First you must generate a SSL certificate and key combo. Here is an example using openssl:
|
||||
Stash can run over HTTPS with some additional work. First you must generate a SSL certificate and key combo. Here is an example using openssl:
|
||||
|
||||
`openssl req -x509 -newkey rsa:4096 -sha256 -days 7300 -nodes -keyout stash.key -out stash.crt -extensions san -config <(echo "[req]"; echo distinguished_name=req; echo "[san]"; echo subjectAltName=DNS:stash.server,IP:127.0.0.1) -subj /CN=stash.server`
|
||||
|
||||
@@ -52,25 +68,32 @@ This command would need customizing for your environment. [This link](https://s
|
||||
|
||||
Once you have a certificate and key file name them `stash.crt` and `stash.key` and place them in the `~/.stash` directory. Stash detects these and starts up using HTTPS rather than HTTP.
|
||||
|
||||
# FAQ
|
||||
# Customization
|
||||
|
||||
> I'm unable to run the app on OSX or Linux
|
||||
## Themes and CSS Customization
|
||||
There is a [directory of community-created themes](https://github.com/stashapp/stash/wiki/Themes) on our Wiki, along with instructions on how to install them.
|
||||
|
||||
Try running `chmod u+x stash-osx` or `chmod u+x stash-linux` to make the file executable.
|
||||
You can also 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).
|
||||
|
||||
> I have a question not answered here.
|
||||
# Support (FAQ)
|
||||
|
||||
Join the [Discord server](https://discord.gg/2TsNFKt).
|
||||
Answers to other Frequently Asked Questions can be found [on our Wiki](https://github.com/stashapp/stash/wiki/FAQ)
|
||||
|
||||
# Development
|
||||
For issues not addressed there, there are a few options.
|
||||
|
||||
## Install
|
||||
* Read the [Wiki](https://github.com/stashapp/stash/wiki)
|
||||
* Check the in-app documentation (also available [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en)
|
||||
* Join the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support.
|
||||
|
||||
# Compiling From Source Code
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
* [Go](https://golang.org/dl/)
|
||||
* [Revive](https://github.com/mgechev/revive) - Configurable linter
|
||||
* Go Install: `go get github.com/mgechev/revive`
|
||||
* [Packr2](https://github.com/gobuffalo/packr/tree/v2.0.2/v2) - Static asset bundler
|
||||
* Go Install: `go get github.com/gobuffalo/packr/v2/packr2@v2.0.2`
|
||||
* [Packr2](https://github.com/gobuffalo/packr/) - Static asset bundler
|
||||
* Go Install: `go get github.com/gobuffalo/packr/v2/packr2`
|
||||
* [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.5` folder (before running make generate for first time).
|
||||
@@ -117,15 +140,8 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
|
||||
|
||||
## Cross compiling
|
||||
|
||||
This project uses a modification of [this](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment
|
||||
This project uses a modification of the [CI-GoReleaser](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment
|
||||
where the app can be cross-compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following
|
||||
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.
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ FROM ubuntu:20.04 as app
|
||||
RUN apt-get update && apt-get -y install ca-certificates
|
||||
COPY --from=compiler /stash/stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
|
||||
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
EXPOSE 9999
|
||||
CMD ["stash"]
|
||||
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
|
||||
# must be built from /dist directory
|
||||
|
||||
FROM ubuntu:20.04 as prep
|
||||
LABEL MAINTAINER="https://discord.gg/Uz29ny"
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y install curl xz-utils && \
|
||||
apt-get autoclean -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
FROM --platform=$BUILDPLATFORM ubuntu:20.04 AS prep
|
||||
ARG TARGETPLATFORM
|
||||
WORKDIR /
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN curl --http1.1 -o /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
|
||||
tar xf /ffmpeg.tar.xz && \
|
||||
rm ffmpeg.tar.xz && \
|
||||
mv /ffmpeg*/ /ffmpeg/
|
||||
|
||||
COPY stash-* /
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-pi; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then BIN=stash-linux-arm32v7; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then BIN=stash-linux-arm64v8; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then BIN=stash-linux; \
|
||||
fi; \
|
||||
mv $BIN /stash
|
||||
FROM ubuntu:20.04 as app
|
||||
RUN apt-get update && apt-get -y install ca-certificates
|
||||
COPY --from=prep /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
|
||||
COPY /stash-linux /usr/bin/stash
|
||||
run apt update && apt install -y python3 python3 python-is-python3 python3-requests ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=prep /stash /usr/bin/
|
||||
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
EXPOSE 9999
|
||||
CMD ["stash"]
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
DOCKER_TAG=$1
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
|
||||
# must build the image from dist directory
|
||||
docker build -t stashapp/stash:$DOCKER_TAG -f ./docker/ci/x86_64/Dockerfile ./dist
|
||||
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push --output type=image,name=stashapp/stash:$DOCKER_TAG,push=true -f docker/ci/x86_64/Dockerfile dist/
|
||||
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
docker push stashapp/stash:$DOCKER_TAG
|
||||
|
||||
@@ -20,5 +20,8 @@ RUN curl --http1.1 -o /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/f
|
||||
FROM ubuntu:20.04 as app
|
||||
RUN apt-get update && apt-get -y install ca-certificates
|
||||
COPY --from=prep /stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
|
||||
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
EXPOSE 9999
|
||||
CMD ["stash"]
|
||||
|
||||
@@ -20,5 +20,8 @@ RUN curl --http1.1 -o /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/f
|
||||
FROM ubuntu:20.04 as app
|
||||
RUN apt-get update && apt-get -y install ca-certificates
|
||||
COPY --from=prep /stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
|
||||
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
EXPOSE 9999
|
||||
CMD ["stash"]
|
||||
|
||||
10
go.mod
10
go.mod
@@ -4,10 +4,12 @@ require (
|
||||
github.com/99designs/gqlgen v0.12.2
|
||||
github.com/Yamashou/gqlgenc v0.0.0-20200902035953-4dbef3551953
|
||||
github.com/antchfx/htmlquery v1.2.3
|
||||
github.com/bmatcuk/doublestar/v2 v2.0.1
|
||||
github.com/chromedp/cdproto v0.0.0-20200608134039-8a80cdaf865c
|
||||
github.com/chromedp/chromedp v0.5.3
|
||||
github.com/corona10/goimagehash v1.0.3
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.0
|
||||
github.com/fvbommel/sortorder v1.0.2
|
||||
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
|
||||
@@ -18,8 +20,9 @@ require (
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
|
||||
github.com/jmoiron/sqlx v1.2.0
|
||||
github.com/json-iterator/go v1.1.9
|
||||
github.com/mattn/go-sqlite3 v1.13.0
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/rs/cors v1.6.0
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
@@ -30,8 +33,9 @@ require (
|
||||
github.com/vektah/gqlparser/v2 v2.0.1
|
||||
github.com/vektra/mockery/v2 v2.2.1
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd
|
||||
golang.org/x/tools v0.0.0-20200915031644-64986481280e // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
)
|
||||
|
||||
17
go.sum
17
go.sum
@@ -62,8 +62,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/bmatcuk/doublestar/v2 v2.0.1 h1:EFT91DmIMRcrUEcYUW7AqSAwKvNzP5+CoDmNVBbcQOU=
|
||||
github.com/bmatcuk/doublestar/v2 v2.0.1/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
|
||||
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=
|
||||
@@ -85,6 +83,8 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/corona10/goimagehash v1.0.3 h1:NZM518aKLmoNluluhfHGxT3LGOnrojrxhGn63DR/CZA=
|
||||
github.com/corona10/goimagehash v1.0.3/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
@@ -101,6 +101,7 @@ github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKX
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
@@ -128,6 +129,8 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk=
|
||||
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
|
||||
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
@@ -538,8 +541,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/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
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=
|
||||
@@ -572,6 +575,8 @@ github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007 h1:Ohgj9L0EYOgXxkDp+
|
||||
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007/go.mod h1:wKCOWMb6iNlvKiOToY2cNuaovSXvIiv1zDi9QDR7aGQ=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -614,6 +619,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
|
||||
github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.1.0 h1:g0fH8RicVgNl+zVZDCDfbdWxAWoAEJyI7I3TZYXFiig=
|
||||
@@ -800,6 +807,8 @@ golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86h
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
|
||||
@@ -2,13 +2,14 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
stashes {
|
||||
path
|
||||
excludeVideo
|
||||
excludeImage
|
||||
excludeImage
|
||||
}
|
||||
databasePath
|
||||
generatedPath
|
||||
cachePath
|
||||
calculateMD5
|
||||
videoFileNamingAlgorithm
|
||||
parallelTasks
|
||||
previewSegments
|
||||
previewSegmentDuration
|
||||
previewExcludeStart
|
||||
@@ -16,6 +17,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
previewPreset
|
||||
maxTranscodeSize
|
||||
maxStreamingTranscodeSize
|
||||
apiKey
|
||||
username
|
||||
password
|
||||
maxSessionAge
|
||||
@@ -30,6 +32,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
excludes
|
||||
imageExcludes
|
||||
scraperUserAgent
|
||||
scraperCertCheck
|
||||
scraperCDPPath
|
||||
stashBoxes {
|
||||
name
|
||||
@@ -39,6 +42,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
}
|
||||
|
||||
fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
menuItems
|
||||
soundOnPreview
|
||||
wallShowTitle
|
||||
wallPlayback
|
||||
@@ -48,6 +52,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
css
|
||||
cssEnabled
|
||||
language
|
||||
slideshowDelay
|
||||
}
|
||||
|
||||
fragment ConfigData on ConfigResult {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
fragment GallerySlimData on Gallery {
|
||||
fragment SlimGalleryData on Gallery {
|
||||
id
|
||||
checksum
|
||||
path
|
||||
@@ -7,20 +7,36 @@ fragment GallerySlimData on Gallery {
|
||||
url
|
||||
details
|
||||
rating
|
||||
organized
|
||||
image_count
|
||||
cover {
|
||||
...SlimImageData
|
||||
file {
|
||||
size
|
||||
width
|
||||
height
|
||||
}
|
||||
|
||||
paths {
|
||||
thumbnail
|
||||
}
|
||||
}
|
||||
studio {
|
||||
...StudioData
|
||||
id
|
||||
name
|
||||
image_path
|
||||
}
|
||||
tags {
|
||||
...TagData
|
||||
id
|
||||
name
|
||||
}
|
||||
performers {
|
||||
...PerformerData
|
||||
id
|
||||
name
|
||||
gender
|
||||
favorite
|
||||
image_path
|
||||
}
|
||||
scene {
|
||||
scenes {
|
||||
id
|
||||
title
|
||||
path
|
||||
|
||||
@@ -7,6 +7,7 @@ fragment GalleryData on Gallery {
|
||||
url
|
||||
details
|
||||
rating
|
||||
organized
|
||||
images {
|
||||
...SlimImageData
|
||||
}
|
||||
@@ -14,18 +15,16 @@ fragment GalleryData on Gallery {
|
||||
...SlimImageData
|
||||
}
|
||||
studio {
|
||||
...StudioData
|
||||
...SlimStudioData
|
||||
}
|
||||
tags {
|
||||
...TagData
|
||||
...SlimTagData
|
||||
}
|
||||
|
||||
performers {
|
||||
...PerformerData
|
||||
}
|
||||
scene {
|
||||
id
|
||||
title
|
||||
path
|
||||
scenes {
|
||||
...SlimSceneData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ fragment SlimImageData on Image {
|
||||
checksum
|
||||
title
|
||||
rating
|
||||
organized
|
||||
o_counter
|
||||
path
|
||||
|
||||
@@ -37,6 +38,7 @@ fragment SlimImageData on Image {
|
||||
performers {
|
||||
id
|
||||
name
|
||||
gender
|
||||
favorite
|
||||
image_path
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ fragment ImageData on Image {
|
||||
checksum
|
||||
title
|
||||
rating
|
||||
organized
|
||||
o_counter
|
||||
path
|
||||
|
||||
@@ -22,11 +23,11 @@ fragment ImageData on Image {
|
||||
}
|
||||
|
||||
studio {
|
||||
...StudioData
|
||||
...SlimStudioData
|
||||
}
|
||||
|
||||
tags {
|
||||
...TagData
|
||||
...SlimTagData
|
||||
}
|
||||
|
||||
performers {
|
||||
|
||||
@@ -9,7 +9,7 @@ fragment MovieData on Movie {
|
||||
director
|
||||
|
||||
studio {
|
||||
...StudioData
|
||||
...SlimStudioData
|
||||
}
|
||||
|
||||
synopsis
|
||||
|
||||
@@ -3,8 +3,14 @@ fragment SlimPerformerData on Performer {
|
||||
name
|
||||
gender
|
||||
image_path
|
||||
favorite
|
||||
tags {
|
||||
id
|
||||
name
|
||||
}
|
||||
stash_ids {
|
||||
endpoint
|
||||
stash_id
|
||||
}
|
||||
rating
|
||||
}
|
||||
|
||||
@@ -20,8 +20,20 @@ fragment PerformerData on Performer {
|
||||
favorite
|
||||
image_path
|
||||
scene_count
|
||||
image_count
|
||||
gallery_count
|
||||
|
||||
tags {
|
||||
...SlimTagData
|
||||
}
|
||||
|
||||
stash_ids {
|
||||
stash_id
|
||||
endpoint
|
||||
}
|
||||
rating
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
weight
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ fragment SlimSceneData on Scene {
|
||||
date
|
||||
rating
|
||||
o_counter
|
||||
organized
|
||||
path
|
||||
phash
|
||||
|
||||
file {
|
||||
size
|
||||
@@ -28,6 +30,7 @@ fragment SlimSceneData on Scene {
|
||||
webp
|
||||
vtt
|
||||
chapters_vtt
|
||||
sprite
|
||||
}
|
||||
|
||||
scene_markers {
|
||||
@@ -36,7 +39,7 @@ fragment SlimSceneData on Scene {
|
||||
seconds
|
||||
}
|
||||
|
||||
gallery {
|
||||
galleries {
|
||||
id
|
||||
path
|
||||
title
|
||||
@@ -65,6 +68,7 @@ fragment SlimSceneData on Scene {
|
||||
performers {
|
||||
id
|
||||
name
|
||||
gender
|
||||
favorite
|
||||
image_path
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ fragment SceneData on Scene {
|
||||
date
|
||||
rating
|
||||
o_counter
|
||||
organized
|
||||
path
|
||||
phash
|
||||
|
||||
file {
|
||||
size
|
||||
@@ -34,12 +36,12 @@ fragment SceneData on Scene {
|
||||
...SceneMarkerData
|
||||
}
|
||||
|
||||
gallery {
|
||||
...GalleryData
|
||||
galleries {
|
||||
...SlimGalleryData
|
||||
}
|
||||
|
||||
studio {
|
||||
...StudioData
|
||||
...SlimStudioData
|
||||
}
|
||||
|
||||
movies {
|
||||
@@ -50,7 +52,7 @@ fragment SceneData on Scene {
|
||||
}
|
||||
|
||||
tags {
|
||||
...TagData
|
||||
...SlimTagData
|
||||
}
|
||||
|
||||
performers {
|
||||
|
||||
@@ -15,7 +15,14 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
||||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
tags {
|
||||
...ScrapedSceneTagData
|
||||
}
|
||||
image
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
weight
|
||||
}
|
||||
|
||||
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||
@@ -36,8 +43,15 @@ fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
tags {
|
||||
...ScrapedSceneTagData
|
||||
}
|
||||
remote_site_id
|
||||
images
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
weight
|
||||
}
|
||||
|
||||
fragment ScrapedMovieStudioData on ScrapedMovieStudio {
|
||||
@@ -183,3 +197,10 @@ fragment ScrapedStashBoxSceneData on ScrapedScene {
|
||||
...ScrapedSceneMovieData
|
||||
}
|
||||
}
|
||||
|
||||
fragment ScrapedStashBoxPerformerData on StashBoxPerformerQueryResult {
|
||||
query
|
||||
results {
|
||||
...ScrapedScenePerformerData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,6 @@ fragment SlimStudioData on Studio {
|
||||
parent_studio {
|
||||
id
|
||||
}
|
||||
details
|
||||
rating
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ fragment StudioData on Studio {
|
||||
url
|
||||
image_path
|
||||
scene_count
|
||||
image_count
|
||||
gallery_count
|
||||
}
|
||||
child_studios {
|
||||
id
|
||||
@@ -18,11 +20,17 @@ fragment StudioData on Studio {
|
||||
url
|
||||
image_path
|
||||
scene_count
|
||||
image_count
|
||||
gallery_count
|
||||
}
|
||||
image_path
|
||||
scene_count
|
||||
image_count
|
||||
gallery_count
|
||||
stash_ids {
|
||||
stash_id
|
||||
endpoint
|
||||
}
|
||||
details
|
||||
rating
|
||||
}
|
||||
|
||||
5
graphql/documents/data/tag-slim.graphql
Normal file
5
graphql/documents/data/tag-slim.graphql
Normal file
@@ -0,0 +1,5 @@
|
||||
fragment SlimTagData on Tag {
|
||||
id
|
||||
name
|
||||
image_path
|
||||
}
|
||||
@@ -4,4 +4,7 @@ fragment TagData on Tag {
|
||||
image_path
|
||||
scene_count
|
||||
scene_marker_count
|
||||
image_count
|
||||
gallery_count
|
||||
performer_count
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
mutation Setup($input: SetupInput!) {
|
||||
setup(input: $input)
|
||||
}
|
||||
|
||||
mutation Migrate($input: MigrateInput!) {
|
||||
migrate(input: $input)
|
||||
}
|
||||
|
||||
mutation ConfigureGeneral($input: ConfigGeneralInput!) {
|
||||
configureGeneral(input: $input) {
|
||||
...ConfigGeneralData
|
||||
@@ -8,4 +16,8 @@ mutation ConfigureInterface($input: ConfigInterfaceInput!) {
|
||||
configureInterface(input: $input) {
|
||||
...ConfigInterfaceData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation GenerateAPIKey($input: GenerateAPIKeyInput!) {
|
||||
generateAPIKey(input: $input)
|
||||
}
|
||||
|
||||
@@ -1,79 +1,23 @@
|
||||
mutation GalleryCreate(
|
||||
$title: String!,
|
||||
$details: String,
|
||||
$url: String,
|
||||
$date: String,
|
||||
$rating: Int,
|
||||
$scene_id: ID,
|
||||
$studio_id: ID,
|
||||
$performer_ids: [ID!] = [],
|
||||
$tag_ids: [ID!] = []) {
|
||||
$input: GalleryCreateInput!) {
|
||||
|
||||
galleryCreate(input: {
|
||||
title: $title,
|
||||
details: $details,
|
||||
url: $url,
|
||||
date: $date,
|
||||
rating: $rating,
|
||||
scene_id: $scene_id,
|
||||
studio_id: $studio_id,
|
||||
tag_ids: $tag_ids,
|
||||
performer_ids: $performer_ids
|
||||
}) {
|
||||
galleryCreate(input: $input) {
|
||||
...GalleryData
|
||||
}
|
||||
}
|
||||
|
||||
mutation GalleryUpdate(
|
||||
$id: ID!,
|
||||
$title: String,
|
||||
$details: String,
|
||||
$url: String,
|
||||
$date: String,
|
||||
$rating: Int,
|
||||
$scene_id: ID,
|
||||
$studio_id: ID,
|
||||
$performer_ids: [ID!] = [],
|
||||
$tag_ids: [ID!] = []) {
|
||||
$input: GalleryUpdateInput!) {
|
||||
|
||||
galleryUpdate(input: {
|
||||
id: $id,
|
||||
title: $title,
|
||||
details: $details,
|
||||
url: $url,
|
||||
date: $date,
|
||||
rating: $rating,
|
||||
scene_id: $scene_id,
|
||||
studio_id: $studio_id,
|
||||
tag_ids: $tag_ids,
|
||||
performer_ids: $performer_ids
|
||||
}) {
|
||||
galleryUpdate(input: $input) {
|
||||
...GalleryData
|
||||
}
|
||||
}
|
||||
|
||||
mutation BulkGalleryUpdate(
|
||||
$ids: [ID!] = [],
|
||||
$url: String,
|
||||
$date: String,
|
||||
$details: String,
|
||||
$rating: Int,
|
||||
$scene_id: ID,
|
||||
$studio_id: ID,
|
||||
$tag_ids: BulkUpdateIds,
|
||||
$performer_ids: BulkUpdateIds) {
|
||||
$input: BulkGalleryUpdateInput!) {
|
||||
|
||||
bulkGalleryUpdate(input: {
|
||||
ids: $ids,
|
||||
details: $details,
|
||||
url: $url,
|
||||
date: $date,
|
||||
rating: $rating,
|
||||
scene_id: $scene_id,
|
||||
studio_id: $studio_id,
|
||||
tag_ids: $tag_ids,
|
||||
performer_ids: $performer_ids
|
||||
}) {
|
||||
bulkGalleryUpdate(input: $input) {
|
||||
...GalleryData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,15 @@
|
||||
mutation ImageUpdate(
|
||||
$id: ID!,
|
||||
$title: String,
|
||||
$rating: Int,
|
||||
$studio_id: ID,
|
||||
$gallery_ids: [ID!] = [],
|
||||
$performer_ids: [ID!] = [],
|
||||
$tag_ids: [ID!] = []) {
|
||||
$input: ImageUpdateInput!) {
|
||||
|
||||
imageUpdate(input: {
|
||||
id: $id,
|
||||
title: $title,
|
||||
rating: $rating,
|
||||
studio_id: $studio_id,
|
||||
gallery_ids: $gallery_ids,
|
||||
performer_ids: $performer_ids,
|
||||
tag_ids: $tag_ids
|
||||
}) {
|
||||
imageUpdate(input: $input) {
|
||||
...SlimImageData
|
||||
}
|
||||
}
|
||||
|
||||
mutation BulkImageUpdate(
|
||||
$ids: [ID!] = [],
|
||||
$title: String,
|
||||
$rating: Int,
|
||||
$studio_id: ID,
|
||||
$gallery_ids: BulkUpdateIds,
|
||||
$performer_ids: BulkUpdateIds,
|
||||
$tag_ids: BulkUpdateIds) {
|
||||
$input: BulkImageUpdateInput!) {
|
||||
|
||||
bulkImageUpdate(input: {
|
||||
ids: $ids,
|
||||
title: $title,
|
||||
rating: $rating,
|
||||
studio_id: $studio_id,
|
||||
gallery_ids: $gallery_ids,
|
||||
performer_ids: $performer_ids,
|
||||
tag_ids: $tag_ids
|
||||
}) {
|
||||
bulkImageUpdate(input: $input) {
|
||||
...SlimImageData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ mutation MetadataAutoTag($input: AutoTagMetadataInput!) {
|
||||
metadataAutoTag(input: $input)
|
||||
}
|
||||
|
||||
mutation MetadataClean {
|
||||
metadataClean
|
||||
mutation MetadataClean($input: CleanMetadataInput!) {
|
||||
metadataClean(input: $input)
|
||||
}
|
||||
|
||||
mutation MigrateHashNaming {
|
||||
@@ -36,4 +36,8 @@ mutation MigrateHashNaming {
|
||||
|
||||
mutation StopJob {
|
||||
stopJob
|
||||
}
|
||||
}
|
||||
|
||||
mutation BackupDatabase($input: BackupDatabaseInput!) {
|
||||
backupDatabase(input: $input)
|
||||
}
|
||||
|
||||
@@ -16,25 +16,16 @@ mutation MovieCreate(
|
||||
}
|
||||
}
|
||||
|
||||
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 }) {
|
||||
mutation MovieUpdate($input: MovieUpdateInput!) {
|
||||
movieUpdate(input: $input) {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
|
||||
mutation MovieDestroy($id: ID!) {
|
||||
movieDestroy(input: { id: $id })
|
||||
}
|
||||
}
|
||||
|
||||
mutation MoviesDestroy($ids: [ID!]!) {
|
||||
moviesDestroy(ids: $ids)
|
||||
}
|
||||
|
||||
@@ -1,93 +1,23 @@
|
||||
mutation PerformerCreate(
|
||||
$name: String!,
|
||||
$url: String,
|
||||
$gender: GenderEnum,
|
||||
$birthdate: String,
|
||||
$ethnicity: String,
|
||||
$country: String,
|
||||
$eye_color: String,
|
||||
$height: String,
|
||||
$measurements: String,
|
||||
$fake_tits: String,
|
||||
$career_length: String,
|
||||
$tattoos: String,
|
||||
$piercings: String,
|
||||
$aliases: String,
|
||||
$twitter: String,
|
||||
$instagram: String,
|
||||
$favorite: Boolean,
|
||||
$stash_ids: [StashIDInput!],
|
||||
$image: String) {
|
||||
$input: PerformerCreateInput!) {
|
||||
|
||||
performerCreate(input: {
|
||||
name: $name,
|
||||
url: $url,
|
||||
gender: $gender,
|
||||
birthdate: $birthdate,
|
||||
ethnicity: $ethnicity,
|
||||
country: $country,
|
||||
eye_color: $eye_color,
|
||||
height: $height,
|
||||
measurements: $measurements,
|
||||
fake_tits: $fake_tits,
|
||||
career_length: $career_length,
|
||||
tattoos: $tattoos,
|
||||
piercings: $piercings,
|
||||
aliases: $aliases,
|
||||
twitter: $twitter,
|
||||
instagram: $instagram,
|
||||
favorite: $favorite,
|
||||
stash_ids: $stash_ids,
|
||||
image: $image
|
||||
}) {
|
||||
performerCreate(input: $input) {
|
||||
...PerformerData
|
||||
}
|
||||
}
|
||||
|
||||
mutation PerformerUpdate(
|
||||
$id: ID!,
|
||||
$name: String,
|
||||
$url: String,
|
||||
$gender: GenderEnum,
|
||||
$birthdate: String,
|
||||
$ethnicity: String,
|
||||
$country: String,
|
||||
$eye_color: String,
|
||||
$height: String,
|
||||
$measurements: String,
|
||||
$fake_tits: String,
|
||||
$career_length: String,
|
||||
$tattoos: String,
|
||||
$piercings: String,
|
||||
$aliases: String,
|
||||
$twitter: String,
|
||||
$instagram: String,
|
||||
$favorite: Boolean,
|
||||
$stash_ids: [StashIDInput!],
|
||||
$image: String) {
|
||||
$input: PerformerUpdateInput!) {
|
||||
|
||||
performerUpdate(input: {
|
||||
id: $id,
|
||||
name: $name,
|
||||
url: $url,
|
||||
gender: $gender,
|
||||
birthdate: $birthdate,
|
||||
ethnicity: $ethnicity,
|
||||
country: $country,
|
||||
eye_color: $eye_color,
|
||||
height: $height,
|
||||
measurements: $measurements,
|
||||
fake_tits: $fake_tits,
|
||||
career_length: $career_length,
|
||||
tattoos: $tattoos,
|
||||
piercings: $piercings,
|
||||
aliases: $aliases,
|
||||
twitter: $twitter,
|
||||
instagram: $instagram,
|
||||
favorite: $favorite,
|
||||
stash_ids: $stash_ids,
|
||||
image: $image
|
||||
}) {
|
||||
performerUpdate(input: $input) {
|
||||
...PerformerData
|
||||
}
|
||||
}
|
||||
|
||||
mutation BulkPerformerUpdate(
|
||||
$input: BulkPerformerUpdateInput!) {
|
||||
|
||||
bulkPerformerUpdate(input: $input) {
|
||||
...PerformerData
|
||||
}
|
||||
}
|
||||
@@ -95,3 +25,7 @@ mutation PerformerUpdate(
|
||||
mutation PerformerDestroy($id: ID!) {
|
||||
performerDestroy(input: { id: $id })
|
||||
}
|
||||
|
||||
mutation PerformersDestroy($ids: [ID!]!) {
|
||||
performersDestroy(ids: $ids)
|
||||
}
|
||||
|
||||
@@ -1,62 +1,16 @@
|
||||
mutation SceneUpdate(
|
||||
$id: ID!,
|
||||
$title: String,
|
||||
$details: String,
|
||||
$url: String,
|
||||
$date: String,
|
||||
$rating: Int,
|
||||
$studio_id: ID,
|
||||
$gallery_id: ID,
|
||||
$performer_ids: [ID!] = [],
|
||||
$movies: [SceneMovieInput!] = [],
|
||||
$tag_ids: [ID!] = [],
|
||||
$stash_ids: [StashIDInput!],
|
||||
$cover_image: String) {
|
||||
$input: SceneUpdateInput!) {
|
||||
|
||||
sceneUpdate(input: {
|
||||
id: $id,
|
||||
title: $title,
|
||||
details: $details,
|
||||
url: $url,
|
||||
date: $date,
|
||||
rating: $rating,
|
||||
studio_id: $studio_id,
|
||||
gallery_id: $gallery_id,
|
||||
performer_ids: $performer_ids,
|
||||
movies: $movies,
|
||||
tag_ids: $tag_ids,
|
||||
stash_ids: $stash_ids,
|
||||
cover_image: $cover_image
|
||||
}) {
|
||||
...SceneData
|
||||
sceneUpdate(input: $input) {
|
||||
...SceneData
|
||||
}
|
||||
}
|
||||
|
||||
mutation BulkSceneUpdate(
|
||||
$ids: [ID!] = [],
|
||||
$title: String,
|
||||
$details: String,
|
||||
$url: String,
|
||||
$date: String,
|
||||
$rating: Int,
|
||||
$studio_id: ID,
|
||||
$gallery_id: ID,
|
||||
$performer_ids: BulkUpdateIds,
|
||||
$tag_ids: BulkUpdateIds) {
|
||||
$input: BulkSceneUpdateInput!) {
|
||||
|
||||
bulkSceneUpdate(input: {
|
||||
ids: $ids,
|
||||
title: $title,
|
||||
details: $details,
|
||||
url: $url,
|
||||
date: $date,
|
||||
rating: $rating,
|
||||
studio_id: $studio_id,
|
||||
gallery_id: $gallery_id,
|
||||
performer_ids: $performer_ids,
|
||||
tag_ids: $tag_ids
|
||||
}) {
|
||||
...SceneData
|
||||
bulkSceneUpdate(input: $input) {
|
||||
...SceneData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
mutation SubmitStashBoxFingerprints($input: StashBoxFingerprintSubmissionInput!) {
|
||||
submitStashBoxFingerprints(input: $input)
|
||||
}
|
||||
|
||||
mutation StashBoxBatchPerformerTag($input: StashBoxBatchPerformerTagInput!) {
|
||||
stashBoxBatchPerformerTag(input: $input)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
mutation StudioCreate(
|
||||
$name: String!,
|
||||
$url: String,
|
||||
$image: String,
|
||||
$stash_ids: [StashIDInput!],
|
||||
$parent_id: ID) {
|
||||
|
||||
studioCreate(input: { name: $name, url: $url, image: $image, stash_ids: $stash_ids, parent_id: $parent_id }) {
|
||||
mutation StudioCreate($input: StudioCreateInput!) {
|
||||
studioCreate(input: $input) {
|
||||
...StudioData
|
||||
}
|
||||
}
|
||||
|
||||
mutation StudioUpdate(
|
||||
$id: ID!
|
||||
$name: String,
|
||||
$url: String,
|
||||
$image: String,
|
||||
$stash_ids: [StashIDInput!],
|
||||
$parent_id: ID) {
|
||||
|
||||
studioUpdate(input: { id: $id, name: $name, url: $url, image: $image, stash_ids: $stash_ids, parent_id: $parent_id }) {
|
||||
mutation StudioUpdate($input: StudioUpdateInput!) {
|
||||
studioUpdate(input: $input) {
|
||||
...StudioData
|
||||
}
|
||||
}
|
||||
@@ -26,3 +13,7 @@ mutation StudioUpdate(
|
||||
mutation StudioDestroy($id: ID!) {
|
||||
studioDestroy(input: { id: $id })
|
||||
}
|
||||
|
||||
mutation StudiosDestroy($ids: [ID!]!) {
|
||||
studiosDestroy(ids: $ids)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,12 @@ mutation TagDestroy($id: ID!) {
|
||||
tagDestroy(input: { id: $id })
|
||||
}
|
||||
|
||||
mutation TagUpdate($id: ID!, $name: String!, $image: String) {
|
||||
tagUpdate(input: { id: $id, name: $name, image: $image }) {
|
||||
mutation TagsDestroy($ids: [ID!]!) {
|
||||
tagsDestroy(ids: $ids)
|
||||
}
|
||||
|
||||
mutation TagUpdate($input: TagUpdateInput!) {
|
||||
tagUpdate(input: $input) {
|
||||
...TagData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ query FindGalleries($filter: FindFilterType, $gallery_filter: GalleryFilterType)
|
||||
findGalleries(gallery_filter: $gallery_filter, filter: $filter) {
|
||||
count
|
||||
galleries {
|
||||
...GallerySlimData
|
||||
...SlimGalleryData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,37 +13,29 @@ query AllTags {
|
||||
}
|
||||
|
||||
query AllPerformersForFilter {
|
||||
allPerformersSlim {
|
||||
allPerformers {
|
||||
...SlimPerformerData
|
||||
}
|
||||
}
|
||||
|
||||
query AllStudiosForFilter {
|
||||
allStudiosSlim {
|
||||
allStudios {
|
||||
...SlimStudioData
|
||||
}
|
||||
}
|
||||
query AllMoviesForFilter {
|
||||
allMoviesSlim {
|
||||
allMovies {
|
||||
...SlimMovieData
|
||||
}
|
||||
}
|
||||
|
||||
query AllTagsForFilter {
|
||||
allTagsSlim {
|
||||
allTags {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
query ValidGalleriesForScene($scene_id: ID!) {
|
||||
validGalleriesForScene(scene_id: $scene_id) {
|
||||
id
|
||||
path
|
||||
title
|
||||
}
|
||||
}
|
||||
|
||||
query Stats {
|
||||
stats {
|
||||
scene_count,
|
||||
|
||||
@@ -16,6 +16,12 @@ query FindScenesByPathRegex($filter: FindFilterType) {
|
||||
}
|
||||
}
|
||||
|
||||
query FindDuplicateScenes($distance: Int) {
|
||||
findDuplicateScenes(distance: $distance) {
|
||||
...SlimSceneData
|
||||
}
|
||||
}
|
||||
|
||||
query FindScene($id: ID!, $checksum: String) {
|
||||
findScene(id: $id, checksum: $checksum) {
|
||||
...SceneData
|
||||
@@ -45,7 +51,7 @@ query ParseSceneFilenames($filter: FindFilterType!, $config: SceneParserInput!)
|
||||
date
|
||||
rating
|
||||
studio_id
|
||||
gallery_id
|
||||
gallery_ids
|
||||
movies {
|
||||
movie_id
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ query ScrapeFreeones($performer_name: String!) {
|
||||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
details
|
||||
death_date
|
||||
hair_color
|
||||
weight
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,8 +90,14 @@ query ScrapeMovieURL($url: String!) {
|
||||
}
|
||||
}
|
||||
|
||||
query QueryStashBoxScene($input: StashBoxQueryInput!) {
|
||||
query QueryStashBoxScene($input: StashBoxSceneQueryInput!) {
|
||||
queryStashBoxScene(input: $input) {
|
||||
...ScrapedStashBoxSceneData
|
||||
}
|
||||
}
|
||||
|
||||
query QueryStashBoxPerformer($input: StashBoxPerformerQueryInput!) {
|
||||
queryStashBoxPerformer(input: $input) {
|
||||
...ScrapedStashBoxPerformerData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,3 +5,13 @@ query JobStatus {
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
query SystemStatus {
|
||||
systemStatus {
|
||||
databaseSchema
|
||||
databasePath
|
||||
appSchema
|
||||
status
|
||||
configPath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ type Query {
|
||||
|
||||
findScenesByPathRegex(filter: FindFilterType): FindScenesResultType!
|
||||
|
||||
""" Returns any groups of scenes that are perceptual duplicates within the queried distance """
|
||||
findDuplicateScenes(distance: Int): [[Scene!]!]!
|
||||
|
||||
"""Return valid stream paths"""
|
||||
sceneStreams(id: ID): [SceneStreamEndpoint!]!
|
||||
|
||||
@@ -50,8 +53,6 @@ type Query {
|
||||
|
||||
"""Get marker strings"""
|
||||
markerStrings(q: String, sort: String): [MarkerStringsResultType]!
|
||||
"""Get the list of valid galleries for a given scene ID"""
|
||||
validGalleriesForScene(scene_id: ID): [Gallery!]!
|
||||
"""Get stats"""
|
||||
stats: StatsResultType!
|
||||
"""Organize scene markers by tag for a given scene ID"""
|
||||
@@ -90,7 +91,8 @@ type Query {
|
||||
scrapeFreeonesPerformerList(query: String!): [String!]!
|
||||
|
||||
"""Query StashBox for scenes"""
|
||||
queryStashBoxScene(input: StashBoxQueryInput!): [ScrapedScene!]!
|
||||
queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]!
|
||||
queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]!
|
||||
|
||||
# Plugins
|
||||
"""List loaded plugins"""
|
||||
@@ -105,7 +107,7 @@ type Query {
|
||||
directory(path: String): Directory!
|
||||
|
||||
# Metadata
|
||||
|
||||
systemStatus: SystemStatus!
|
||||
jobStatus: MetadataUpdateStatus!
|
||||
|
||||
# Get everything
|
||||
@@ -117,11 +119,6 @@ type Query {
|
||||
|
||||
# Get everything with minimal metadata
|
||||
|
||||
allPerformersSlim: [Performer!]!
|
||||
allStudiosSlim: [Studio!]!
|
||||
allMoviesSlim: [Movie!]!
|
||||
allTagsSlim: [Tag!]!
|
||||
|
||||
# Version
|
||||
version: Version!
|
||||
|
||||
@@ -130,6 +127,9 @@ type Query {
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
setup(input: SetupInput!): Boolean!
|
||||
migrate(input: MigrateInput!): Boolean!
|
||||
|
||||
sceneUpdate(input: SceneUpdateInput!): Scene
|
||||
bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!]
|
||||
sceneDestroy(input: SceneDestroyInput!): Boolean!
|
||||
@@ -175,23 +175,31 @@ type Mutation {
|
||||
performerCreate(input: PerformerCreateInput!): Performer
|
||||
performerUpdate(input: PerformerUpdateInput!): Performer
|
||||
performerDestroy(input: PerformerDestroyInput!): Boolean!
|
||||
performersDestroy(ids: [ID!]!): Boolean!
|
||||
bulkPerformerUpdate(input: BulkPerformerUpdateInput!): [Performer!]
|
||||
|
||||
studioCreate(input: StudioCreateInput!): Studio
|
||||
studioUpdate(input: StudioUpdateInput!): Studio
|
||||
studioDestroy(input: StudioDestroyInput!): Boolean!
|
||||
studiosDestroy(ids: [ID!]!): Boolean!
|
||||
|
||||
movieCreate(input: MovieCreateInput!): Movie
|
||||
movieUpdate(input: MovieUpdateInput!): Movie
|
||||
movieDestroy(input: MovieDestroyInput!): Boolean!
|
||||
moviesDestroy(ids: [ID!]!): Boolean!
|
||||
|
||||
tagCreate(input: TagCreateInput!): Tag
|
||||
tagUpdate(input: TagUpdateInput!): Tag
|
||||
tagDestroy(input: TagDestroyInput!): Boolean!
|
||||
tagsDestroy(ids: [ID!]!): Boolean!
|
||||
|
||||
"""Change general configuration options"""
|
||||
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
|
||||
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
|
||||
|
||||
"""Generate and set (or clear) API key"""
|
||||
generateAPIKey(input: GenerateAPIKeyInput!): String!
|
||||
|
||||
"""Returns a link to download the result"""
|
||||
exportObjects(input: ExportObjectsInput!): String
|
||||
|
||||
@@ -209,7 +217,7 @@ type Mutation {
|
||||
"""Start auto-tagging. Returns the job ID"""
|
||||
metadataAutoTag(input: AutoTagMetadataInput!): String!
|
||||
"""Clean metadata. Returns the job ID"""
|
||||
metadataClean: String!
|
||||
metadataClean(input: CleanMetadataInput!): String!
|
||||
"""Migrate generated files for the current hash naming"""
|
||||
migrateHashNaming: String!
|
||||
|
||||
@@ -222,8 +230,14 @@ type Mutation {
|
||||
|
||||
stopJob: Boolean!
|
||||
|
||||
""" Submit fingerprints to stash-box instance """
|
||||
"""Submit fingerprints to stash-box instance"""
|
||||
submitStashBoxFingerprints(input: StashBoxFingerprintSubmissionInput!): Boolean!
|
||||
|
||||
"""Backup the database. Optionally returns a link to download the database file"""
|
||||
backupDatabase(input: BackupDatabaseInput!): String
|
||||
|
||||
"""Run batch performer tag task. Returns the job ID."""
|
||||
stashBoxBatchPerformerTag(input: StashBoxBatchPerformerTagInput!): String!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
input SetupInput {
|
||||
"""Empty to indicate $HOME/.stash/config.yml default"""
|
||||
configLocation: String!
|
||||
stashes: [StashConfigInput!]!
|
||||
"""Empty to indicate default"""
|
||||
databaseFile: String!
|
||||
"""Empty to indicate default"""
|
||||
generatedLocation: String!
|
||||
}
|
||||
|
||||
enum StreamingResolutionEnum {
|
||||
"240p", LOW
|
||||
"480p", STANDARD
|
||||
@@ -35,6 +45,8 @@ input ConfigGeneralInput {
|
||||
calculateMD5: Boolean!
|
||||
"""Hash algorithm to use for generated file naming"""
|
||||
videoFileNamingAlgorithm: HashAlgorithm!
|
||||
"""Number of parallel tasks to start during scan/generate"""
|
||||
parallelTasks: Int
|
||||
"""Number of segments in a preview file"""
|
||||
previewSegments: Int
|
||||
"""Preview segment duration, in seconds"""
|
||||
@@ -79,6 +91,8 @@ input ConfigGeneralInput {
|
||||
scraperUserAgent: String
|
||||
"""Scraper CDP path. Path to chrome executable or remote address"""
|
||||
scraperCDPPath: String
|
||||
"""Whether the scraper should check for invalid certificates"""
|
||||
scraperCertCheck: Boolean!
|
||||
"""Stash-box instances used for tagging"""
|
||||
stashBoxes: [StashBoxInput!]!
|
||||
}
|
||||
@@ -90,12 +104,18 @@ type ConfigGeneralResult {
|
||||
databasePath: String!
|
||||
"""Path to generated files"""
|
||||
generatedPath: String!
|
||||
"""Path to the config file used"""
|
||||
configFilePath: String!
|
||||
"""Path to scrapers"""
|
||||
scrapersPath: String!
|
||||
"""Path to cache"""
|
||||
cachePath: String!
|
||||
"""Whether to calculate MD5 checksums for scene video files"""
|
||||
calculateMD5: Boolean!
|
||||
"""Hash algorithm to use for generated file naming"""
|
||||
videoFileNamingAlgorithm: HashAlgorithm!
|
||||
"""Number of parallel tasks to start during scan/generate"""
|
||||
parallelTasks: Int!
|
||||
"""Number of segments in a preview file"""
|
||||
previewSegments: Int!
|
||||
"""Preview segment duration, in seconds"""
|
||||
@@ -110,6 +130,8 @@ type ConfigGeneralResult {
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"""Max streaming transcode size"""
|
||||
maxStreamingTranscodeSize: StreamingResolutionEnum
|
||||
"""API Key"""
|
||||
apiKey: String!
|
||||
"""Username"""
|
||||
username: String!
|
||||
"""Password"""
|
||||
@@ -140,11 +162,15 @@ type ConfigGeneralResult {
|
||||
scraperUserAgent: String
|
||||
"""Scraper CDP path. Path to chrome executable or remote address"""
|
||||
scraperCDPPath: String
|
||||
"""Whether the scraper should check for invalid certificates"""
|
||||
scraperCertCheck: Boolean!
|
||||
"""Stash-box instances used for tagging"""
|
||||
stashBoxes: [StashBox!]!
|
||||
}
|
||||
|
||||
input ConfigInterfaceInput {
|
||||
"""Ordered list of items that should be shown in the menu"""
|
||||
menuItems: [String!]
|
||||
"""Enable sound on mouseover previews"""
|
||||
soundOnPreview: Boolean
|
||||
"""Show title and tags in wall view"""
|
||||
@@ -160,10 +186,15 @@ input ConfigInterfaceInput {
|
||||
"""Custom CSS"""
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
"""Interface language"""
|
||||
language: String
|
||||
"""Slideshow Delay"""
|
||||
slideshowDelay: Int
|
||||
}
|
||||
|
||||
type ConfigInterfaceResult {
|
||||
"""Ordered list of items that should be shown in the menu"""
|
||||
menuItems: [String!]
|
||||
"""Enable sound on mouseover previews"""
|
||||
soundOnPreview: Boolean
|
||||
"""Show title and tags in wall view"""
|
||||
@@ -174,13 +205,15 @@ type ConfigInterfaceResult {
|
||||
maximumLoopDuration: Int
|
||||
"""If true, video will autostart on load in the scene player"""
|
||||
autostartVideo: Boolean
|
||||
"""If true, studio overlays will be shown as text instead of logo images"""
|
||||
"""If true, studio overlays will be shown as text instead of logo images"""
|
||||
showStudioAsText: Boolean
|
||||
"""Custom CSS"""
|
||||
css: String
|
||||
cssEnabled: Boolean
|
||||
"""Interface language"""
|
||||
language: String
|
||||
"""Slideshow Delay"""
|
||||
slideshowDelay: Int
|
||||
}
|
||||
|
||||
"""All configuration settings"""
|
||||
@@ -208,3 +241,7 @@ type StashConfig {
|
||||
excludeVideo: Boolean!
|
||||
excludeImage: Boolean!
|
||||
}
|
||||
|
||||
input GenerateAPIKeyInput {
|
||||
clear: Boolean
|
||||
}
|
||||
|
||||
@@ -6,17 +6,26 @@ enum SortDirectionEnum {
|
||||
input FindFilterType {
|
||||
q: String
|
||||
page: Int
|
||||
"""use per_page = -1 to indicate all results. Defaults to 25."""
|
||||
per_page: Int
|
||||
sort: String
|
||||
direction: SortDirectionEnum
|
||||
}
|
||||
|
||||
enum ResolutionEnum {
|
||||
"144p", VERY_LOW
|
||||
"240p", LOW
|
||||
"360p", R360P
|
||||
"480p", STANDARD
|
||||
"540p", WEB_HD
|
||||
"720p", STANDARD_HD
|
||||
"1080p", FULL_HD
|
||||
"1440p", QUAD_HD
|
||||
"1920p", VR_HD
|
||||
"4k", FOUR_K
|
||||
"5k", FIVE_K
|
||||
"6k", SIX_K
|
||||
"8k", EIGHT_K
|
||||
}
|
||||
|
||||
input PerformerFilterType {
|
||||
@@ -38,7 +47,7 @@ input PerformerFilterType {
|
||||
measurements: StringCriterionInput
|
||||
"""Filter by fake tits value"""
|
||||
fake_tits: StringCriterionInput
|
||||
"""Filter by career length"""
|
||||
"""Filter by career length"""
|
||||
career_length: StringCriterionInput
|
||||
"""Filter by tattoos"""
|
||||
tattoos: StringCriterionInput
|
||||
@@ -50,8 +59,28 @@ input PerformerFilterType {
|
||||
gender: GenderCriterionInput
|
||||
"""Filter to only include performers missing this property"""
|
||||
is_missing: String
|
||||
"""Filter to only include performers with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
"""Filter by tag count"""
|
||||
tag_count: IntCriterionInput
|
||||
"""Filter by scene count"""
|
||||
scene_count: IntCriterionInput
|
||||
"""Filter by image count"""
|
||||
image_count: IntCriterionInput
|
||||
"""Filter by gallery count"""
|
||||
gallery_count: IntCriterionInput
|
||||
"""Filter by StashID"""
|
||||
stash_id: String
|
||||
stash_id: StringCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
"""Filter by hair color"""
|
||||
hair_color: StringCriterionInput
|
||||
"""Filter by weight"""
|
||||
weight: IntCriterionInput
|
||||
"""Filter by death year"""
|
||||
death_year: IntCriterionInput
|
||||
}
|
||||
|
||||
input SceneMarkerFilterType {
|
||||
@@ -66,10 +95,16 @@ input SceneMarkerFilterType {
|
||||
}
|
||||
|
||||
input SceneFilterType {
|
||||
AND: SceneFilterType
|
||||
OR: SceneFilterType
|
||||
NOT: SceneFilterType
|
||||
|
||||
"""Filter by path"""
|
||||
path: StringCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
"""Filter by organized"""
|
||||
organized: Boolean
|
||||
"""Filter by o-counter"""
|
||||
o_counter: IntCriterionInput
|
||||
"""Filter by resolution"""
|
||||
@@ -86,10 +121,18 @@ input SceneFilterType {
|
||||
movies: MultiCriterionInput
|
||||
"""Filter to only include scenes with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
"""Filter by tag count"""
|
||||
tag_count: IntCriterionInput
|
||||
"""Filter to only include scenes with performers with these tags"""
|
||||
performer_tags: MultiCriterionInput
|
||||
"""Filter to only include scenes with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter by performer count"""
|
||||
performer_count: IntCriterionInput
|
||||
"""Filter by StashID"""
|
||||
stash_id: String
|
||||
stash_id: StringCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
}
|
||||
|
||||
input MovieFilterType {
|
||||
@@ -97,18 +140,34 @@ input MovieFilterType {
|
||||
studios: MultiCriterionInput
|
||||
"""Filter to only include movies missing this property"""
|
||||
is_missing: String
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
}
|
||||
|
||||
input StudioFilterType {
|
||||
"""Filter to only include studios with this parent studio"""
|
||||
parents: MultiCriterionInput
|
||||
"""Filter by StashID"""
|
||||
stash_id: String
|
||||
stash_id: StringCriterionInput
|
||||
"""Filter to only include studios missing this property"""
|
||||
is_missing: String
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
"""Filter by scene count"""
|
||||
scene_count: IntCriterionInput
|
||||
"""Filter by image count"""
|
||||
image_count: IntCriterionInput
|
||||
"""Filter by gallery count"""
|
||||
gallery_count: IntCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
}
|
||||
|
||||
input GalleryFilterType {
|
||||
AND: GalleryFilterType
|
||||
OR: GalleryFilterType
|
||||
NOT: GalleryFilterType
|
||||
|
||||
"""Filter by path"""
|
||||
path: StringCriterionInput
|
||||
"""Filter to only include galleries missing this property"""
|
||||
@@ -117,34 +176,63 @@ input GalleryFilterType {
|
||||
is_zip: Boolean
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
"""Filter by organized"""
|
||||
organized: Boolean
|
||||
"""Filter by average image resolution"""
|
||||
average_resolution: ResolutionEnum
|
||||
"""Filter to only include scenes with this studio"""
|
||||
"""Filter to only include galleries with this studio"""
|
||||
studios: MultiCriterionInput
|
||||
"""Filter to only include scenes with these tags"""
|
||||
"""Filter to only include galleries with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
"""Filter to only include scenes with these performers"""
|
||||
"""Filter by tag count"""
|
||||
tag_count: IntCriterionInput
|
||||
"""Filter to only include galleries with performers with these tags"""
|
||||
performer_tags: MultiCriterionInput
|
||||
"""Filter to only include galleries with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter by performer count"""
|
||||
performer_count: IntCriterionInput
|
||||
"""Filter by number of images in this gallery"""
|
||||
image_count: IntCriterionInput
|
||||
"""Filter by url"""
|
||||
url: StringCriterionInput
|
||||
}
|
||||
|
||||
input TagFilterType {
|
||||
AND: TagFilterType
|
||||
OR: TagFilterType
|
||||
NOT: TagFilterType
|
||||
|
||||
"""Filter to only include tags missing this property"""
|
||||
is_missing: String
|
||||
|
||||
"""Filter by number of scenes with this tag"""
|
||||
scene_count: IntCriterionInput
|
||||
|
||||
"""Filter by number of images with this tag"""
|
||||
image_count: IntCriterionInput
|
||||
|
||||
"""Filter by number of galleries with this tag"""
|
||||
gallery_count: IntCriterionInput
|
||||
|
||||
"""Filter by number of performers with this tag"""
|
||||
performer_count: IntCriterionInput
|
||||
|
||||
"""Filter by number of markers with this tag"""
|
||||
marker_count: IntCriterionInput
|
||||
}
|
||||
|
||||
input ImageFilterType {
|
||||
AND: ImageFilterType
|
||||
OR: ImageFilterType
|
||||
NOT: ImageFilterType
|
||||
|
||||
"""Filter by path"""
|
||||
path: StringCriterionInput
|
||||
"""Filter by rating"""
|
||||
rating: IntCriterionInput
|
||||
"""Filter by organized"""
|
||||
organized: Boolean
|
||||
"""Filter by o-counter"""
|
||||
o_counter: IntCriterionInput
|
||||
"""Filter by resolution"""
|
||||
@@ -155,8 +243,14 @@ input ImageFilterType {
|
||||
studios: MultiCriterionInput
|
||||
"""Filter to only include images with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
"""Filter by tag count"""
|
||||
tag_count: IntCriterionInput
|
||||
"""Filter to only include images with performers with these tags"""
|
||||
performer_tags: MultiCriterionInput
|
||||
"""Filter to only include images with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter by performer count"""
|
||||
performer_count: IntCriterionInput
|
||||
"""Filter to only include images with these galleries"""
|
||||
galleries: MultiCriterionInput
|
||||
}
|
||||
@@ -178,6 +272,10 @@ enum CriterionModifier {
|
||||
INCLUDES_ALL,
|
||||
INCLUDES,
|
||||
EXCLUDES,
|
||||
"""MATCHES REGEX"""
|
||||
MATCHES_REGEX,
|
||||
"""NOT MATCHES REGEX"""
|
||||
NOT_MATCHES_REGEX,
|
||||
}
|
||||
|
||||
input StringCriterionInput {
|
||||
|
||||
@@ -8,7 +8,8 @@ type Gallery {
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
scene: Scene
|
||||
organized: Boolean!
|
||||
scenes: [Scene!]!
|
||||
studio: Studio
|
||||
image_count: Int!
|
||||
tags: [Tag!]!
|
||||
@@ -31,7 +32,8 @@ input GalleryCreateInput {
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
scene_id: ID
|
||||
organized: Boolean
|
||||
scene_ids: [ID!]
|
||||
studio_id: ID
|
||||
tag_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
@@ -45,7 +47,8 @@ input GalleryUpdateInput {
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
scene_id: ID
|
||||
organized: Boolean
|
||||
scene_ids: [ID!]
|
||||
studio_id: ID
|
||||
tag_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
@@ -58,7 +61,8 @@ input BulkGalleryUpdateInput {
|
||||
date: String
|
||||
details: String
|
||||
rating: Int
|
||||
scene_id: ID
|
||||
organized: Boolean
|
||||
scene_ids: BulkUpdateIds
|
||||
studio_id: ID
|
||||
tag_ids: BulkUpdateIds
|
||||
performer_ids: BulkUpdateIds
|
||||
|
||||
@@ -4,6 +4,7 @@ type Image {
|
||||
title: String
|
||||
rating: Int
|
||||
o_counter: Int
|
||||
organized: Boolean!
|
||||
path: String!
|
||||
|
||||
file: ImageFileType! # Resolver
|
||||
@@ -31,6 +32,7 @@ input ImageUpdateInput {
|
||||
id: ID!
|
||||
title: String
|
||||
rating: Int
|
||||
organized: Boolean
|
||||
|
||||
studio_id: ID
|
||||
performer_ids: [ID!]
|
||||
@@ -43,6 +45,7 @@ input BulkImageUpdateInput {
|
||||
ids: [ID!]
|
||||
title: String
|
||||
rating: Int
|
||||
organized: Boolean
|
||||
|
||||
studio_id: ID
|
||||
performer_ids: BulkUpdateIds
|
||||
|
||||
@@ -7,6 +7,7 @@ input GenerateMetadataInput {
|
||||
previewOptions: GeneratePreviewOptionsInput
|
||||
markers: Boolean!
|
||||
transcodes: Boolean!
|
||||
phashes: Boolean!
|
||||
|
||||
"""scene ids to generate for"""
|
||||
sceneIDs: [ID!]
|
||||
@@ -32,10 +33,28 @@ input GeneratePreviewOptionsInput {
|
||||
|
||||
input ScanMetadataInput {
|
||||
paths: [String!]
|
||||
useFileMetadata: Boolean!
|
||||
"""Set name, date, details from metadata (if present)"""
|
||||
useFileMetadata: Boolean
|
||||
"""Strip file extension from title"""
|
||||
stripFileExtension: Boolean
|
||||
"""Generate previews during scan"""
|
||||
scanGeneratePreviews: Boolean
|
||||
"""Generate image previews during scan"""
|
||||
scanGenerateImagePreviews: Boolean
|
||||
"""Generate sprites during scan"""
|
||||
scanGenerateSprites: Boolean
|
||||
"""Generate phashes during scan"""
|
||||
scanGeneratePhashes: Boolean
|
||||
}
|
||||
|
||||
input CleanMetadataInput {
|
||||
"""Do a dry run. Don't delete any files"""
|
||||
dryRun: Boolean!
|
||||
}
|
||||
|
||||
input AutoTagMetadataInput {
|
||||
"""Paths to tag, null for all files"""
|
||||
paths: [String!]
|
||||
"""IDs of performers to tag files with, or "*" for all"""
|
||||
performers: [String!]
|
||||
"""IDs of studios to tag files with, or "*" for all"""
|
||||
@@ -83,3 +102,25 @@ input ImportObjectsInput {
|
||||
duplicateBehaviour: ImportDuplicateEnum!
|
||||
missingRefBehaviour: ImportMissingRefEnum!
|
||||
}
|
||||
|
||||
input BackupDatabaseInput {
|
||||
download: Boolean
|
||||
}
|
||||
|
||||
enum SystemStatusEnum {
|
||||
SETUP
|
||||
NEEDS_MIGRATION
|
||||
OK
|
||||
}
|
||||
|
||||
type SystemStatus {
|
||||
databaseSchema: Int
|
||||
databasePath: String
|
||||
configPath: String
|
||||
appSchema: Int!
|
||||
status: SystemStatusEnum!
|
||||
}
|
||||
|
||||
input MigrateInput {
|
||||
backupPath: String!
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ input MovieCreateInput {
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
"""This should be base64 encoded"""
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
front_image: String
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
back_image: String
|
||||
}
|
||||
|
||||
@@ -44,8 +45,9 @@ input MovieUpdateInput {
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
"""This should be base64 encoded"""
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
front_image: String
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
back_image: String
|
||||
}
|
||||
|
||||
|
||||
@@ -27,11 +27,19 @@ type Performer {
|
||||
piercings: String
|
||||
aliases: String
|
||||
favorite: Boolean!
|
||||
tags: [Tag!]!
|
||||
|
||||
image_path: String # Resolver
|
||||
scene_count: Int # Resolver
|
||||
image_count: Int # Resolver
|
||||
gallery_count: Int # Resolver
|
||||
scenes: [Scene!]!
|
||||
stash_ids: [StashID!]!
|
||||
rating: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
weight: Int
|
||||
}
|
||||
|
||||
input PerformerCreateInput {
|
||||
@@ -52,9 +60,15 @@ input PerformerCreateInput {
|
||||
twitter: String
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
"""This should be base64 encoded"""
|
||||
tag_ids: [ID!]
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
weight: Int
|
||||
}
|
||||
|
||||
input PerformerUpdateInput {
|
||||
@@ -76,9 +90,42 @@ input PerformerUpdateInput {
|
||||
twitter: String
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
"""This should be base64 encoded"""
|
||||
tag_ids: [ID!]
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
weight: Int
|
||||
}
|
||||
|
||||
input BulkPerformerUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
url: String
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
height: String
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
twitter: String
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
tag_ids: BulkUpdateIds
|
||||
rating: Int
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
weight: Int
|
||||
}
|
||||
|
||||
input PerformerDestroyInput {
|
||||
|
||||
@@ -16,6 +16,7 @@ type ScenePathsType {
|
||||
webp: String # Resolver
|
||||
vtt: String # Resolver
|
||||
chapters_vtt: String # Resolver
|
||||
sprite: String # Resolver
|
||||
}
|
||||
|
||||
type SceneMovie {
|
||||
@@ -32,14 +33,16 @@ type Scene {
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
organized: Boolean!
|
||||
o_counter: Int
|
||||
path: String!
|
||||
phash: String
|
||||
|
||||
file: SceneFileType! # Resolver
|
||||
paths: ScenePathsType! # Resolver
|
||||
|
||||
scene_markers: [SceneMarker!]!
|
||||
gallery: Gallery
|
||||
galleries: [Gallery!]!
|
||||
studio: Studio
|
||||
movies: [SceneMovie!]!
|
||||
tags: [Tag!]!
|
||||
@@ -60,12 +63,13 @@ input SceneUpdateInput {
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
organized: Boolean
|
||||
studio_id: ID
|
||||
gallery_id: ID
|
||||
gallery_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
movies: [SceneMovieInput!]
|
||||
tag_ids: [ID!]
|
||||
"""This should be base64 encoded"""
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
cover_image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
}
|
||||
@@ -89,8 +93,9 @@ input BulkSceneUpdateInput {
|
||||
url: String
|
||||
date: String
|
||||
rating: Int
|
||||
organized: Boolean
|
||||
studio_id: ID
|
||||
gallery_id: ID
|
||||
gallery_ids: BulkUpdateIds
|
||||
performer_ids: BulkUpdateIds
|
||||
tag_ids: BulkUpdateIds
|
||||
}
|
||||
@@ -131,7 +136,7 @@ type SceneParserResult {
|
||||
date: String
|
||||
rating: Int
|
||||
studio_id: ID
|
||||
gallery_id: ID
|
||||
gallery_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
movies: [SceneMovieID!]
|
||||
tag_ids: [ID!]
|
||||
|
||||
@@ -17,8 +17,9 @@ type ScrapedMovie {
|
||||
synopsis: String
|
||||
studio: ScrapedMovieStudio
|
||||
|
||||
"""This should be base64 encoded"""
|
||||
"""This should be a base64 encoded data URL"""
|
||||
front_image: String
|
||||
"""This should be a base64 encoded data URL"""
|
||||
back_image: String
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,15 @@ type ScrapedPerformer {
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
# Should be ScrapedPerformerTag - but would be identical types
|
||||
tags: [ScrapedSceneTag!]
|
||||
|
||||
"""This should be base64 encoded"""
|
||||
"""This should be a base64 encoded data URL"""
|
||||
image: String
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
weight: String
|
||||
}
|
||||
|
||||
input ScrapedPerformerInput {
|
||||
@@ -39,5 +45,10 @@ input ScrapedPerformerInput {
|
||||
piercings: String
|
||||
aliases: String
|
||||
|
||||
# not including tags for the input
|
||||
# not including image for the input
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
weight: String
|
||||
}
|
||||
@@ -45,9 +45,14 @@ type ScrapedScenePerformer {
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
tags: [ScrapedSceneTag!]
|
||||
|
||||
remote_site_id: String
|
||||
images: [String!]
|
||||
details: String
|
||||
death_date: String
|
||||
hair_color: String
|
||||
weight: String
|
||||
}
|
||||
|
||||
type ScrapedSceneMovie {
|
||||
@@ -84,7 +89,7 @@ type ScrapedScene {
|
||||
url: String
|
||||
date: String
|
||||
|
||||
"""This should be base64 encoded"""
|
||||
"""This should be a base64 encoded data URL"""
|
||||
image: String
|
||||
|
||||
file: SceneFileType # Resolver
|
||||
@@ -110,7 +115,7 @@ type ScrapedGallery {
|
||||
performers: [ScrapedScenePerformer!]
|
||||
}
|
||||
|
||||
input StashBoxQueryInput {
|
||||
input StashBoxSceneQueryInput {
|
||||
"""Index of the configured stash-box instance to use"""
|
||||
stash_box_index: Int!
|
||||
"""Instructs query by scene fingerprints"""
|
||||
@@ -119,8 +124,30 @@ input StashBoxQueryInput {
|
||||
q: String
|
||||
}
|
||||
|
||||
input StashBoxPerformerQueryInput {
|
||||
"""Index of the configured stash-box instance to use"""
|
||||
stash_box_index: Int!
|
||||
"""Instructs query by scene fingerprints"""
|
||||
performer_ids: [ID!]
|
||||
"""Query by query string"""
|
||||
q: String
|
||||
}
|
||||
|
||||
type StashBoxPerformerQueryResult {
|
||||
query: String!
|
||||
results: [ScrapedScenePerformer!]!
|
||||
}
|
||||
|
||||
type StashBoxFingerprint {
|
||||
algorithm: String!
|
||||
hash: String!
|
||||
duration: Int!
|
||||
}
|
||||
|
||||
input StashBoxBatchPerformerTagInput {
|
||||
endpoint: Int!
|
||||
exclude_fields: [String!]
|
||||
refresh: Boolean!
|
||||
performer_ids: [ID!]
|
||||
performer_names: [String!]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
type StatsResultType {
|
||||
scene_count: Int!
|
||||
scenes_size: Int!
|
||||
scenes_size: Float!
|
||||
image_count: Int!
|
||||
images_size: Int!
|
||||
images_size: Float!
|
||||
gallery_count: Int!
|
||||
performer_count: Int!
|
||||
studio_count: Int!
|
||||
|
||||
@@ -8,16 +8,22 @@ type Studio {
|
||||
|
||||
image_path: String # Resolver
|
||||
scene_count: Int # Resolver
|
||||
image_count: Int # Resolver
|
||||
gallery_count: Int # Resolver
|
||||
stash_ids: [StashID!]!
|
||||
rating: Int
|
||||
details: String
|
||||
}
|
||||
|
||||
input StudioCreateInput {
|
||||
name: String!
|
||||
url: String
|
||||
parent_id: ID
|
||||
"""This should be base64 encoded"""
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
details: String
|
||||
}
|
||||
|
||||
input StudioUpdateInput {
|
||||
@@ -25,9 +31,11 @@ input StudioUpdateInput {
|
||||
name: String
|
||||
url: String
|
||||
parent_id: ID,
|
||||
"""This should be base64 encoded"""
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
rating: Int
|
||||
details: String
|
||||
}
|
||||
|
||||
input StudioDestroyInput {
|
||||
|
||||
@@ -5,12 +5,15 @@ type Tag {
|
||||
image_path: String # Resolver
|
||||
scene_count: Int # Resolver
|
||||
scene_marker_count: Int # Resolver
|
||||
image_count: Int # Resolver
|
||||
gallery_count: Int # Resolver
|
||||
performer_count: Int
|
||||
}
|
||||
|
||||
input TagCreateInput {
|
||||
name: String!
|
||||
|
||||
"""This should be base64 encoded"""
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
}
|
||||
|
||||
@@ -18,7 +21,7 @@ input TagUpdateInput {
|
||||
id: ID!
|
||||
name: String!
|
||||
|
||||
"""This should be base64 encoded"""
|
||||
"""This should be a URL or a base64 encoded data URL"""
|
||||
image: String
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,11 @@ fragment PerformerFragment on Performer {
|
||||
piercings {
|
||||
...BodyModificationFragment
|
||||
}
|
||||
details
|
||||
death_date {
|
||||
...FuzzyDateFragment
|
||||
}
|
||||
weight
|
||||
}
|
||||
|
||||
fragment PerformerAppearanceFragment on PerformerAppearance {
|
||||
@@ -134,6 +139,18 @@ query SearchScene($term: String!) {
|
||||
}
|
||||
}
|
||||
|
||||
query SearchPerformer($term: String!) {
|
||||
searchPerformer(term: $term) {
|
||||
...PerformerFragment
|
||||
}
|
||||
}
|
||||
|
||||
query FindPerformerByID($id: ID!) {
|
||||
findPerformer(id: $id) {
|
||||
...PerformerFragment
|
||||
}
|
||||
}
|
||||
|
||||
mutation SubmitFingerprint($input: FingerprintSubmission!) {
|
||||
submitFingerprint(input: $input)
|
||||
}
|
||||
|
||||
8
main.go
8
main.go
@@ -3,9 +3,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/api"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
@@ -13,12 +11,6 @@ import (
|
||||
|
||||
func main() {
|
||||
manager.Initialize()
|
||||
|
||||
// perform the post-migration for new databases
|
||||
if database.Initialize(config.GetDatabasePath()) {
|
||||
manager.GetInstance().PostMigrate()
|
||||
}
|
||||
|
||||
api.Start()
|
||||
blockForever()
|
||||
}
|
||||
|
||||
141
pkg/api/changeset_translator.go
Normal file
141
pkg/api/changeset_translator.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
const updateInputField = "input"
|
||||
|
||||
func getArgumentMap(ctx context.Context) map[string]interface{} {
|
||||
rctx := graphql.GetResolverContext(ctx)
|
||||
reqCtx := graphql.GetRequestContext(ctx)
|
||||
return rctx.Field.ArgumentMap(reqCtx.Variables)
|
||||
}
|
||||
|
||||
func getUpdateInputMap(ctx context.Context) map[string]interface{} {
|
||||
args := getArgumentMap(ctx)
|
||||
|
||||
input, _ := args[updateInputField]
|
||||
var ret map[string]interface{}
|
||||
if input != nil {
|
||||
ret, _ = input.(map[string]interface{})
|
||||
}
|
||||
|
||||
if ret == nil {
|
||||
ret = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func getUpdateInputMaps(ctx context.Context) []map[string]interface{} {
|
||||
args := getArgumentMap(ctx)
|
||||
|
||||
input, _ := args[updateInputField]
|
||||
var ret []map[string]interface{}
|
||||
if input != nil {
|
||||
// convert []interface{} into []map[string]interface{}
|
||||
iSlice, _ := input.([]interface{})
|
||||
for _, i := range iSlice {
|
||||
m, _ := i.(map[string]interface{})
|
||||
if m != nil {
|
||||
ret = append(ret, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type changesetTranslator struct {
|
||||
inputMap map[string]interface{}
|
||||
}
|
||||
|
||||
func (t changesetTranslator) hasField(field string) bool {
|
||||
if t.inputMap == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, found := t.inputMap[field]
|
||||
return found
|
||||
}
|
||||
|
||||
func (t changesetTranslator) nullString(value *string, field string) *sql.NullString {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := &sql.NullString{}
|
||||
|
||||
if value != nil {
|
||||
ret.String = *value
|
||||
ret.Valid = true
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t changesetTranslator) sqliteDate(value *string, field string) *models.SQLiteDate {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := &models.SQLiteDate{}
|
||||
|
||||
if value != nil {
|
||||
ret.String = *value
|
||||
ret.Valid = true
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t changesetTranslator) nullInt64(value *int, field string) *sql.NullInt64 {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := &sql.NullInt64{}
|
||||
|
||||
if value != nil {
|
||||
ret.Int64 = int64(*value)
|
||||
ret.Valid = true
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t changesetTranslator) nullInt64FromString(value *string, field string) *sql.NullInt64 {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := &sql.NullInt64{}
|
||||
|
||||
if value != nil {
|
||||
ret.Int64, _ = strconv.ParseInt(*value, 10, 64)
|
||||
ret.Valid = true
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t changesetTranslator) nullBool(value *bool, field string) *sql.NullBool {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := &sql.NullBool{}
|
||||
|
||||
if value != nil {
|
||||
ret.Bool = *value
|
||||
ret.Valid = true
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/cpu"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -18,6 +20,7 @@ const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases
|
||||
const apiTags string = "https://api.github.com/repos/stashapp/stash/tags"
|
||||
const apiAcceptHeader string = "application/vnd.github.v3+json"
|
||||
const developmentTag string = "latest_develop"
|
||||
const defaultSHLength int = 7 // default length of SHA short hash returned by <git rev-parse --short HEAD>
|
||||
|
||||
// ErrNoVersion indicates that no version information has been embedded in the
|
||||
// stash binary
|
||||
@@ -25,10 +28,12 @@ var ErrNoVersion = errors.New("no stash version")
|
||||
|
||||
var stashReleases = func() map[string]string {
|
||||
return map[string]string{
|
||||
"windows/amd64": "stash-win.exe",
|
||||
"linux/amd64": "stash-linux",
|
||||
"darwin/amd64": "stash-osx",
|
||||
"linux/amd64": "stash-linux",
|
||||
"windows/amd64": "stash-win.exe",
|
||||
"linux/arm": "stash-pi",
|
||||
"linux/arm64": "stash-linux-arm64v8",
|
||||
"linux/armv7": "stash-linux-arm32v7",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +145,13 @@ func makeGithubRequest(url string, output interface{}) error {
|
||||
// which is the latest pre-release build.
|
||||
func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease string, err error) {
|
||||
|
||||
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
arch := runtime.GOARCH // https://en.wikipedia.org/wiki/Comparison_of_ARM_cores
|
||||
isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4 // armv6 doesn't support any of these features
|
||||
if arch == "arm" && isARMv7 {
|
||||
arch = "armv7"
|
||||
}
|
||||
|
||||
platform := fmt.Sprintf("%s/%s", runtime.GOOS, arch)
|
||||
wantedRelease := stashReleases()[platform]
|
||||
|
||||
version, _, _ := GetVersion()
|
||||
@@ -191,14 +202,20 @@ func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease strin
|
||||
}
|
||||
|
||||
func getReleaseHash(release githubReleasesResponse, shortHash bool, usePreRelease bool) string {
|
||||
shaLength := len(release.Target_commitish)
|
||||
// the /latest API call doesn't return the hash in target_commitish
|
||||
// also add sanity check in case Target_commitish is not 40 characters
|
||||
if !usePreRelease || len(release.Target_commitish) != 40 {
|
||||
if !usePreRelease || shaLength != 40 {
|
||||
return getShaFromTags(shortHash, release.Tag_name)
|
||||
}
|
||||
|
||||
if shortHash {
|
||||
return release.Target_commitish[0:7] //shorthash is first 7 digits of git commit hash
|
||||
last := defaultSHLength // default length of git short hash
|
||||
_, gitShort, _ := GetVersion() // retrieve it to check actual length
|
||||
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
|
||||
last = len(gitShort)
|
||||
}
|
||||
return release.Target_commitish[0:last]
|
||||
}
|
||||
|
||||
return release.Target_commitish
|
||||
@@ -229,14 +246,20 @@ func getShaFromTags(shortHash bool, name string) string {
|
||||
logger.Errorf("Github Tags Api %v", err)
|
||||
return ""
|
||||
}
|
||||
_, gitShort, _ := GetVersion() // retrieve short hash to check actual length
|
||||
|
||||
for _, tag := range tags {
|
||||
if tag.Name == name {
|
||||
if len(tag.Commit.Sha) != 40 {
|
||||
shaLength := len(tag.Commit.Sha)
|
||||
if shaLength != 40 {
|
||||
return ""
|
||||
}
|
||||
if shortHash {
|
||||
return tag.Commit.Sha[0:7] //shorthash is first 7 digits of git commit hash
|
||||
last := defaultSHLength // default length of git short hash
|
||||
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
|
||||
last = len(gitShort)
|
||||
}
|
||||
return tag.Commit.Sha[0:last]
|
||||
}
|
||||
|
||||
return tag.Commit.Sha
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// perform post-migration operations
|
||||
manager.GetInstance().PostMigrate()
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -5,12 +5,13 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type Resolver struct{}
|
||||
type Resolver struct {
|
||||
txnManager models.TransactionManager
|
||||
}
|
||||
|
||||
func (r *Resolver) Gallery() models.GalleryResolver {
|
||||
return &galleryResolver{r}
|
||||
@@ -79,69 +80,84 @@ type scrapedSceneMovieResolver struct{ *Resolver }
|
||||
type scrapedScenePerformerResolver struct{ *Resolver }
|
||||
type scrapedSceneStudioResolver struct{ *Resolver }
|
||||
|
||||
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) ([]*models.SceneMarker, error) {
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
return qb.Wall(q)
|
||||
func (r *Resolver) withTxn(ctx context.Context, fn func(r models.Repository) error) error {
|
||||
return r.txnManager.WithTxn(ctx, fn)
|
||||
}
|
||||
|
||||
func (r *queryResolver) SceneWall(ctx context.Context, q *string) ([]*models.Scene, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
return qb.Wall(q)
|
||||
func (r *Resolver) withReadTxn(ctx context.Context, fn func(r models.ReaderRepository) error) error {
|
||||
return r.txnManager.WithReadTxn(ctx, fn)
|
||||
}
|
||||
|
||||
func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *string) ([]*models.MarkerStringsResultType, error) {
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
return qb.GetMarkerStrings(q, sort)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ValidGalleriesForScene(ctx context.Context, scene_id *string) ([]*models.Gallery, error) {
|
||||
if scene_id == nil {
|
||||
panic("nil scene id") // TODO make scene_id mandatory
|
||||
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.SceneMarker().Wall(q)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sceneID, _ := strconv.Atoi(*scene_id)
|
||||
sqb := models.NewSceneQueryBuilder()
|
||||
scene, err := sqb.Find(sceneID)
|
||||
if err != nil {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Scene().Wall(q)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
validGalleries, err := qb.ValidGalleriesForScenePath(scene.Path)
|
||||
sceneGallery, _ := qb.FindBySceneID(sceneID, nil)
|
||||
if sceneGallery != nil {
|
||||
validGalleries = append(validGalleries, sceneGallery)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *string) (ret []*models.MarkerStringsResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.SceneMarker().GetMarkerStrings(q, sort)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return validGalleries, nil
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) {
|
||||
scenesQB := models.NewSceneQueryBuilder()
|
||||
scenesCount, _ := scenesQB.Count()
|
||||
scenesSize, _ := scenesQB.Size()
|
||||
imageQB := models.NewImageQueryBuilder()
|
||||
imageCount, _ := imageQB.Count()
|
||||
imageSize, _ := imageQB.Size()
|
||||
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,
|
||||
ScenesSize: int(scenesSize),
|
||||
ImageCount: imageCount,
|
||||
ImagesSize: int(imageSize),
|
||||
GalleryCount: galleryCount,
|
||||
PerformerCount: performersCount,
|
||||
StudioCount: studiosCount,
|
||||
MovieCount: moviesCount,
|
||||
TagCount: tagsCount,
|
||||
}, nil
|
||||
var ret models.StatsResultType
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
scenesQB := repo.Scene()
|
||||
imageQB := repo.Image()
|
||||
galleryQB := repo.Gallery()
|
||||
studiosQB := repo.Studio()
|
||||
performersQB := repo.Performer()
|
||||
moviesQB := repo.Movie()
|
||||
tagsQB := repo.Tag()
|
||||
scenesCount, _ := scenesQB.Count()
|
||||
scenesSize, _ := scenesQB.Size()
|
||||
imageCount, _ := imageQB.Count()
|
||||
imageSize, _ := imageQB.Size()
|
||||
galleryCount, _ := galleryQB.Count()
|
||||
performersCount, _ := performersQB.Count()
|
||||
studiosCount, _ := studiosQB.Count()
|
||||
moviesCount, _ := moviesQB.Count()
|
||||
tagsCount, _ := tagsQB.Count()
|
||||
|
||||
ret = models.StatsResultType{
|
||||
SceneCount: scenesCount,
|
||||
ScenesSize: scenesSize,
|
||||
ImageCount: imageCount,
|
||||
ImagesSize: imageSize,
|
||||
GalleryCount: galleryCount,
|
||||
PerformerCount: performersCount,
|
||||
StudioCount: studiosCount,
|
||||
MovieCount: moviesCount,
|
||||
TagCount: tagsCount,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Version(ctx context.Context) (*models.Version, error) {
|
||||
@@ -171,31 +187,41 @@ func (r *queryResolver) Latestversion(ctx context.Context) (*models.ShortVersion
|
||||
|
||||
// Get scene marker tags which show up under the video.
|
||||
func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([]*models.SceneMarkerTag, error) {
|
||||
sceneID, _ := strconv.Atoi(scene_id)
|
||||
sqb := models.NewSceneMarkerQueryBuilder()
|
||||
sceneMarkers, err := sqb.FindBySceneID(sceneID, nil)
|
||||
sceneID, err := strconv.Atoi(scene_id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags := make(map[int]*models.SceneMarkerTag)
|
||||
var keys []int
|
||||
tqb := models.NewTagQueryBuilder()
|
||||
for _, sceneMarker := range sceneMarkers {
|
||||
markerPrimaryTag, err := tqb.Find(sceneMarker.PrimaryTagID, nil)
|
||||
tags := make(map[int]*models.SceneMarkerTag)
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
sceneMarkers, err := repo.SceneMarker().FindBySceneID(sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
_, hasKey := tags[markerPrimaryTag.ID]
|
||||
var sceneMarkerTag *models.SceneMarkerTag
|
||||
if !hasKey {
|
||||
sceneMarkerTag = &models.SceneMarkerTag{Tag: markerPrimaryTag}
|
||||
tags[markerPrimaryTag.ID] = sceneMarkerTag
|
||||
keys = append(keys, markerPrimaryTag.ID)
|
||||
} else {
|
||||
sceneMarkerTag = tags[markerPrimaryTag.ID]
|
||||
|
||||
tqb := repo.Tag()
|
||||
for _, sceneMarker := range sceneMarkers {
|
||||
markerPrimaryTag, err := tqb.Find(sceneMarker.PrimaryTagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, hasKey := tags[markerPrimaryTag.ID]
|
||||
var sceneMarkerTag *models.SceneMarkerTag
|
||||
if !hasKey {
|
||||
sceneMarkerTag = &models.SceneMarkerTag{Tag: markerPrimaryTag}
|
||||
tags[markerPrimaryTag.ID] = sceneMarkerTag
|
||||
keys = append(keys, markerPrimaryTag.ID)
|
||||
} else {
|
||||
sceneMarkerTag = tags[markerPrimaryTag.ID]
|
||||
}
|
||||
tags[markerPrimaryTag.ID].SceneMarkers = append(tags[markerPrimaryTag.ID].SceneMarkers, sceneMarker)
|
||||
}
|
||||
tags[markerPrimaryTag.ID].SceneMarkers = append(tags[markerPrimaryTag.ID].SceneMarkers, sceneMarker)
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort so that primary tags that show up earlier in the video are first.
|
||||
@@ -212,13 +238,3 @@ func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// wasFieldIncluded returns true if the given field was included in the request.
|
||||
// Slices are unmarshalled to empty slices even if the field was omitted. This
|
||||
// method determines if it was omitted altogether.
|
||||
func wasFieldIncluded(ctx context.Context, field string) bool {
|
||||
rctx := graphql.GetRequestContext(ctx)
|
||||
|
||||
_, ret := rctx.Variables[field]
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -22,30 +22,39 @@ func (r *galleryResolver) Title(ctx context.Context, obj *models.Gallery) (*stri
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) ([]*models.Image, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
|
||||
return qb.FindByGalleryID(obj.ID)
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (*models.Image, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
|
||||
imgs, err := qb.FindByGalleryID(obj.ID)
|
||||
if err != nil {
|
||||
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret []*models.Image, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Image().FindByGalleryID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret *models.Image
|
||||
if len(imgs) > 0 {
|
||||
ret = imgs[0]
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
for _, img := range imgs {
|
||||
if image.IsCover(img) {
|
||||
ret = img
|
||||
break
|
||||
func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *models.Image, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
imgs, err := repo.Image().FindByGalleryID(obj.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(imgs) > 0 {
|
||||
ret = imgs[0]
|
||||
}
|
||||
|
||||
for _, img := range imgs {
|
||||
if image.IsCover(img) {
|
||||
ret = img
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
@@ -81,35 +90,66 @@ func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Scene(ctx context.Context, obj *models.Gallery) (*models.Scene, error) {
|
||||
if !obj.SceneID.Valid {
|
||||
return nil, nil
|
||||
func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Scene().FindByGalleryID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
return qb.Find(int(obj.SceneID.Int64))
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (*models.Studio, error) {
|
||||
func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret *models.Studio, err error) {
|
||||
if !obj.StudioID.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.Find(int(obj.StudioID.Int64), nil)
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) ([]*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
return qb.FindByGalleryID(obj.ID, nil)
|
||||
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Tag().FindByGalleryID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) ([]*models.Performer, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
return qb.FindByGalleryID(obj.ID, nil)
|
||||
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Performer().FindByGalleryID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (int, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
return qb.CountByGalleryID(obj.ID)
|
||||
func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Image().CountByGalleryID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*models.Im
|
||||
|
||||
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*models.ImagePathsType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
builder := urlbuilders.NewImageURLBuilder(baseURL, obj.ID)
|
||||
builder := urlbuilders.NewImageURLBuilder(baseURL, obj)
|
||||
thumbnailPath := builder.GetThumbnailURL()
|
||||
imagePath := builder.GetImageURL()
|
||||
return &models.ImagePathsType{
|
||||
@@ -43,26 +43,51 @@ func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*models.I
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) ([]*models.Gallery, error) {
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
return qb.FindByImageID(obj.ID, nil)
|
||||
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret []*models.Gallery, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Gallery().FindByImageID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (*models.Studio, error) {
|
||||
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *models.Studio, err error) {
|
||||
if !obj.StudioID.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.Find(int(obj.StudioID.Int64), nil)
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) ([]*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
return qb.FindByImageID(obj.ID, nil)
|
||||
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindByImageID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) ([]*models.Performer, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
return qb.FindByImageID(obj.ID, nil)
|
||||
func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret []*models.Performer, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().FindByImageID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -53,10 +53,16 @@ func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, er
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (*models.Studio, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *models.Studio, err error) {
|
||||
if obj.StudioID.Valid {
|
||||
return qb.Find(int(obj.StudioID.Int64), nil)
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
@@ -78,18 +84,42 @@ func (r *movieResolver) Synopsis(ctx context.Context, obj *models.Movie) (*strin
|
||||
|
||||
func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
frontimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj.ID).GetMovieFrontImageURL()
|
||||
frontimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieFrontImageURL()
|
||||
return &frontimagePath, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
// don't return any thing if there is no back image
|
||||
var img []byte
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
img, err = repo.Movie().GetBackImage(obj.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if img == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
backimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj.ID).GetMovieBackImageURL()
|
||||
backimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).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)
|
||||
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Scene().CountByMovieID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
@@ -134,22 +136,112 @@ func (r *performerResolver) Favorite(ctx context.Context, obj *models.Performer)
|
||||
|
||||
func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
imagePath := urlbuilders.NewPerformerURLBuilder(baseURL, obj.ID).GetPerformerImageURL()
|
||||
imagePath := urlbuilders.NewPerformerURLBuilder(baseURL, obj).GetPerformerImageURL()
|
||||
return &imagePath, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
res, err := qb.CountByPerformerID(obj.ID)
|
||||
return &res, err
|
||||
func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindByPerformerID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) ([]*models.Scene, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
return qb.FindByPerformerID(obj.ID)
|
||||
func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Scene().CountByPerformerID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) {
|
||||
qb := models.NewJoinsQueryBuilder()
|
||||
return qb.GetPerformerStashIDs(obj.ID)
|
||||
func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = image.CountByPerformerID(repo.Image(), obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = gallery.CountByPerformerID(repo.Gallery(), obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Scene().FindByPerformerID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) (ret []*models.StashID, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().GetStashIDs(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Details(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Details.Valid {
|
||||
return &obj.Details.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.DeathDate.Valid {
|
||||
return &obj.DeathDate.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) HairColor(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.HairColor.Valid {
|
||||
return &obj.HairColor.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Weight(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
if obj.Weight.Valid {
|
||||
weight := int(obj.Weight.Int64)
|
||||
return &weight, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
@@ -78,11 +79,13 @@ func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.Sc
|
||||
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.ScenePathsType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
|
||||
builder.APIKey = config.GetInstance().GetAPIKey()
|
||||
screenshotPath := builder.GetScreenshotURL(obj.UpdatedAt.Timestamp)
|
||||
previewPath := builder.GetStreamPreviewURL()
|
||||
streamPath := builder.GetStreamURL()
|
||||
webpPath := builder.GetStreamPreviewImageURL()
|
||||
vttPath := builder.GetSpriteVTTURL()
|
||||
spritePath := builder.GetSpriteURL()
|
||||
chaptersVttPath := builder.GetChaptersVTTURL()
|
||||
return &models.ScenePathsType{
|
||||
Screenshot: &screenshotPath,
|
||||
@@ -91,67 +94,121 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.S
|
||||
Webp: &webpPath,
|
||||
Vtt: &vttPath,
|
||||
ChaptersVtt: &chaptersVttPath,
|
||||
Sprite: &spritePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) ([]*models.SceneMarker, error) {
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
return qb.FindBySceneID(obj.ID, nil)
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Gallery(ctx context.Context, obj *models.Scene) (*models.Gallery, error) {
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
return qb.FindBySceneID(obj.ID, nil)
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (*models.Studio, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
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 {
|
||||
func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (ret []*models.SceneMarker, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.SceneMarker().FindBySceneID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []*models.SceneMovie
|
||||
for _, sm := range sceneMovies {
|
||||
movie, err := qb.Find(sm.MovieID, nil)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Gallery().FindBySceneID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *models.Studio, err error) {
|
||||
if !obj.StudioID.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*models.SceneMovie, err error) {
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
mqb := repo.Movie()
|
||||
|
||||
sceneMovies, err := qb.GetMovies(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
sceneIdx := sm.SceneIndex
|
||||
sceneMovie := &models.SceneMovie{
|
||||
Movie: movie,
|
||||
for _, sm := range sceneMovies {
|
||||
movie, err := mqb.Find(sm.MovieID)
|
||||
if err != nil {
|
||||
return 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)
|
||||
}
|
||||
|
||||
if sceneIdx.Valid {
|
||||
var idx int
|
||||
idx = int(sceneIdx.Int64)
|
||||
sceneMovie.SceneIndex = &idx
|
||||
}
|
||||
|
||||
ret = append(ret, sceneMovie)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindBySceneID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) ([]*models.Performer, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
return qb.FindBySceneID(obj.ID, nil)
|
||||
func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().FindBySceneID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) ([]*models.StashID, error) {
|
||||
qb := models.NewJoinsQueryBuilder()
|
||||
return qb.GetSceneStashIDs(obj.ID)
|
||||
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Scene().GetStashIDs(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Phash(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
if obj.Phash.Valid {
|
||||
hexval := utils.PhashToString(obj.Phash.Int64)
|
||||
return &hexval, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -2,29 +2,47 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker) (*models.Scene, error) {
|
||||
func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker) (ret *models.Scene, err error) {
|
||||
if !obj.SceneID.Valid {
|
||||
panic("Invalid scene id")
|
||||
}
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
sceneID := int(obj.SceneID.Int64)
|
||||
scene, err := qb.Find(sceneID)
|
||||
return scene, err
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
sceneID := int(obj.SceneID.Int64)
|
||||
ret, err = repo.Scene().Find(sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneMarker) (*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
tag, err := qb.Find(obj.PrimaryTagID, nil)
|
||||
return tag, err
|
||||
func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneMarker) (ret *models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().Find(obj.PrimaryTagID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (r *sceneMarkerResolver) Tags(ctx context.Context, obj *models.SceneMarker) ([]*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
return qb.FindBySceneMarkerID(obj.ID, nil)
|
||||
func (r *sceneMarkerResolver) Tags(ctx context.Context, obj *models.SceneMarker) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().FindBySceneMarkerID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (r *sceneMarkerResolver) Stream(ctx context.Context, obj *models.SceneMarker) (string, error) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
@@ -23,31 +25,109 @@ func (r *studioResolver) URL(ctx context.Context, obj *models.Studio) (*string,
|
||||
|
||||
func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*string, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj.ID).GetStudioImageURL()
|
||||
imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj).GetStudioImageURL()
|
||||
|
||||
var hasImage bool
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
hasImage, err = repo.Studio().HasImage(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// indicate that image is missing by setting default query param to true
|
||||
if !hasImage {
|
||||
imagePath = imagePath + "?default=true"
|
||||
}
|
||||
|
||||
return &imagePath, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
res, err := qb.CountByStudioID(obj.ID)
|
||||
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = repo.Scene().CountByStudioID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (*models.Studio, error) {
|
||||
func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = image.CountByStudioID(repo.Image(), obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = gallery.CountByStudioID(repo.Gallery(), obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
|
||||
if !obj.ParentID.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.Find(int(obj.ParentID.Int64), nil)
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().Find(int(obj.ParentID.Int64))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) ([]*models.Studio, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.FindChildren(obj.ID, nil)
|
||||
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (ret []*models.Studio, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().FindChildren(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*models.StashID, error) {
|
||||
qb := models.NewJoinsQueryBuilder()
|
||||
return qb.GetStudioStashIDs(obj.ID)
|
||||
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) (ret []*models.StashID, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().GetStashIDs(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Details(ctx context.Context, obj *models.Studio) (*string, error) {
|
||||
if obj.Details.Valid {
|
||||
return &obj.Details.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -4,29 +4,73 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (*int, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
if obj == nil {
|
||||
return nil, nil
|
||||
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||
var count int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
count, err = repo.Scene().CountByTagID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count, err := qb.CountByTagID(obj.ID)
|
||||
|
||||
return &count, err
|
||||
}
|
||||
|
||||
func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (*int, error) {
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
if obj == nil {
|
||||
return nil, nil
|
||||
func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||
var count int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
count, err = repo.SceneMarker().CountByTagID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count, err := qb.CountByTagID(obj.ID)
|
||||
|
||||
return &count, err
|
||||
}
|
||||
|
||||
func (r *tagResolver) ImageCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = image.CountByTagID(repo.Image(), obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) GalleryCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||
var res int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
res, err = gallery.CountByTagID(repo.Gallery(), obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||
var count int
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
count, err = repo.Performer().CountByTagID(obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &count, err
|
||||
}
|
||||
|
||||
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj.ID).GetTagImageURL()
|
||||
imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj).GetTagImageURL()
|
||||
return &imagePath, nil
|
||||
}
|
||||
|
||||
@@ -13,15 +13,37 @@ import (
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) Setup(ctx context.Context, input models.SetupInput) (bool, error) {
|
||||
err := manager.GetInstance().Setup(input)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) Migrate(ctx context.Context, input models.MigrateInput) (bool, error) {
|
||||
err := manager.GetInstance().Migrate(input)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.ConfigGeneralInput) (*models.ConfigGeneralResult, error) {
|
||||
c := config.GetInstance()
|
||||
existingPaths := c.GetStashPaths()
|
||||
if len(input.Stashes) > 0 {
|
||||
for _, s := range input.Stashes {
|
||||
exists, err := utils.DirExists(s.Path)
|
||||
if !exists {
|
||||
return makeConfigGeneralResult(), err
|
||||
// Only validate existence of new paths
|
||||
isNew := true
|
||||
for _, path := range existingPaths {
|
||||
if path.Path == s.Path {
|
||||
isNew = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isNew {
|
||||
exists, err := utils.DirExists(s.Path)
|
||||
if !exists {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
}
|
||||
}
|
||||
config.Set(config.Stash, input.Stashes)
|
||||
c.Set(config.Stash, input.Stashes)
|
||||
}
|
||||
|
||||
if input.DatabasePath != nil {
|
||||
@@ -29,133 +51,140 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
if ext != ".db" && ext != ".sqlite" && ext != ".sqlite3" {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("invalid database path, use extension db, sqlite, or sqlite3")
|
||||
}
|
||||
config.Set(config.Database, input.DatabasePath)
|
||||
c.Set(config.Database, input.DatabasePath)
|
||||
}
|
||||
|
||||
if input.GeneratedPath != nil {
|
||||
if err := utils.EnsureDir(*input.GeneratedPath); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
config.Set(config.Generated, input.GeneratedPath)
|
||||
c.Set(config.Generated, input.GeneratedPath)
|
||||
}
|
||||
|
||||
if input.CachePath != nil {
|
||||
if err := utils.EnsureDir(*input.CachePath); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
if *input.CachePath != "" {
|
||||
if err := utils.EnsureDir(*input.CachePath); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
}
|
||||
config.Set(config.Cache, input.CachePath)
|
||||
c.Set(config.Cache, input.CachePath)
|
||||
}
|
||||
|
||||
if !input.CalculateMd5 && input.VideoFileNamingAlgorithm == models.HashAlgorithmMd5 {
|
||||
return makeConfigGeneralResult(), errors.New("calculateMD5 must be true if using MD5")
|
||||
}
|
||||
|
||||
if input.VideoFileNamingAlgorithm != config.GetVideoFileNamingAlgorithm() {
|
||||
if input.VideoFileNamingAlgorithm != c.GetVideoFileNamingAlgorithm() {
|
||||
// validate changing VideoFileNamingAlgorithm
|
||||
if err := manager.ValidateVideoFileNamingAlgorithm(input.VideoFileNamingAlgorithm); err != nil {
|
||||
if err := manager.ValidateVideoFileNamingAlgorithm(r.txnManager, input.VideoFileNamingAlgorithm); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
config.Set(config.VideoFileNamingAlgorithm, input.VideoFileNamingAlgorithm)
|
||||
c.Set(config.VideoFileNamingAlgorithm, input.VideoFileNamingAlgorithm)
|
||||
}
|
||||
|
||||
config.Set(config.CalculateMD5, input.CalculateMd5)
|
||||
c.Set(config.CalculateMD5, input.CalculateMd5)
|
||||
|
||||
if input.ParallelTasks != nil {
|
||||
c.Set(config.ParallelTasks, *input.ParallelTasks)
|
||||
}
|
||||
if input.PreviewSegments != nil {
|
||||
config.Set(config.PreviewSegments, *input.PreviewSegments)
|
||||
c.Set(config.PreviewSegments, *input.PreviewSegments)
|
||||
}
|
||||
if input.PreviewSegmentDuration != nil {
|
||||
config.Set(config.PreviewSegmentDuration, *input.PreviewSegmentDuration)
|
||||
c.Set(config.PreviewSegmentDuration, *input.PreviewSegmentDuration)
|
||||
}
|
||||
if input.PreviewExcludeStart != nil {
|
||||
config.Set(config.PreviewExcludeStart, *input.PreviewExcludeStart)
|
||||
c.Set(config.PreviewExcludeStart, *input.PreviewExcludeStart)
|
||||
}
|
||||
if input.PreviewExcludeEnd != nil {
|
||||
config.Set(config.PreviewExcludeEnd, *input.PreviewExcludeEnd)
|
||||
c.Set(config.PreviewExcludeEnd, *input.PreviewExcludeEnd)
|
||||
}
|
||||
if input.PreviewPreset != nil {
|
||||
config.Set(config.PreviewPreset, input.PreviewPreset.String())
|
||||
c.Set(config.PreviewPreset, input.PreviewPreset.String())
|
||||
}
|
||||
|
||||
if input.MaxTranscodeSize != nil {
|
||||
config.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||
c.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||
}
|
||||
|
||||
if input.MaxStreamingTranscodeSize != nil {
|
||||
config.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
|
||||
c.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
|
||||
}
|
||||
|
||||
if input.Username != nil {
|
||||
config.Set(config.Username, input.Username)
|
||||
c.Set(config.Username, input.Username)
|
||||
}
|
||||
|
||||
if input.Password != nil {
|
||||
// bit of a hack - check if the passed in password is the same as the stored hash
|
||||
// and only set if they are different
|
||||
currentPWHash := config.GetPasswordHash()
|
||||
currentPWHash := c.GetPasswordHash()
|
||||
|
||||
if *input.Password != currentPWHash {
|
||||
config.SetPassword(*input.Password)
|
||||
c.SetPassword(*input.Password)
|
||||
}
|
||||
}
|
||||
|
||||
if input.MaxSessionAge != nil {
|
||||
config.Set(config.MaxSessionAge, *input.MaxSessionAge)
|
||||
c.Set(config.MaxSessionAge, *input.MaxSessionAge)
|
||||
}
|
||||
|
||||
if input.LogFile != nil {
|
||||
config.Set(config.LogFile, input.LogFile)
|
||||
c.Set(config.LogFile, input.LogFile)
|
||||
}
|
||||
|
||||
config.Set(config.LogOut, input.LogOut)
|
||||
config.Set(config.LogAccess, input.LogAccess)
|
||||
c.Set(config.LogOut, input.LogOut)
|
||||
c.Set(config.LogAccess, input.LogAccess)
|
||||
|
||||
if input.LogLevel != config.GetLogLevel() {
|
||||
config.Set(config.LogLevel, input.LogLevel)
|
||||
if input.LogLevel != c.GetLogLevel() {
|
||||
c.Set(config.LogLevel, input.LogLevel)
|
||||
logger.SetLogLevel(input.LogLevel)
|
||||
}
|
||||
|
||||
if input.Excludes != nil {
|
||||
config.Set(config.Exclude, input.Excludes)
|
||||
c.Set(config.Exclude, input.Excludes)
|
||||
}
|
||||
|
||||
if input.ImageExcludes != nil {
|
||||
config.Set(config.ImageExclude, input.ImageExcludes)
|
||||
c.Set(config.ImageExclude, input.ImageExcludes)
|
||||
}
|
||||
|
||||
if input.VideoExtensions != nil {
|
||||
config.Set(config.VideoExtensions, input.VideoExtensions)
|
||||
c.Set(config.VideoExtensions, input.VideoExtensions)
|
||||
}
|
||||
|
||||
if input.ImageExtensions != nil {
|
||||
config.Set(config.ImageExtensions, input.ImageExtensions)
|
||||
c.Set(config.ImageExtensions, input.ImageExtensions)
|
||||
}
|
||||
|
||||
if input.GalleryExtensions != nil {
|
||||
config.Set(config.GalleryExtensions, input.GalleryExtensions)
|
||||
c.Set(config.GalleryExtensions, input.GalleryExtensions)
|
||||
}
|
||||
|
||||
config.Set(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
|
||||
c.Set(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
|
||||
|
||||
refreshScraperCache := false
|
||||
if input.ScraperUserAgent != nil {
|
||||
config.Set(config.ScraperUserAgent, input.ScraperUserAgent)
|
||||
c.Set(config.ScraperUserAgent, input.ScraperUserAgent)
|
||||
refreshScraperCache = true
|
||||
}
|
||||
|
||||
if input.ScraperCDPPath != nil {
|
||||
config.Set(config.ScraperCDPPath, input.ScraperCDPPath)
|
||||
c.Set(config.ScraperCDPPath, input.ScraperCDPPath)
|
||||
refreshScraperCache = true
|
||||
}
|
||||
|
||||
c.Set(config.ScraperCertCheck, input.ScraperCertCheck)
|
||||
|
||||
if input.StashBoxes != nil {
|
||||
if err := config.ValidateStashBoxes(input.StashBoxes); err != nil {
|
||||
if err := c.ValidateStashBoxes(input.StashBoxes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Set(config.StashBoxes, input.StashBoxes)
|
||||
c.Set(config.StashBoxes, input.StashBoxes)
|
||||
}
|
||||
|
||||
if err := config.Write(); err != nil {
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
@@ -168,32 +197,41 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) {
|
||||
c := config.GetInstance()
|
||||
if input.MenuItems != nil {
|
||||
c.Set(config.MenuItems, input.MenuItems)
|
||||
}
|
||||
|
||||
if input.SoundOnPreview != nil {
|
||||
config.Set(config.SoundOnPreview, *input.SoundOnPreview)
|
||||
c.Set(config.SoundOnPreview, *input.SoundOnPreview)
|
||||
}
|
||||
|
||||
if input.WallShowTitle != nil {
|
||||
config.Set(config.WallShowTitle, *input.WallShowTitle)
|
||||
c.Set(config.WallShowTitle, *input.WallShowTitle)
|
||||
}
|
||||
|
||||
if input.WallPlayback != nil {
|
||||
config.Set(config.WallPlayback, *input.WallPlayback)
|
||||
c.Set(config.WallPlayback, *input.WallPlayback)
|
||||
}
|
||||
|
||||
if input.MaximumLoopDuration != nil {
|
||||
config.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
|
||||
c.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
|
||||
}
|
||||
|
||||
if input.AutostartVideo != nil {
|
||||
config.Set(config.AutostartVideo, *input.AutostartVideo)
|
||||
c.Set(config.AutostartVideo, *input.AutostartVideo)
|
||||
}
|
||||
|
||||
if input.ShowStudioAsText != nil {
|
||||
config.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
|
||||
c.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
|
||||
}
|
||||
|
||||
if input.Language != nil {
|
||||
config.Set(config.Language, *input.Language)
|
||||
c.Set(config.Language, *input.Language)
|
||||
}
|
||||
|
||||
if input.SlideshowDelay != nil {
|
||||
c.Set(config.SlideshowDelay, *input.SlideshowDelay)
|
||||
}
|
||||
|
||||
css := ""
|
||||
@@ -202,15 +240,38 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
||||
css = *input.CSS
|
||||
}
|
||||
|
||||
config.SetCSS(css)
|
||||
c.SetCSS(css)
|
||||
|
||||
if input.CSSEnabled != nil {
|
||||
config.Set(config.CSSEnabled, *input.CSSEnabled)
|
||||
c.Set(config.CSSEnabled, *input.CSSEnabled)
|
||||
}
|
||||
|
||||
if err := config.Write(); err != nil {
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigInterfaceResult(), err
|
||||
}
|
||||
|
||||
return makeConfigInterfaceResult(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input models.GenerateAPIKeyInput) (string, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
var newAPIKey string
|
||||
if input.Clear == nil || !*input.Clear {
|
||||
username := c.GetUsername()
|
||||
if username != "" {
|
||||
var err error
|
||||
newAPIKey, err = manager.GenerateAPIKey(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(config.ApiKey, newAPIKey)
|
||||
if err := c.Write(); err != nil {
|
||||
return newAPIKey, err
|
||||
}
|
||||
|
||||
return newAPIKey, nil
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -61,108 +60,115 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.Galle
|
||||
newGallery.StudioID = sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
if input.SceneID != nil {
|
||||
sceneID, _ := strconv.ParseInt(*input.SceneID, 10, 64)
|
||||
newGallery.SceneID = sql.NullInt64{Int64: sceneID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
newGallery.SceneID = sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
// Start the transaction and save the performer
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
gallery, err := qb.Create(newGallery, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
var performerJoins []models.PerformersGalleries
|
||||
for _, pid := range input.PerformerIds {
|
||||
performerID, _ := strconv.Atoi(pid)
|
||||
performerJoin := models.PerformersGalleries{
|
||||
PerformerID: performerID,
|
||||
GalleryID: gallery.ID,
|
||||
// Start the transaction and save the gallery
|
||||
var gallery *models.Gallery
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Gallery()
|
||||
var err error
|
||||
gallery, err = qb.Create(newGallery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersGalleries(gallery.ID, performerJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
var tagJoins []models.GalleriesTags
|
||||
for _, tid := range input.TagIds {
|
||||
tagID, _ := strconv.Atoi(tid)
|
||||
tagJoin := models.GalleriesTags{
|
||||
GalleryID: gallery.ID,
|
||||
TagID: tagID,
|
||||
// Save the performers
|
||||
if err := r.updateGalleryPerformers(qb, gallery.ID, input.PerformerIds); err != nil {
|
||||
return err
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateGalleriesTags(gallery.ID, tagJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
// Save the tags
|
||||
if err := r.updateGalleryTags(qb, gallery.ID, input.TagIds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the scenes
|
||||
if err := r.updateGalleryScenes(qb, gallery.ID, input.SceneIds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gallery, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.GalleryUpdateInput) (*models.Gallery, error) {
|
||||
// Start the transaction and save the gallery
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
ret, err := r.galleryUpdate(input, tx)
|
||||
|
||||
func (r *mutationResolver) updateGalleryPerformers(qb models.GalleryReaderWriter, galleryID int, performerIDs []string) error {
|
||||
ids, err := utils.StringSliceToIntSlice(performerIDs)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return qb.UpdatePerformers(galleryID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.GalleryUpdateInput) ([]*models.Gallery, error) {
|
||||
func (r *mutationResolver) updateGalleryTags(qb models.GalleryReaderWriter, galleryID int, tagIDs []string) error {
|
||||
ids, err := utils.StringSliceToIntSlice(tagIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateTags(galleryID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateGalleryScenes(qb models.GalleryReaderWriter, galleryID int, sceneIDs []string) error {
|
||||
ids, err := utils.StringSliceToIntSlice(sceneIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateScenes(galleryID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.GalleryUpdateInput) (ret *models.Gallery, err error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Start the transaction and save the gallery
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
var ret []*models.Gallery
|
||||
|
||||
for _, gallery := range input {
|
||||
thisGallery, err := r.galleryUpdate(*gallery, tx)
|
||||
ret = append(ret, thisGallery)
|
||||
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
ret, err = r.galleryUpdate(input, translator, repo)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, tx *sqlx.Tx) (*models.Gallery, error) {
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.GalleryUpdateInput) (ret []*models.Gallery, err error) {
|
||||
inputMaps := getUpdateInputMaps(ctx)
|
||||
|
||||
// Start the transaction and save the gallery
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
for i, gallery := range input {
|
||||
translator := changesetTranslator{
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
thisGallery, err := r.galleryUpdate(*gallery, translator, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, thisGallery)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Gallery, error) {
|
||||
qb := repo.Gallery()
|
||||
|
||||
// Populate gallery from the input
|
||||
galleryID, _ := strconv.Atoi(input.ID)
|
||||
originalGallery, err := qb.Find(galleryID, nil)
|
||||
galleryID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
originalGallery, err := qb.Find(galleryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -176,6 +182,7 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, tx *sq
|
||||
ID: galleryID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
|
||||
if input.Title != nil {
|
||||
// ensure title is not empty
|
||||
if *input.Title == "" {
|
||||
@@ -190,65 +197,40 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, tx *sq
|
||||
|
||||
updatedGallery.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||
}
|
||||
if input.Details != nil {
|
||||
updatedGallery.Details = &sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
if input.URL != nil {
|
||||
updatedGallery.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Date != nil {
|
||||
updatedGallery.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
updatedGallery.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
// rating must be nullable
|
||||
updatedGallery.Rating = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedGallery.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
updatedGallery.StudioID = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
updatedGallery.Details = translator.nullString(input.Details, "details")
|
||||
updatedGallery.URL = translator.nullString(input.URL, "url")
|
||||
updatedGallery.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedGallery.Organized = input.Organized
|
||||
|
||||
// gallery scene is set from the scene only
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
gallery, err := qb.UpdatePartial(updatedGallery, tx)
|
||||
gallery, err := qb.UpdatePartial(updatedGallery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
var performerJoins []models.PerformersGalleries
|
||||
for _, pid := range input.PerformerIds {
|
||||
performerID, _ := strconv.Atoi(pid)
|
||||
performerJoin := models.PerformersGalleries{
|
||||
PerformerID: performerID,
|
||||
GalleryID: galleryID,
|
||||
if translator.hasField("performer_ids") {
|
||||
if err := r.updateGalleryPerformers(qb, galleryID, input.PerformerIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersGalleries(galleryID, performerJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
var tagJoins []models.GalleriesTags
|
||||
for _, tid := range input.TagIds {
|
||||
tagID, _ := strconv.Atoi(tid)
|
||||
tagJoin := models.GalleriesTags{
|
||||
GalleryID: galleryID,
|
||||
TagID: tagID,
|
||||
if translator.hasField("tag_ids") {
|
||||
if err := r.updateGalleryTags(qb, galleryID, input.TagIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateGalleriesTags(galleryID, tagJoins, tx); err != nil {
|
||||
return nil, err
|
||||
|
||||
// Save the scenes
|
||||
if translator.hasField("scene_ids") {
|
||||
if err := r.updateGalleryScenes(qb, galleryID, input.SceneIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return gallery, nil
|
||||
@@ -258,227 +240,181 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.B
|
||||
// Populate gallery from the input
|
||||
updatedTime := time.Now()
|
||||
|
||||
// Start the transaction and save the gallery marker
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
updatedGallery := models.GalleryPartial{
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
if input.Details != nil {
|
||||
updatedGallery.Details = &sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
if input.URL != nil {
|
||||
updatedGallery.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Date != nil {
|
||||
updatedGallery.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
if input.Rating != nil {
|
||||
// a rating of 0 means unset the rating
|
||||
if *input.Rating == 0 {
|
||||
updatedGallery.Rating = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
updatedGallery.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
}
|
||||
}
|
||||
if input.StudioID != nil {
|
||||
// empty string means unset the studio
|
||||
if *input.StudioID == "" {
|
||||
updatedGallery.StudioID = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedGallery.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
}
|
||||
}
|
||||
if input.SceneID != nil {
|
||||
// empty string means unset the studio
|
||||
if *input.SceneID == "" {
|
||||
updatedGallery.SceneID = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
sceneID, _ := strconv.ParseInt(*input.SceneID, 10, 64)
|
||||
updatedGallery.SceneID = &sql.NullInt64{Int64: sceneID, Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
updatedGallery.Details = translator.nullString(input.Details, "details")
|
||||
updatedGallery.URL = translator.nullString(input.URL, "url")
|
||||
updatedGallery.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedGallery.Organized = input.Organized
|
||||
|
||||
ret := []*models.Gallery{}
|
||||
|
||||
for _, galleryIDStr := range input.Ids {
|
||||
galleryID, _ := strconv.Atoi(galleryIDStr)
|
||||
updatedGallery.ID = galleryID
|
||||
// Start the transaction and save the galleries
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Gallery()
|
||||
|
||||
gallery, err := qb.UpdatePartial(updatedGallery, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
for _, galleryIDStr := range input.Ids {
|
||||
galleryID, _ := strconv.Atoi(galleryIDStr)
|
||||
updatedGallery.ID = galleryID
|
||||
|
||||
ret = append(ret, gallery)
|
||||
|
||||
// Save the performers
|
||||
if wasFieldIncluded(ctx, "performer_ids") {
|
||||
performerIDs, err := adjustGalleryPerformerIDs(tx, galleryID, *input.PerformerIds)
|
||||
gallery, err := qb.UpdatePartial(updatedGallery)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
var performerJoins []models.PerformersGalleries
|
||||
for _, performerID := range performerIDs {
|
||||
performerJoin := models.PerformersGalleries{
|
||||
PerformerID: performerID,
|
||||
GalleryID: galleryID,
|
||||
ret = append(ret, gallery)
|
||||
|
||||
// Save the performers
|
||||
if translator.hasField("performer_ids") {
|
||||
performerIDs, err := adjustGalleryPerformerIDs(qb, galleryID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdatePerformers(galleryID, performerIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersGalleries(galleryID, performerJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
tagIDs, err := adjustGalleryTagIDs(qb, galleryID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateTags(galleryID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the scenes
|
||||
if translator.hasField("scene_ids") {
|
||||
sceneIDs, err := adjustGallerySceneIDs(qb, galleryID, *input.SceneIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateScenes(galleryID, sceneIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if wasFieldIncluded(ctx, "tag_ids") {
|
||||
tagIDs, err := adjustGalleryTagIDs(tx, galleryID, *input.TagIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tagJoins []models.GalleriesTags
|
||||
for _, tagID := range tagIDs {
|
||||
tagJoin := models.GalleriesTags{
|
||||
GalleryID: galleryID,
|
||||
TagID: tagID,
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateGalleriesTags(galleryID, tagJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func adjustGalleryPerformerIDs(tx *sqlx.Tx, galleryID 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.GetGalleryPerformers(galleryID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range performerJoins {
|
||||
ret = append(ret, join.PerformerID)
|
||||
}
|
||||
func adjustGalleryPerformerIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetPerformerIDs(galleryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustGalleryTagIDs(tx *sqlx.Tx, galleryID int, ids models.BulkUpdateIds) ([]int, error) {
|
||||
var ret []int
|
||||
func adjustGalleryTagIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetTagIDs(galleryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
|
||||
// adding to the joins
|
||||
tagJoins, err := jqb.GetGalleryTags(galleryID, tx)
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range tagJoins {
|
||||
ret = append(ret, join.TagID)
|
||||
}
|
||||
func adjustGallerySceneIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetSceneIDs(galleryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.GalleryDestroyInput) (bool, error) {
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
iqb := models.NewImageQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
galleryIDs, err := utils.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var galleries []*models.Gallery
|
||||
var imgsToPostProcess []*models.Image
|
||||
var imgsToDelete []*models.Image
|
||||
|
||||
for _, id := range input.Ids {
|
||||
galleryID, _ := strconv.Atoi(id)
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Gallery()
|
||||
iqb := repo.Image()
|
||||
|
||||
for _, id := range galleryIDs {
|
||||
gallery, err := qb.Find(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
return fmt.Errorf("gallery with id %d not found", id)
|
||||
}
|
||||
|
||||
gallery, err := qb.Find(galleryID, tx)
|
||||
if gallery != nil {
|
||||
galleries = append(galleries, gallery)
|
||||
}
|
||||
err = qb.Destroy(galleryID, tx)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
// if this is a zip-based gallery, delete the images as well
|
||||
if gallery.Zip {
|
||||
imgs, err := iqb.FindByGalleryID(galleryID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, img := range imgs {
|
||||
err = iqb.Destroy(img.ID, tx)
|
||||
// if this is a zip-based gallery, delete the images as well first
|
||||
if gallery.Zip {
|
||||
imgs, err := iqb.FindByGalleryID(id)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
|
||||
imgsToPostProcess = append(imgsToPostProcess, img)
|
||||
}
|
||||
} else if input.DeleteFile != nil && *input.DeleteFile {
|
||||
// Delete image if it is only attached to this gallery
|
||||
imgs, err := iqb.FindByGalleryID(galleryID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, img := range imgs {
|
||||
imgGalleries, err := qb.FindByImageID(img.ID, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(imgGalleries) == 0 {
|
||||
err = iqb.Destroy(img.ID, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
for _, img := range imgs {
|
||||
if err := iqb.Destroy(img.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imgsToDelete = append(imgsToDelete, img)
|
||||
imgsToPostProcess = append(imgsToPostProcess, img)
|
||||
}
|
||||
} else if input.DeleteFile != nil && *input.DeleteFile {
|
||||
// Delete image if it is only attached to this gallery
|
||||
imgs, err := iqb.FindByGalleryID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, img := range imgs {
|
||||
imgGalleries, err := qb.FindByImageID(img.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(imgGalleries) == 0 {
|
||||
if err := iqb.Destroy(img.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imgsToDelete = append(imgsToDelete, img)
|
||||
imgsToPostProcess = append(imgsToPostProcess, img)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := qb.Destroy(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -506,34 +442,39 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.GalleryAddInput) (bool, error) {
|
||||
galleryID, _ := strconv.Atoi(input.GalleryID)
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
gallery, err := qb.Find(galleryID, nil)
|
||||
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
return false, errors.New("gallery not found")
|
||||
imageIDs, err := utils.StringSliceToIntSlice(input.ImageIds)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if gallery.Zip {
|
||||
return false, errors.New("cannot modify zip gallery images")
|
||||
}
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
for _, id := range input.ImageIds {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
_, err := jqb.AddImageGallery(imageID, galleryID, tx)
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Gallery()
|
||||
gallery, err := qb.Find(galleryID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
if gallery == nil {
|
||||
return errors.New("gallery not found")
|
||||
}
|
||||
|
||||
if gallery.Zip {
|
||||
return errors.New("cannot modify zip gallery images")
|
||||
}
|
||||
|
||||
newIDs, err := qb.GetImageIDs(galleryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newIDs = utils.IntAppendUniques(newIDs, imageIDs)
|
||||
return qb.UpdateImages(galleryID, newIDs)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -541,34 +482,39 @@ func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.Ga
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input models.GalleryRemoveInput) (bool, error) {
|
||||
galleryID, _ := strconv.Atoi(input.GalleryID)
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
gallery, err := qb.Find(galleryID, nil)
|
||||
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
return false, errors.New("gallery not found")
|
||||
imageIDs, err := utils.StringSliceToIntSlice(input.ImageIds)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if gallery.Zip {
|
||||
return false, errors.New("cannot modify zip gallery images")
|
||||
}
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
for _, id := range input.ImageIds {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
_, err := jqb.RemoveImageGallery(imageID, galleryID, tx)
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Gallery()
|
||||
gallery, err := qb.Find(galleryID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
if gallery == nil {
|
||||
return errors.New("gallery not found")
|
||||
}
|
||||
|
||||
if gallery.Zip {
|
||||
return errors.New("cannot modify zip gallery images")
|
||||
}
|
||||
|
||||
newIDs, err := qb.GetImageIDs(galleryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newIDs = utils.IntExclude(newIDs, imageIDs)
|
||||
return qb.UpdateImages(galleryID, newIDs)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,323 +2,257 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) ImageUpdate(ctx context.Context, input models.ImageUpdateInput) (*models.Image, error) {
|
||||
// Start the transaction and save the image
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
ret, err := r.imageUpdate(input, tx)
|
||||
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
func (r *mutationResolver) ImageUpdate(ctx context.Context, input models.ImageUpdateInput) (ret *models.Image, err error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
// Start the transaction and save the image
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
ret, err = r.imageUpdate(input, translator, repo)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*models.ImageUpdateInput) ([]*models.Image, error) {
|
||||
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*models.ImageUpdateInput) (ret []*models.Image, err error) {
|
||||
inputMaps := getUpdateInputMaps(ctx)
|
||||
|
||||
// Start the transaction and save the image
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
for i, image := range input {
|
||||
translator := changesetTranslator{
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
var ret []*models.Image
|
||||
thisImage, err := r.imageUpdate(*image, translator, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, image := range input {
|
||||
thisImage, err := r.imageUpdate(*image, tx)
|
||||
ret = append(ret, thisImage)
|
||||
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
ret = append(ret, thisImage)
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) imageUpdate(input models.ImageUpdateInput, tx *sqlx.Tx) (*models.Image, error) {
|
||||
func (r *mutationResolver) imageUpdate(input models.ImageUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Image, error) {
|
||||
// Populate image from the input
|
||||
imageID, _ := strconv.Atoi(input.ID)
|
||||
imageID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedTime := time.Now()
|
||||
updatedImage := models.ImagePartial{
|
||||
ID: imageID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
if input.Title != nil {
|
||||
updatedImage.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
updatedImage.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
// rating must be nullable
|
||||
updatedImage.Rating = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
updatedImage.Title = translator.nullString(input.Title, "title")
|
||||
updatedImage.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedImage.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedImage.Organized = input.Organized
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedImage.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
updatedImage.StudioID = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
qb := models.NewImageQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
image, err := qb.Update(updatedImage, tx)
|
||||
qb := repo.Image()
|
||||
image, err := qb.Update(updatedImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// don't set the galleries directly. Use add/remove gallery images interface instead
|
||||
if translator.hasField("gallery_ids") {
|
||||
if err := r.updateImageGalleries(qb, imageID, input.GalleryIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
var performerJoins []models.PerformersImages
|
||||
for _, pid := range input.PerformerIds {
|
||||
performerID, _ := strconv.Atoi(pid)
|
||||
performerJoin := models.PerformersImages{
|
||||
PerformerID: performerID,
|
||||
ImageID: imageID,
|
||||
if translator.hasField("performer_ids") {
|
||||
if err := r.updateImagePerformers(qb, imageID, input.PerformerIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersImages(imageID, performerJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
var tagJoins []models.ImagesTags
|
||||
for _, tid := range input.TagIds {
|
||||
tagID, _ := strconv.Atoi(tid)
|
||||
tagJoin := models.ImagesTags{
|
||||
ImageID: imageID,
|
||||
TagID: tagID,
|
||||
if translator.hasField("tag_ids") {
|
||||
if err := r.updateImageTags(qb, imageID, input.TagIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateImagesTags(imageID, tagJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input models.BulkImageUpdateInput) ([]*models.Image, error) {
|
||||
func (r *mutationResolver) updateImageGalleries(qb models.ImageReaderWriter, imageID int, galleryIDs []string) error {
|
||||
ids, err := utils.StringSliceToIntSlice(galleryIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateGalleries(imageID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateImagePerformers(qb models.ImageReaderWriter, imageID int, performerIDs []string) error {
|
||||
ids, err := utils.StringSliceToIntSlice(performerIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdatePerformers(imageID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateImageTags(qb models.ImageReaderWriter, imageID int, tagsIDs []string) error {
|
||||
ids, err := utils.StringSliceToIntSlice(tagsIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateTags(imageID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input models.BulkImageUpdateInput) (ret []*models.Image, err error) {
|
||||
imageIDs, err := utils.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Populate image from the input
|
||||
updatedTime := time.Now()
|
||||
|
||||
// Start the transaction and save the image marker
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewImageQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
|
||||
updatedImage := models.ImagePartial{
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
if input.Title != nil {
|
||||
updatedImage.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||
}
|
||||
if input.Rating != nil {
|
||||
// a rating of 0 means unset the rating
|
||||
if *input.Rating == 0 {
|
||||
updatedImage.Rating = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
updatedImage.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
}
|
||||
}
|
||||
if input.StudioID != nil {
|
||||
// empty string means unset the studio
|
||||
if *input.StudioID == "" {
|
||||
updatedImage.StudioID = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedImage.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
ret := []*models.Image{}
|
||||
updatedImage.Title = translator.nullString(input.Title, "title")
|
||||
updatedImage.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedImage.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedImage.Organized = input.Organized
|
||||
|
||||
for _, imageIDStr := range input.Ids {
|
||||
imageID, _ := strconv.Atoi(imageIDStr)
|
||||
updatedImage.ID = imageID
|
||||
// Start the transaction and save the image marker
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
|
||||
image, err := qb.Update(updatedImage, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
for _, imageID := range imageIDs {
|
||||
updatedImage.ID = imageID
|
||||
|
||||
ret = append(ret, image)
|
||||
|
||||
// Save the galleries
|
||||
if wasFieldIncluded(ctx, "gallery_ids") {
|
||||
galleryIDs, err := adjustImageGalleryIDs(tx, imageID, *input.GalleryIds)
|
||||
image, err := qb.Update(updatedImage)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
var galleryJoins []models.GalleriesImages
|
||||
for _, gid := range galleryIDs {
|
||||
galleryJoin := models.GalleriesImages{
|
||||
GalleryID: gid,
|
||||
ImageID: imageID,
|
||||
ret = append(ret, image)
|
||||
|
||||
// Save the galleries
|
||||
if translator.hasField("gallery_ids") {
|
||||
galleryIDs, err := adjustImageGalleryIDs(qb, imageID, *input.GalleryIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateGalleries(imageID, galleryIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
galleryJoins = append(galleryJoins, galleryJoin)
|
||||
}
|
||||
if err := jqb.UpdateGalleriesImages(imageID, galleryJoins, tx); err != nil {
|
||||
return nil, err
|
||||
|
||||
// Save the performers
|
||||
if translator.hasField("performer_ids") {
|
||||
performerIDs, err := adjustImagePerformerIDs(qb, imageID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdatePerformers(imageID, performerIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
tagIDs, err := adjustImageTagIDs(qb, imageID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateTags(imageID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
if wasFieldIncluded(ctx, "performer_ids") {
|
||||
performerIDs, err := adjustImagePerformerIDs(tx, imageID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var performerJoins []models.PerformersImages
|
||||
for _, performerID := range performerIDs {
|
||||
performerJoin := models.PerformersImages{
|
||||
PerformerID: performerID,
|
||||
ImageID: imageID,
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersImages(imageID, performerJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if wasFieldIncluded(ctx, "tag_ids") {
|
||||
tagIDs, err := adjustImageTagIDs(tx, imageID, *input.TagIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tagJoins []models.ImagesTags
|
||||
for _, tagID := range tagIDs {
|
||||
tagJoin := models.ImagesTags{
|
||||
ImageID: imageID,
|
||||
TagID: tagID,
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateImagesTags(imageID, tagJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func adjustImageGalleryIDs(tx *sqlx.Tx, imageID 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
|
||||
galleryJoins, err := jqb.GetImageGalleries(imageID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range galleryJoins {
|
||||
ret = append(ret, join.GalleryID)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustImagePerformerIDs(tx *sqlx.Tx, imageID 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.GetImagePerformers(imageID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range performerJoins {
|
||||
ret = append(ret, join.PerformerID)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustImageTagIDs(tx *sqlx.Tx, imageID 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.GetImageTags(imageID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range tagJoins {
|
||||
ret = append(ret, join.TagID)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageDestroyInput) (bool, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
imageID, _ := strconv.Atoi(input.ID)
|
||||
image, err := qb.Find(imageID)
|
||||
err = qb.Destroy(imageID, tx)
|
||||
|
||||
func adjustImageGalleryIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetGalleryIDs(imageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustImagePerformerIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetPerformerIDs(imageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustImageTagIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetTagIDs(imageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageDestroyInput) (ret bool, err error) {
|
||||
imageID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
var image *models.Image
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
|
||||
image, err = qb.Find(imageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if image == nil {
|
||||
return fmt.Errorf("image with id %d not found", imageID)
|
||||
}
|
||||
|
||||
return qb.Destroy(imageID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -337,27 +271,35 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.ImagesDestroyInput) (bool, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
var images []*models.Image
|
||||
for _, id := range input.Ids {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
|
||||
image, err := qb.Find(imageID)
|
||||
if image != nil {
|
||||
images = append(images, image)
|
||||
}
|
||||
err = qb.Destroy(imageID, tx)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.ImagesDestroyInput) (ret bool, err error) {
|
||||
imageIDs, err := utils.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
var images []*models.Image
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
|
||||
for _, imageID := range imageIDs {
|
||||
|
||||
image, err := qb.Find(imageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if image == nil {
|
||||
return fmt.Errorf("image with id %d not found", imageID)
|
||||
}
|
||||
|
||||
images = append(images, image)
|
||||
if err := qb.Destroy(imageID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -378,62 +320,56 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (int, error) {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewImageQueryBuilder()
|
||||
|
||||
newVal, err := qb.IncrementOCounter(imageID, tx)
|
||||
func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (ret int, err error) {
|
||||
imageID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
|
||||
ret, err = qb.IncrementOCounter(imageID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newVal, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (int, error) {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewImageQueryBuilder()
|
||||
|
||||
newVal, err := qb.DecrementOCounter(imageID, tx)
|
||||
func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (ret int, err error) {
|
||||
imageID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
|
||||
ret, err = qb.DecrementOCounter(imageID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newVal, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageResetO(ctx context.Context, id string) (int, error) {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewImageQueryBuilder()
|
||||
|
||||
newVal, err := qb.ResetOCounter(imageID, tx)
|
||||
func (r *mutationResolver) ImageResetO(ctx context.Context, id string) (ret int, err error) {
|
||||
imageID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Image()
|
||||
|
||||
ret, err = qb.ResetOCounter(imageID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newVal, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
|
||||
@@ -15,13 +20,20 @@ func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMe
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataImport(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Import()
|
||||
if err := manager.GetInstance().Import(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImportObjects(ctx context.Context, input models.ImportObjectsInput) (string, error) {
|
||||
t := manager.CreateImportTask(config.GetVideoFileNamingAlgorithm(), input)
|
||||
_, err := manager.GetInstance().RunSingleTask(t)
|
||||
t, err := manager.CreateImportTask(config.GetInstance().GetVideoFileNamingAlgorithm(), input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = manager.GetInstance().RunSingleTask(t)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -30,12 +42,15 @@ func (r *mutationResolver) ImportObjects(ctx context.Context, input models.Impor
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataExport(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Export()
|
||||
if err := manager.GetInstance().Export(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ExportObjects(ctx context.Context, input models.ExportObjectsInput) (*string, error) {
|
||||
t := manager.CreateExportTask(config.GetVideoFileNamingAlgorithm(), input)
|
||||
t := manager.CreateExportTask(config.GetInstance().GetVideoFileNamingAlgorithm(), input)
|
||||
wg, err := manager.GetInstance().RunSingleTask(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -61,12 +76,12 @@ func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.Ge
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
|
||||
manager.GetInstance().AutoTag(input.Performers, input.Studios, input.Tags)
|
||||
manager.GetInstance().AutoTag(input)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataClean(ctx context.Context) (string, error) {
|
||||
manager.GetInstance().Clean()
|
||||
func (r *mutationResolver) MetadataClean(ctx context.Context, input models.CleanMetadataInput) (string, error) {
|
||||
manager.GetInstance().Clean(input)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
@@ -89,3 +104,42 @@ func (r *mutationResolver) JobStatus(ctx context.Context) (*models.MetadataUpdat
|
||||
func (r *mutationResolver) StopJob(ctx context.Context) (bool, error) {
|
||||
return manager.GetInstance().Status.Stop(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.BackupDatabaseInput) (*string, error) {
|
||||
// if download is true, then backup to temporary file and return a link
|
||||
download := input.Download != nil && *input.Download
|
||||
mgr := manager.GetInstance()
|
||||
var backupPath string
|
||||
if download {
|
||||
utils.EnsureDir(mgr.Paths.Generated.Downloads)
|
||||
f, err := ioutil.TempFile(mgr.Paths.Generated.Downloads, "backup*.sqlite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backupPath = f.Name()
|
||||
f.Close()
|
||||
} else {
|
||||
backupPath = database.DatabaseBackupPath()
|
||||
}
|
||||
|
||||
err := database.Backup(database.DB, backupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if download {
|
||||
downloadHash := mgr.DownloadStore.RegisterFile(backupPath, "", false)
|
||||
logger.Debugf("Generated backup file %s with hash %s", backupPath, downloadHash)
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
fn := filepath.Base(database.DatabaseBackupPath())
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
||||
return &ret, nil
|
||||
} else {
|
||||
logger.Infof("Successfully backed up database to: %s", backupPath)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
@@ -27,7 +26,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
if input.FrontImage != nil {
|
||||
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
|
||||
frontimageData, err = utils.ProcessImageInput(*input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -35,7 +34,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
if input.BackImage != nil {
|
||||
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
|
||||
backimageData, err = utils.ProcessImageInput(*input.BackImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -85,24 +84,23 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(frontimageData) > 0 {
|
||||
if err := qb.UpdateMovieImages(movie.ID, frontimageData, backimageData, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
var movie *models.Movie
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Movie()
|
||||
movie, err = qb.Create(newMovie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
// update image table
|
||||
if len(frontimageData) > 0 {
|
||||
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -111,25 +109,32 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
||||
|
||||
func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUpdateInput) (*models.Movie, error) {
|
||||
// Populate movie from the input
|
||||
movieID, _ := strconv.Atoi(input.ID)
|
||||
movieID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedMovie := models.MoviePartial{
|
||||
ID: movieID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
var frontimageData []byte
|
||||
var err error
|
||||
frontImageIncluded := wasFieldIncluded(ctx, "front_image")
|
||||
frontImageIncluded := translator.hasField("front_image")
|
||||
if input.FrontImage != nil {
|
||||
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
|
||||
frontimageData, err = utils.ProcessImageInput(*input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
backImageIncluded := wasFieldIncluded(ctx, "back_image")
|
||||
backImageIncluded := translator.hasField("back_image")
|
||||
var backimageData []byte
|
||||
if input.BackImage != nil {
|
||||
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
|
||||
backimageData, err = utils.ProcessImageInput(*input.BackImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -142,94 +147,59 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
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}
|
||||
}
|
||||
updatedMovie.Aliases = translator.nullString(input.Aliases, "aliases")
|
||||
updatedMovie.Duration = translator.nullInt64(input.Duration, "duration")
|
||||
updatedMovie.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedMovie.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedMovie.Director = translator.nullString(input.Director, "director")
|
||||
updatedMovie.Synopsis = translator.nullString(input.Synopsis, "synopsis")
|
||||
updatedMovie.URL = translator.nullString(input.URL, "url")
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// update image table
|
||||
if frontImageIncluded || backImageIncluded {
|
||||
if !frontImageIncluded {
|
||||
frontimageData, err = qb.GetFrontImage(updatedMovie.ID, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
var movie *models.Movie
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Movie()
|
||||
movie, err = qb.Update(updatedMovie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !backImageIncluded {
|
||||
backimageData, err = qb.GetBackImage(updatedMovie.ID, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
|
||||
// update image table
|
||||
if frontImageIncluded || backImageIncluded {
|
||||
if !frontImageIncluded {
|
||||
frontimageData, err = qb.GetFrontImage(updatedMovie.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !backImageIncluded {
|
||||
backimageData, err = qb.GetBackImage(updatedMovie.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(frontimageData) == 0 && len(backimageData) == 0 {
|
||||
// both images are being nulled. Destroy them.
|
||||
if err := qb.DestroyImages(movie.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// HACK - if front image is null and back image is not null, then set the front image
|
||||
// to the default image since we can't have a null front image and a non-null back image
|
||||
if frontimageData == nil && backimageData != nil {
|
||||
frontimageData, _ = utils.ProcessImageInput(models.DefaultMovieImage)
|
||||
}
|
||||
|
||||
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(frontimageData) == 0 && len(backimageData) == 0 {
|
||||
// both images are being nulled. Destroy them.
|
||||
if err := qb.DestroyMovieImages(movie.ID, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// HACK - if front image is null and back image is not null, then set the front image
|
||||
// to the default image since we can't have a null front image and a non-null back image
|
||||
if frontimageData == nil && backimageData != nil {
|
||||
_, frontimageData, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
|
||||
}
|
||||
|
||||
if err := qb.UpdateMovieImages(movie.ID, frontimageData, backimageData, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -237,13 +207,35 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
||||
}
|
||||
|
||||
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()
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
return repo.Movie().Destroy(id)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string) (bool, error) {
|
||||
ids, err := utils.StringSliceToIntSlice(movieIDs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Movie()
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
|
||||
@@ -3,11 +3,12 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -19,7 +20,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
var err error
|
||||
|
||||
if input.Image != nil {
|
||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -84,43 +85,64 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
} else {
|
||||
newPerformer.Favorite = sql.NullBool{Bool: false, Valid: true}
|
||||
}
|
||||
if input.Rating != nil {
|
||||
newPerformer.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
newPerformer.Rating = sql.NullInt64{Valid: false}
|
||||
}
|
||||
if input.Details != nil {
|
||||
newPerformer.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
if input.DeathDate != nil {
|
||||
newPerformer.DeathDate = models.SQLiteDate{String: *input.DeathDate, Valid: true}
|
||||
}
|
||||
if input.HairColor != nil {
|
||||
newPerformer.HairColor = sql.NullString{String: *input.HairColor, Valid: true}
|
||||
}
|
||||
if input.Weight != nil {
|
||||
weight := int64(*input.Weight)
|
||||
newPerformer.Weight = sql.NullInt64{Int64: weight, Valid: true}
|
||||
}
|
||||
|
||||
if err := performer.ValidateDeathDate(nil, input.Birthdate, input.DeathDate); err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Start the transaction and save the performer
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
var performer *models.Performer
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Performer()
|
||||
|
||||
performer, err := qb.Create(newPerformer, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdatePerformerImage(performer.ID, imageData, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
performer, err = qb.Create(newPerformer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
var stashIDJoins []models.StashID
|
||||
for _, stashID := range input.StashIds {
|
||||
newJoin := models.StashID{
|
||||
StashID: stashID.StashID,
|
||||
Endpoint: stashID.Endpoint,
|
||||
if len(input.TagIds) > 0 {
|
||||
if err := r.updatePerformerTags(qb, performer.ID, input.TagIds); err != nil {
|
||||
return err
|
||||
}
|
||||
stashIDJoins = append(stashIDJoins, newJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformerStashIDs(performer.ID, stashIDJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(performer.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(performer.ID, stashIDJoins); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -130,133 +152,259 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) {
|
||||
// Populate performer from the input
|
||||
performerID, _ := strconv.Atoi(input.ID)
|
||||
updatedPerformer := models.Performer{
|
||||
updatedPerformer := models.PerformerPartial{
|
||||
ID: performerID,
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: time.Now()},
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
var imageData []byte
|
||||
var err error
|
||||
imageIncluded := wasFieldIncluded(ctx, "image")
|
||||
imageIncluded := translator.hasField("image")
|
||||
if input.Image != nil {
|
||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if input.Name != nil {
|
||||
// generate checksum from performer name rather than image
|
||||
checksum := utils.MD5FromString(*input.Name)
|
||||
|
||||
updatedPerformer.Name = sql.NullString{String: *input.Name, Valid: true}
|
||||
updatedPerformer.Checksum = checksum
|
||||
}
|
||||
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}
|
||||
}
|
||||
if input.Ethnicity != nil {
|
||||
updatedPerformer.Ethnicity = sql.NullString{String: *input.Ethnicity, Valid: true}
|
||||
}
|
||||
if input.Country != nil {
|
||||
updatedPerformer.Country = sql.NullString{String: *input.Country, Valid: true}
|
||||
}
|
||||
if input.EyeColor != nil {
|
||||
updatedPerformer.EyeColor = sql.NullString{String: *input.EyeColor, Valid: true}
|
||||
}
|
||||
if input.Height != nil {
|
||||
updatedPerformer.Height = sql.NullString{String: *input.Height, Valid: true}
|
||||
}
|
||||
if input.Measurements != nil {
|
||||
updatedPerformer.Measurements = sql.NullString{String: *input.Measurements, Valid: true}
|
||||
}
|
||||
if input.FakeTits != nil {
|
||||
updatedPerformer.FakeTits = sql.NullString{String: *input.FakeTits, Valid: true}
|
||||
}
|
||||
if input.CareerLength != nil {
|
||||
updatedPerformer.CareerLength = sql.NullString{String: *input.CareerLength, Valid: true}
|
||||
}
|
||||
if input.Tattoos != nil {
|
||||
updatedPerformer.Tattoos = sql.NullString{String: *input.Tattoos, Valid: true}
|
||||
}
|
||||
if input.Piercings != nil {
|
||||
updatedPerformer.Piercings = sql.NullString{String: *input.Piercings, Valid: true}
|
||||
}
|
||||
if input.Aliases != nil {
|
||||
updatedPerformer.Aliases = sql.NullString{String: *input.Aliases, Valid: true}
|
||||
}
|
||||
if input.Twitter != nil {
|
||||
updatedPerformer.Twitter = sql.NullString{String: *input.Twitter, Valid: true}
|
||||
}
|
||||
if input.Instagram != nil {
|
||||
updatedPerformer.Instagram = sql.NullString{String: *input.Instagram, Valid: true}
|
||||
}
|
||||
if input.Favorite != nil {
|
||||
updatedPerformer.Favorite = sql.NullBool{Bool: *input.Favorite, Valid: true}
|
||||
} else {
|
||||
updatedPerformer.Favorite = sql.NullBool{Bool: false, Valid: true}
|
||||
updatedPerformer.Name = &sql.NullString{String: *input.Name, Valid: true}
|
||||
updatedPerformer.Checksum = &checksum
|
||||
}
|
||||
|
||||
// Start the transaction and save the performer
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
updatedPerformer.URL = translator.nullString(input.URL, "url")
|
||||
|
||||
performer, err := qb.Update(updatedPerformer, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdatePerformerImage(performer.ID, imageData, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
} else if imageIncluded {
|
||||
// must be unsetting
|
||||
if err := qb.DestroyPerformerImage(performer.ID, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
if translator.hasField("gender") {
|
||||
if input.Gender != nil {
|
||||
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
|
||||
} else {
|
||||
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
var stashIDJoins []models.StashID
|
||||
for _, stashID := range input.StashIds {
|
||||
newJoin := models.StashID{
|
||||
StashID: stashID.StashID,
|
||||
Endpoint: stashID.Endpoint,
|
||||
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
|
||||
updatedPerformer.Country = translator.nullString(input.Country, "country")
|
||||
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
|
||||
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
|
||||
updatedPerformer.Height = translator.nullString(input.Height, "height")
|
||||
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
|
||||
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
|
||||
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
|
||||
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
|
||||
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedPerformer.Details = translator.nullString(input.Details, "details")
|
||||
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
|
||||
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
|
||||
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
|
||||
|
||||
// Start the transaction and save the p
|
||||
var p *models.Performer
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Performer()
|
||||
|
||||
// need to get existing performer
|
||||
existing, err := qb.Find(updatedPerformer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
return fmt.Errorf("performer with id %d not found", updatedPerformer.ID)
|
||||
}
|
||||
|
||||
if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stashIDJoins = append(stashIDJoins, newJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformerStashIDs(performerID, stashIDJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
p, err = qb.Update(updatedPerformer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
if err := r.updatePerformerTags(qb, p.ID, input.TagIds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(p.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if imageIncluded {
|
||||
// must be unsetting
|
||||
if err := qb.DestroyImage(p.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if translator.hasField("stash_ids") {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(performerID, stashIDJoins); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return performer, nil
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updatePerformerTags(qb models.PerformerReaderWriter, performerID int, tagsIDs []string) error {
|
||||
ids, err := utils.StringSliceToIntSlice(tagsIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateTags(performerID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models.BulkPerformerUpdateInput) ([]*models.Performer, error) {
|
||||
performerIDs, err := utils.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Populate performer from the input
|
||||
updatedTime := time.Now()
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
updatedPerformer := models.PerformerPartial{
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
|
||||
updatedPerformer.URL = translator.nullString(input.URL, "url")
|
||||
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
|
||||
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
|
||||
updatedPerformer.Country = translator.nullString(input.Country, "country")
|
||||
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
|
||||
updatedPerformer.Height = translator.nullString(input.Height, "height")
|
||||
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
|
||||
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
|
||||
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
|
||||
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
|
||||
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
|
||||
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
||||
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
|
||||
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedPerformer.Details = translator.nullString(input.Details, "details")
|
||||
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
|
||||
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
|
||||
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
|
||||
|
||||
if translator.hasField("gender") {
|
||||
if input.Gender != nil {
|
||||
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
|
||||
} else {
|
||||
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
|
||||
}
|
||||
}
|
||||
|
||||
ret := []*models.Performer{}
|
||||
|
||||
// Start the transaction and save the scene marker
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Performer()
|
||||
|
||||
for _, performerID := range performerIDs {
|
||||
updatedPerformer.ID = performerID
|
||||
|
||||
// need to get existing performer
|
||||
existing, err := qb.Find(performerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
return fmt.Errorf("performer with id %d not found", performerID)
|
||||
}
|
||||
|
||||
if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
performer, err := qb.Update(updatedPerformer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, performer)
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
tagIDs, err := adjustTagIDs(qb, performerID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateTags(performerID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) PerformerDestroy(ctx context.Context, input models.PerformerDestroyInput) (bool, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
if err := qb.Destroy(input.ID, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
return repo.Performer().Destroy(id)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) PerformersDestroy(ctx context.Context, performerIDs []string) (bool, error) {
|
||||
ids, err := utils.StringSliceToIntSlice(performerIDs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Performer()
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
|
||||
@@ -23,6 +23,7 @@ func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, t
|
||||
}
|
||||
}
|
||||
|
||||
config := config.GetInstance()
|
||||
serverConnection := common.StashServerConnection{
|
||||
Scheme: "http",
|
||||
Port: config.GetPort(),
|
||||
|
||||
@@ -3,64 +3,64 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (*models.Scene, error) {
|
||||
// Start the transaction and save the scene
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
ret, err := r.sceneUpdate(input, tx)
|
||||
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (ret *models.Scene, err error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
// Start the transaction and save the scene
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
ret, err = r.sceneUpdate(input, translator, repo)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.SceneUpdateInput) ([]*models.Scene, error) {
|
||||
func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.SceneUpdateInput) (ret []*models.Scene, err error) {
|
||||
inputMaps := getUpdateInputMaps(ctx)
|
||||
|
||||
// Start the transaction and save the scene
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
for i, scene := range input {
|
||||
translator := changesetTranslator{
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
var ret []*models.Scene
|
||||
thisScene, err := r.sceneUpdate(*scene, translator, repo)
|
||||
ret = append(ret, thisScene)
|
||||
|
||||
for _, scene := range input {
|
||||
thisScene, err := r.sceneUpdate(*scene, tx)
|
||||
ret = append(ret, thisScene)
|
||||
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.Tx) (*models.Scene, error) {
|
||||
func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Scene, error) {
|
||||
// Populate scene from the input
|
||||
sceneID, _ := strconv.Atoi(input.ID)
|
||||
sceneID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var coverImageData []byte
|
||||
|
||||
@@ -69,22 +69,18 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.T
|
||||
ID: sceneID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
if input.Title != nil {
|
||||
updatedScene.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||
}
|
||||
if input.Details != nil {
|
||||
updatedScene.Details = &sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
if input.URL != nil {
|
||||
updatedScene.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Date != nil {
|
||||
updatedScene.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
|
||||
updatedScene.Title = translator.nullString(input.Title, "title")
|
||||
updatedScene.Details = translator.nullString(input.Details, "details")
|
||||
updatedScene.URL = translator.nullString(input.URL, "url")
|
||||
updatedScene.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedScene.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedScene.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedScene.Organized = input.Organized
|
||||
|
||||
if input.CoverImage != nil && *input.CoverImage != "" {
|
||||
var err error
|
||||
_, coverImageData, err = utils.ProcessBase64Image(*input.CoverImage)
|
||||
coverImageData, err = utils.ProcessImageInput(*input.CoverImage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -92,81 +88,85 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.T
|
||||
// update the cover after updating the scene
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
updatedScene.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
// rating must be nullable
|
||||
updatedScene.Rating = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedScene.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
updatedScene.StudioID = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
scene, err := qb.Update(updatedScene, tx)
|
||||
qb := repo.Scene()
|
||||
scene, err := qb.Update(updatedScene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update cover table
|
||||
if len(coverImageData) > 0 {
|
||||
if err := qb.UpdateSceneCover(sceneID, coverImageData, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the existing gallery value
|
||||
gqb := models.NewGalleryQueryBuilder()
|
||||
err = gqb.ClearGalleryId(sceneID, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if input.GalleryID != nil {
|
||||
// Save the gallery
|
||||
galleryID, _ := strconv.Atoi(*input.GalleryID)
|
||||
updatedGallery := models.Gallery{
|
||||
ID: galleryID,
|
||||
SceneID: sql.NullInt64{Int64: int64(sceneID), Valid: true},
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
gqb := models.NewGalleryQueryBuilder()
|
||||
_, err := gqb.Update(updatedGallery, tx)
|
||||
if err != nil {
|
||||
if err := qb.UpdateCover(sceneID, coverImageData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
var performerJoins []models.PerformersScenes
|
||||
for _, pid := range input.PerformerIds {
|
||||
performerID, _ := strconv.Atoi(pid)
|
||||
performerJoin := models.PerformersScenes{
|
||||
PerformerID: performerID,
|
||||
SceneID: sceneID,
|
||||
if translator.hasField("performer_ids") {
|
||||
if err := r.updateScenePerformers(qb, sceneID, input.PerformerIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersScenes(sceneID, performerJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the movies
|
||||
if translator.hasField("movies") {
|
||||
if err := r.updateSceneMovies(qb, sceneID, input.Movies); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
if err := r.updateSceneTags(qb, sceneID, input.TagIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the galleries
|
||||
if translator.hasField("gallery_ids") {
|
||||
if err := r.updateSceneGalleries(qb, sceneID, input.GalleryIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if translator.hasField("stash_ids") {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(sceneID, stashIDJoins); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// only update the cover image if provided and everything else was successful
|
||||
if coverImageData != nil {
|
||||
err = manager.SetSceneScreenshot(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return scene, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateScenePerformers(qb models.SceneReaderWriter, sceneID int, performerIDs []string) error {
|
||||
ids, err := utils.StringSliceToIntSlice(performerIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdatePerformers(sceneID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) updateSceneMovies(qb models.SceneReaderWriter, sceneID int, movies []*models.SceneMovieInput) error {
|
||||
var movieJoins []models.MoviesScenes
|
||||
|
||||
for _, movie := range input.Movies {
|
||||
|
||||
movieID, _ := strconv.Atoi(movie.MovieID)
|
||||
for _, movie := range movies {
|
||||
movieID, err := strconv.Atoi(movie.MovieID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
movieJoin := models.MoviesScenes{
|
||||
MovieID: movieID,
|
||||
SceneID: sceneID,
|
||||
}
|
||||
|
||||
if movie.SceneIndex != nil {
|
||||
@@ -178,169 +178,106 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.T
|
||||
|
||||
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 {
|
||||
tagID, _ := strconv.Atoi(tid)
|
||||
tagJoin := models.ScenesTags{
|
||||
SceneID: sceneID,
|
||||
TagID: tagID,
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateScenesTags(sceneID, tagJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return qb.UpdateMovies(sceneID, movieJoins)
|
||||
}
|
||||
|
||||
// only update the cover image if provided and everything else was successful
|
||||
if coverImageData != nil {
|
||||
err = manager.SetSceneScreenshot(scene.GetHash(config.GetVideoFileNamingAlgorithm()), coverImageData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (r *mutationResolver) updateSceneTags(qb models.SceneReaderWriter, sceneID int, tagsIDs []string) error {
|
||||
ids, err := utils.StringSliceToIntSlice(tagsIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return qb.UpdateTags(sceneID, ids)
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
var stashIDJoins []models.StashID
|
||||
for _, stashID := range input.StashIds {
|
||||
newJoin := models.StashID{
|
||||
StashID: stashID.StashID,
|
||||
Endpoint: stashID.Endpoint,
|
||||
}
|
||||
stashIDJoins = append(stashIDJoins, newJoin)
|
||||
}
|
||||
if err := jqb.UpdateSceneStashIDs(sceneID, stashIDJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (r *mutationResolver) updateSceneGalleries(qb models.SceneReaderWriter, sceneID int, galleryIDs []string) error {
|
||||
ids, err := utils.StringSliceToIntSlice(galleryIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return scene, nil
|
||||
return qb.UpdateGalleries(sceneID, ids)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.BulkSceneUpdateInput) ([]*models.Scene, error) {
|
||||
sceneIDs, err := utils.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Populate scene from the input
|
||||
updatedTime := time.Now()
|
||||
|
||||
// Start the transaction and save the scene marker
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
updatedScene := models.ScenePartial{
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
if input.Title != nil {
|
||||
updatedScene.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||
}
|
||||
if input.Details != nil {
|
||||
updatedScene.Details = &sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
if input.URL != nil {
|
||||
updatedScene.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Date != nil {
|
||||
updatedScene.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
if input.Rating != nil {
|
||||
// a rating of 0 means unset the rating
|
||||
if *input.Rating == 0 {
|
||||
updatedScene.Rating = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
updatedScene.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
}
|
||||
}
|
||||
if input.StudioID != nil {
|
||||
// empty string means unset the studio
|
||||
if *input.StudioID == "" {
|
||||
updatedScene.StudioID = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedScene.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
updatedScene.Title = translator.nullString(input.Title, "title")
|
||||
updatedScene.Details = translator.nullString(input.Details, "details")
|
||||
updatedScene.URL = translator.nullString(input.URL, "url")
|
||||
updatedScene.Date = translator.sqliteDate(input.Date, "date")
|
||||
updatedScene.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
updatedScene.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
|
||||
updatedScene.Organized = input.Organized
|
||||
|
||||
ret := []*models.Scene{}
|
||||
|
||||
for _, sceneIDStr := range input.Ids {
|
||||
sceneID, _ := strconv.Atoi(sceneIDStr)
|
||||
updatedScene.ID = sceneID
|
||||
// Start the transaction and save the scene marker
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
|
||||
scene, err := qb.Update(updatedScene, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
for _, sceneID := range sceneIDs {
|
||||
updatedScene.ID = sceneID
|
||||
|
||||
ret = append(ret, scene)
|
||||
|
||||
if input.GalleryID != nil {
|
||||
// Save the gallery
|
||||
galleryID, _ := strconv.Atoi(*input.GalleryID)
|
||||
updatedGallery := models.Gallery{
|
||||
ID: galleryID,
|
||||
SceneID: sql.NullInt64{Int64: int64(sceneID), Valid: true},
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
gqb := models.NewGalleryQueryBuilder()
|
||||
_, err := gqb.Update(updatedGallery, tx)
|
||||
scene, err := qb.Update(updatedScene)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
if wasFieldIncluded(ctx, "performer_ids") {
|
||||
performerIDs, err := adjustScenePerformerIDs(tx, sceneID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
var performerJoins []models.PerformersScenes
|
||||
for _, performerID := range performerIDs {
|
||||
performerJoin := models.PerformersScenes{
|
||||
PerformerID: performerID,
|
||||
SceneID: sceneID,
|
||||
ret = append(ret, scene)
|
||||
|
||||
// Save the performers
|
||||
if translator.hasField("performer_ids") {
|
||||
performerIDs, err := adjustScenePerformerIDs(qb, sceneID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersScenes(sceneID, performerJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 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 _, tagID := range tagIDs {
|
||||
tagJoin := models.ScenesTags{
|
||||
SceneID: sceneID,
|
||||
TagID: tagID,
|
||||
if err := qb.UpdatePerformers(sceneID, performerIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateScenesTags(sceneID, tagJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
tagIDs, err := adjustTagIDs(qb, sceneID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateTags(sceneID, tagIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the galleries
|
||||
if translator.hasField("gallery_ids") {
|
||||
galleryIDs, err := adjustSceneGalleryIDs(qb, sceneID, *input.GalleryIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.UpdateGalleries(sceneID, galleryIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -348,6 +285,17 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
|
||||
}
|
||||
|
||||
func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
|
||||
// if we are setting the ids, just return the ids
|
||||
if updateIDs.Mode == models.BulkUpdateIDModeSet {
|
||||
existingIDs = []int{}
|
||||
for _, idStr := range updateIDs.Ids {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
existingIDs = append(existingIDs, id)
|
||||
}
|
||||
|
||||
return existingIDs
|
||||
}
|
||||
|
||||
for _, idStr := range updateIDs.Ids {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
|
||||
@@ -373,67 +321,70 @@ func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
|
||||
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)
|
||||
}
|
||||
func adjustScenePerformerIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetPerformerIDs(sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustSceneTagIDs(tx *sqlx.Tx, sceneID int, ids models.BulkUpdateIds) ([]int, error) {
|
||||
var ret []int
|
||||
type tagIDsGetter interface {
|
||||
GetTagIDs(id int) ([]int, error)
|
||||
}
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
|
||||
// adding to the joins
|
||||
tagJoins, err := jqb.GetSceneTags(sceneID, tx)
|
||||
func adjustTagIDs(qb tagIDsGetter, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetTagIDs(sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
for _, join := range tagJoins {
|
||||
ret = append(ret, join.TagID)
|
||||
}
|
||||
func adjustSceneGalleryIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||
ret, err = qb.GetGalleryIDs(sceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
sceneID, _ := strconv.Atoi(input.ID)
|
||||
scene, err := qb.Find(sceneID)
|
||||
err = manager.DestroyScene(sceneID, tx)
|
||||
|
||||
sceneID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
var scene *models.Scene
|
||||
var postCommitFunc func()
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
var err error
|
||||
scene, err = qb.Find(sceneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if scene == nil {
|
||||
return fmt.Errorf("scene with id %d not found", sceneID)
|
||||
}
|
||||
|
||||
postCommitFunc, err = manager.DestroyScene(scene, repo)
|
||||
return err
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// perform the post-commit actions
|
||||
postCommitFunc()
|
||||
|
||||
// if delete generated is true, then delete the generated files
|
||||
// for the scene
|
||||
if input.DeleteGenerated != nil && *input.DeleteGenerated {
|
||||
manager.DeleteGeneratedSceneFiles(scene, config.GetVideoFileNamingAlgorithm())
|
||||
manager.DeleteGeneratedSceneFiles(scene, config.GetInstance().GetVideoFileNamingAlgorithm())
|
||||
}
|
||||
|
||||
// if delete file is true, then delete the file as well
|
||||
@@ -446,30 +397,36 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.ScenesDestroyInput) (bool, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
var scenes []*models.Scene
|
||||
for _, id := range input.Ids {
|
||||
sceneID, _ := strconv.Atoi(id)
|
||||
var postCommitFuncs []func()
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
|
||||
scene, err := qb.Find(sceneID)
|
||||
if scene != nil {
|
||||
scenes = append(scenes, scene)
|
||||
for _, id := range input.Ids {
|
||||
sceneID, _ := strconv.Atoi(id)
|
||||
|
||||
scene, err := qb.Find(sceneID)
|
||||
if scene != nil {
|
||||
scenes = append(scenes, scene)
|
||||
}
|
||||
f, err := manager.DestroyScene(scene, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
postCommitFuncs = append(postCommitFuncs, f)
|
||||
}
|
||||
err = manager.DestroyScene(sceneID, tx)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
fileNamingAlgo := config.GetVideoFileNamingAlgorithm()
|
||||
for _, f := range postCommitFuncs {
|
||||
f()
|
||||
}
|
||||
|
||||
fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm()
|
||||
for _, scene := range scenes {
|
||||
// if delete generated is true, then delete the generated files
|
||||
// for the scene
|
||||
@@ -488,8 +445,16 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.SceneMarkerCreateInput) (*models.SceneMarker, error) {
|
||||
primaryTagID, _ := strconv.Atoi(input.PrimaryTagID)
|
||||
sceneID, _ := strconv.Atoi(input.SceneID)
|
||||
primaryTagID, err := strconv.Atoi(input.PrimaryTagID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sceneID, err := strconv.Atoi(input.SceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentTime := time.Now()
|
||||
newSceneMarker := models.SceneMarker{
|
||||
Title: input.Title,
|
||||
@@ -500,14 +465,31 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.S
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
}
|
||||
|
||||
return changeMarker(ctx, create, newSceneMarker, input.TagIds)
|
||||
tagIDs, err := utils.StringSliceToIntSlice(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.changeMarker(ctx, create, newSceneMarker, tagIDs)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input models.SceneMarkerUpdateInput) (*models.SceneMarker, error) {
|
||||
// Populate scene marker from the input
|
||||
sceneMarkerID, _ := strconv.Atoi(input.ID)
|
||||
sceneID, _ := strconv.Atoi(input.SceneID)
|
||||
primaryTagID, _ := strconv.Atoi(input.PrimaryTagID)
|
||||
sceneMarkerID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
primaryTagID, err := strconv.Atoi(input.PrimaryTagID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sceneID, err := strconv.Atoi(input.SceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedSceneMarker := models.SceneMarker{
|
||||
ID: sceneMarkerID,
|
||||
Title: input.Title,
|
||||
@@ -517,168 +499,151 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input models.S
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: time.Now()},
|
||||
}
|
||||
|
||||
return changeMarker(ctx, update, updatedSceneMarker, input.TagIds)
|
||||
tagIDs, err := utils.StringSliceToIntSlice(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.changeMarker(ctx, update, updatedSceneMarker, tagIDs)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
markerID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := qb.Destroy(id, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
var postCommitFunc func()
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.SceneMarker()
|
||||
sqb := repo.Scene()
|
||||
|
||||
marker, err := qb.Find(markerID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if marker == nil {
|
||||
return fmt.Errorf("scene marker with id %d not found", markerID)
|
||||
}
|
||||
|
||||
scene, err := sqb.Find(int(marker.SceneID.Int64))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
postCommitFunc, err = manager.DestroySceneMarker(scene, marker, qb)
|
||||
return err
|
||||
}); 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, config.GetVideoFileNamingAlgorithm())
|
||||
}
|
||||
postCommitFunc()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func changeMarker(ctx context.Context, changeType int, changedMarker models.SceneMarker, tagIds []string) (*models.SceneMarker, error) {
|
||||
// Start the transaction and save the scene marker
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
|
||||
func (r *mutationResolver) changeMarker(ctx context.Context, changeType int, changedMarker models.SceneMarker, tagIDs []int) (*models.SceneMarker, error) {
|
||||
var existingMarker *models.SceneMarker
|
||||
var sceneMarker *models.SceneMarker
|
||||
var err error
|
||||
switch changeType {
|
||||
case create:
|
||||
sceneMarker, err = qb.Create(changedMarker, tx)
|
||||
case update:
|
||||
// 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()
|
||||
return nil, err
|
||||
}
|
||||
var scene *models.Scene
|
||||
|
||||
// Save the marker tags
|
||||
var markerTagJoins []models.SceneMarkersTags
|
||||
for _, tid := range tagIds {
|
||||
tagID, _ := strconv.Atoi(tid)
|
||||
if tagID == changedMarker.PrimaryTagID {
|
||||
continue // If this tag is the primary tag, then let's not add it.
|
||||
}
|
||||
markerTag := models.SceneMarkersTags{
|
||||
SceneMarkerID: sceneMarker.ID,
|
||||
TagID: tagID,
|
||||
}
|
||||
markerTagJoins = append(markerTagJoins, markerTag)
|
||||
}
|
||||
switch changeType {
|
||||
case create:
|
||||
if err := jqb.CreateSceneMarkersTags(markerTagJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
case update:
|
||||
if err := jqb.UpdateSceneMarkersTags(changedMarker.ID, markerTagJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Start the transaction and save the scene marker
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.SceneMarker()
|
||||
sqb := repo.Scene()
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
var err error
|
||||
switch changeType {
|
||||
case create:
|
||||
sceneMarker, err = qb.Create(changedMarker)
|
||||
case update:
|
||||
// check to see if timestamp was changed
|
||||
existingMarker, err = qb.Find(changedMarker.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sceneMarker, err = qb.Update(changedMarker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scene, err = sqb.Find(int(existingMarker.SceneID.Int64))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the marker tags
|
||||
// If this tag is the primary tag, then let's not add it.
|
||||
tagIDs = utils.IntExclude(tagIDs, []int{changedMarker.PrimaryTagID})
|
||||
return qb.UpdateTags(sceneMarker.ID, tagIDs)
|
||||
}); err != nil {
|
||||
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, config.GetVideoFileNamingAlgorithm())
|
||||
}
|
||||
if scene != nil && existingMarker != nil && existingMarker.Seconds != changedMarker.Seconds {
|
||||
seconds := int(existingMarker.Seconds)
|
||||
manager.DeleteSceneMarkerFiles(scene, seconds, config.GetInstance().GetVideoFileNamingAlgorithm())
|
||||
}
|
||||
|
||||
return sceneMarker, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (int, error) {
|
||||
sceneID, _ := strconv.Atoi(id)
|
||||
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
|
||||
newVal, err := qb.IncrementOCounter(sceneID, tx)
|
||||
func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
|
||||
ret, err = qb.IncrementOCounter(sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newVal, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (int, error) {
|
||||
sceneID, _ := strconv.Atoi(id)
|
||||
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
|
||||
newVal, err := qb.DecrementOCounter(sceneID, tx)
|
||||
func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
|
||||
ret, err = qb.DecrementOCounter(sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newVal, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (int, error) {
|
||||
sceneID, _ := strconv.Atoi(id)
|
||||
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
|
||||
newVal, err := qb.ResetOCounter(sceneID, tx)
|
||||
func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int, err error) {
|
||||
sceneID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Scene()
|
||||
|
||||
ret, err = qb.ResetOCounter(sceneID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newVal, nil
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneGenerateScreenshot(ctx context.Context, id string, at *float64) (string, error) {
|
||||
|
||||
@@ -4,19 +4,25 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input models.StashBoxFingerprintSubmissionInput) (bool, error) {
|
||||
boxes := config.GetStashBoxes()
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
||||
return false, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex])
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
|
||||
|
||||
return client.SubmitStashBoxFingerprints(input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input models.StashBoxBatchPerformerTagInput) (string, error) {
|
||||
manager.GetInstance().StashBoxBatchPerformerTag(input)
|
||||
return "todo", nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -21,7 +20,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
if input.Image != nil {
|
||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -43,42 +42,43 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
newStudio.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
newStudio.Rating = sql.NullInt64{Valid: false}
|
||||
}
|
||||
if input.Details != nil {
|
||||
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
|
||||
// Start the transaction and save the studio
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
var studio *models.Studio
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Studio()
|
||||
|
||||
studio, err := qb.Create(newStudio, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateStudioImage(studio.ID, imageData, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
var err error
|
||||
studio, err = qb.Create(newStudio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
var stashIDJoins []models.StashID
|
||||
for _, stashID := range input.StashIds {
|
||||
newJoin := models.StashID{
|
||||
StashID: stashID.StashID,
|
||||
Endpoint: stashID.Endpoint,
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(studio.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
stashIDJoins = append(stashIDJoins, newJoin)
|
||||
}
|
||||
if err := jqb.UpdateStudioStashIDs(studio.ID, stashIDJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(studio.ID, stashIDJoins); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -87,7 +87,14 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
|
||||
func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.StudioUpdateInput) (*models.Studio, error) {
|
||||
// Populate studio from the input
|
||||
studioID, _ := strconv.Atoi(input.ID)
|
||||
studioID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
updatedStudio := models.StudioPartial{
|
||||
ID: studioID,
|
||||
@@ -95,10 +102,10 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
}
|
||||
|
||||
var imageData []byte
|
||||
imageIncluded := wasFieldIncluded(ctx, "image")
|
||||
imageIncluded := translator.hasField("image")
|
||||
if input.Image != nil {
|
||||
var err error
|
||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -109,65 +116,49 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
updatedStudio.Name = &sql.NullString{String: *input.Name, Valid: true}
|
||||
updatedStudio.Checksum = &checksum
|
||||
}
|
||||
if input.URL != nil {
|
||||
updatedStudio.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
|
||||
if input.ParentID != nil {
|
||||
parentID, _ := strconv.ParseInt(*input.ParentID, 10, 64)
|
||||
updatedStudio.ParentID = &sql.NullInt64{Int64: parentID, Valid: true}
|
||||
} else {
|
||||
// parent studio must be nullable
|
||||
updatedStudio.ParentID = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
updatedStudio.URL = translator.nullString(input.URL, "url")
|
||||
updatedStudio.Details = translator.nullString(input.Details, "details")
|
||||
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
|
||||
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
|
||||
|
||||
// Start the transaction and save the studio
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
var studio *models.Studio
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Studio()
|
||||
|
||||
if err := manager.ValidateModifyStudio(updatedStudio, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
studio, err := qb.Update(updatedStudio, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateStudioImage(studio.ID, imageData, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
if err := manager.ValidateModifyStudio(updatedStudio, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if imageIncluded {
|
||||
// must be unsetting
|
||||
if err := qb.DestroyStudioImage(studio.ID, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the stash_ids
|
||||
if input.StashIds != nil {
|
||||
var stashIDJoins []models.StashID
|
||||
for _, stashID := range input.StashIds {
|
||||
newJoin := models.StashID{
|
||||
StashID: stashID.StashID,
|
||||
Endpoint: stashID.Endpoint,
|
||||
var err error
|
||||
studio, err = qb.Update(updatedStudio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(studio.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if imageIncluded {
|
||||
// must be unsetting
|
||||
if err := qb.DestroyImage(studio.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
stashIDJoins = append(stashIDJoins, newJoin)
|
||||
}
|
||||
if err := jqb.UpdateStudioStashIDs(studioID, stashIDJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
// Save the stash_ids
|
||||
if translator.hasField("stash_ids") {
|
||||
stashIDJoins := models.StashIDsFromInput(input.StashIds)
|
||||
if err := qb.UpdateStashIDs(studioID, stashIDJoins); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -175,13 +166,35 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StudioDestroy(ctx context.Context, input models.StudioDestroyInput) (bool, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
if err := qb.Destroy(input.ID, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
return repo.Studio().Destroy(id)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StudiosDestroy(ctx context.Context, studioIDs []string) (bool, error) {
|
||||
ids, err := utils.StringSliceToIntSlice(studioIDs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Studio()
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -25,7 +24,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
|
||||
var err error
|
||||
|
||||
if input.Image != nil {
|
||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -33,31 +32,29 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
|
||||
}
|
||||
|
||||
// Start the transaction and save the tag
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewTagQueryBuilder()
|
||||
var tag *models.Tag
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Tag()
|
||||
|
||||
// ensure name is unique
|
||||
if err := manager.EnsureTagNameUnique(newTag, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tag, err := qb.Create(newTag, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateTagImage(tag.ID, imageData, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
// ensure name is unique
|
||||
if err := manager.EnsureTagNameUnique(newTag, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
tag, err = qb.Create(newTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(tag.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -66,7 +63,11 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
|
||||
|
||||
func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdateInput) (*models.Tag, error) {
|
||||
// Populate tag from the input
|
||||
tagID, _ := strconv.Atoi(input.ID)
|
||||
tagID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedTag := models.Tag{
|
||||
ID: tagID,
|
||||
Name: input.Name,
|
||||
@@ -74,11 +75,14 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
||||
}
|
||||
|
||||
var imageData []byte
|
||||
var err error
|
||||
|
||||
imageIncluded := wasFieldIncluded(ctx, "image")
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
imageIncluded := translator.hasField("image")
|
||||
if input.Image != nil {
|
||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||
imageData, err = utils.ProcessImageInput(*input.Image)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -86,50 +90,45 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
||||
}
|
||||
|
||||
// Start the transaction and save the tag
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewTagQueryBuilder()
|
||||
var tag *models.Tag
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Tag()
|
||||
|
||||
// ensure name is unique
|
||||
existing, err := qb.Find(tagID, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("Tag with ID %d not found", tagID)
|
||||
}
|
||||
|
||||
if existing.Name != updatedTag.Name {
|
||||
if err := manager.EnsureTagNameUnique(updatedTag, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
// ensure name is unique
|
||||
existing, err := qb.Find(tagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tag, err := qb.Update(updatedTag, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateTagImage(tag.ID, imageData, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
if existing == nil {
|
||||
return fmt.Errorf("Tag with ID %d not found", tagID)
|
||||
}
|
||||
} else if imageIncluded {
|
||||
// must be unsetting
|
||||
if err := qb.DestroyTagImage(tag.ID, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
if existing.Name != updatedTag.Name {
|
||||
if err := manager.EnsureTagNameUnique(updatedTag, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tag, err = qb.Update(updatedTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(tag.ID, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if imageIncluded {
|
||||
// must be unsetting
|
||||
if err := qb.DestroyImage(tag.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -137,13 +136,35 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
||||
}
|
||||
|
||||
func (r *mutationResolver) TagDestroy(ctx context.Context, input models.TagDestroyInput) (bool, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
if err := qb.Destroy(input.ID, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
tagID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
return repo.Tag().Destroy(tagID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) TagsDestroy(ctx context.Context, tagIDs []string) (bool, error) {
|
||||
ids, err := utils.StringSliceToIntSlice(tagIDs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||
qb := repo.Tag()
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
|
||||
70
pkg/api/resolver_mutation_tag_test.go
Normal file
70
pkg/api/resolver_mutation_tag_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// TODO - move this into a common area
|
||||
func newResolver() *Resolver {
|
||||
return &Resolver{
|
||||
txnManager: mocks.NewTransactionManager(),
|
||||
}
|
||||
}
|
||||
|
||||
const tagName = "tagName"
|
||||
const errTagName = "errTagName"
|
||||
|
||||
const existingTagID = 1
|
||||
const existingTagName = "existingTagName"
|
||||
const newTagID = 2
|
||||
|
||||
func TestTagCreate(t *testing.T) {
|
||||
r := newResolver()
|
||||
|
||||
tagRW := r.txnManager.(*mocks.TransactionManager).Tag().(*mocks.TagReaderWriter)
|
||||
tagRW.On("FindByName", existingTagName, true).Return(&models.Tag{
|
||||
ID: existingTagID,
|
||||
Name: existingTagName,
|
||||
}, nil).Once()
|
||||
tagRW.On("FindByName", errTagName, true).Return(nil, nil).Once()
|
||||
|
||||
expectedErr := errors.New("TagCreate error")
|
||||
tagRW.On("Create", mock.AnythingOfType("models.Tag")).Return(nil, expectedErr)
|
||||
|
||||
_, err := r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
|
||||
Name: existingTagName,
|
||||
})
|
||||
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, err = r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
|
||||
Name: errTagName,
|
||||
})
|
||||
|
||||
assert.Equal(t, expectedErr, err)
|
||||
tagRW.AssertExpectations(t)
|
||||
|
||||
r = newResolver()
|
||||
tagRW = r.txnManager.(*mocks.TransactionManager).Tag().(*mocks.TagReaderWriter)
|
||||
|
||||
tagRW.On("FindByName", tagName, true).Return(nil, nil).Once()
|
||||
tagRW.On("Create", mock.AnythingOfType("models.Tag")).Return(&models.Tag{
|
||||
ID: newTagID,
|
||||
Name: tagName,
|
||||
}, nil)
|
||||
|
||||
tag, err := r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
|
||||
Name: tagName,
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, tag)
|
||||
}
|
||||
@@ -34,6 +34,7 @@ func makeConfigResult() *models.ConfigResult {
|
||||
}
|
||||
|
||||
func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
config := config.GetInstance()
|
||||
logFile := config.GetLogFile()
|
||||
|
||||
maxTranscodeSize := config.GetMaxTranscodeSize()
|
||||
@@ -46,9 +47,12 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
ConfigFilePath: config.GetConfigFilePath(),
|
||||
ScrapersPath: config.GetScrapersPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
CalculateMd5: config.IsCalculateMD5(),
|
||||
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
ParallelTasks: config.GetParallelTasks(),
|
||||
PreviewSegments: config.GetPreviewSegments(),
|
||||
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
||||
PreviewExcludeStart: config.GetPreviewExcludeStart(),
|
||||
@@ -56,6 +60,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
PreviewPreset: config.GetPreviewPreset(),
|
||||
MaxTranscodeSize: &maxTranscodeSize,
|
||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||
APIKey: config.GetAPIKey(),
|
||||
Username: config.GetUsername(),
|
||||
Password: config.GetPasswordHash(),
|
||||
MaxSessionAge: config.GetMaxSessionAge(),
|
||||
@@ -70,12 +75,15 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
Excludes: config.GetExcludes(),
|
||||
ImageExcludes: config.GetImageExcludes(),
|
||||
ScraperUserAgent: &scraperUserAgent,
|
||||
ScraperCertCheck: config.GetScraperCertCheck(),
|
||||
ScraperCDPPath: &scraperCDPPath,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
config := config.GetInstance()
|
||||
menuItems := config.GetMenuItems()
|
||||
soundOnPreview := config.GetSoundOnPreview()
|
||||
wallShowTitle := config.GetWallShowTitle()
|
||||
wallPlayback := config.GetWallPlayback()
|
||||
@@ -85,8 +93,10 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
css := config.GetCSS()
|
||||
cssEnabled := config.GetCSSEnabled()
|
||||
language := config.GetLanguage()
|
||||
slideshowDelay := config.GetSlideshowDelay()
|
||||
|
||||
return &models.ConfigInterfaceResult{
|
||||
MenuItems: menuItems,
|
||||
SoundOnPreview: &soundOnPreview,
|
||||
WallShowTitle: &wallShowTitle,
|
||||
WallPlayback: &wallPlayback,
|
||||
@@ -96,5 +106,6 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
Language: &language,
|
||||
SlideshowDelay: &slideshowDelay,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,37 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindGallery(ctx context.Context, id string) (*models.Gallery, error) {
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
idInt, _ := strconv.Atoi(id)
|
||||
return qb.Find(idInt, nil)
|
||||
func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models.Gallery, err error) {
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Gallery().Find(idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (*models.FindGalleriesResultType, error) {
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
galleries, total := qb.Query(galleryFilter, filter)
|
||||
return &models.FindGalleriesResultType{
|
||||
Count: total,
|
||||
Galleries: galleries,
|
||||
}, nil
|
||||
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *models.FindGalleriesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
galleries, total, err := repo.Gallery().Query(galleryFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindGalleriesResultType{
|
||||
Count: total,
|
||||
Galleries: galleries,
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -8,23 +8,48 @@ import (
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
var image *models.Image
|
||||
var err error
|
||||
if id != nil {
|
||||
idInt, _ := strconv.Atoi(*id)
|
||||
image, err = qb.Find(idInt)
|
||||
} else if checksum != nil {
|
||||
image, err = qb.FindByChecksum(*checksum)
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Image()
|
||||
var err error
|
||||
|
||||
if id != nil {
|
||||
idInt, err := strconv.Atoi(*id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
image, err = qb.Find(idInt)
|
||||
} else if checksum != nil {
|
||||
image, err = qb.FindByChecksum(*checksum)
|
||||
}
|
||||
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return image, err
|
||||
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (*models.FindImagesResultType, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
images, total := qb.Query(imageFilter, filter)
|
||||
return &models.FindImagesResultType{
|
||||
Count: total,
|
||||
Images: images,
|
||||
}, nil
|
||||
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *models.FindImagesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Image()
|
||||
images, total, err := qb.Query(imageFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindImagesResultType{
|
||||
Count: total,
|
||||
Images: images,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -7,27 +7,49 @@ import (
|
||||
"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) FindMovie(ctx context.Context, id string) (ret *models.Movie, err error) {
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().Find(idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, 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) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *models.FindMoviesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
movies, total, err := repo.Movie().Query(movieFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindMoviesResultType{
|
||||
Count: total,
|
||||
Movies: movies,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllMovies(ctx context.Context) ([]*models.Movie, error) {
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
return qb.All()
|
||||
}
|
||||
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Movie, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Movie().All()
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllMoviesSlim(ctx context.Context) ([]*models.Movie, error) {
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
return qb.AllSlim()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -2,31 +2,53 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindPerformer(ctx context.Context, id string) (*models.Performer, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
idInt, _ := strconv.Atoi(id)
|
||||
return qb.Find(idInt)
|
||||
func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *models.Performer, err error) {
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().Find(idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (*models.FindPerformersResultType, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
performers, total := qb.Query(performerFilter, filter)
|
||||
return &models.FindPerformersResultType{
|
||||
Count: total,
|
||||
Performers: performers,
|
||||
}, nil
|
||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *models.FindPerformersResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
performers, total, err := repo.Performer().Query(performerFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindPerformersResultType{
|
||||
Count: total,
|
||||
Performers: performers,
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllPerformers(ctx context.Context) ([]*models.Performer, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
return qb.All()
|
||||
}
|
||||
func (r *queryResolver) AllPerformers(ctx context.Context) (ret []*models.Performer, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Performer().All()
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllPerformersSlim(ctx context.Context) ([]*models.Performer, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
return qb.AllSlim()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -9,70 +9,160 @@ import (
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
var scene *models.Scene
|
||||
var err error
|
||||
if id != nil {
|
||||
idInt, _ := strconv.Atoi(*id)
|
||||
scene, err = qb.Find(idInt)
|
||||
} else if checksum != nil {
|
||||
scene, err = qb.FindByChecksum(*checksum)
|
||||
}
|
||||
return scene, err
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindSceneByHash(ctx context.Context, input models.SceneHashInput) (*models.Scene, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
var scene *models.Scene
|
||||
var err error
|
||||
|
||||
if input.Checksum != nil {
|
||||
scene, err = qb.FindByChecksum(*input.Checksum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Scene()
|
||||
var err error
|
||||
if id != nil {
|
||||
idInt, err := strconv.Atoi(*id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scene, err = qb.Find(idInt)
|
||||
} else if checksum != nil {
|
||||
scene, err = qb.FindByChecksum(*checksum)
|
||||
}
|
||||
}
|
||||
|
||||
if scene == nil && input.Oshash != nil {
|
||||
scene, err = qb.FindByOSHash(*input.Oshash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return scene, err
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIds []int, filter *models.FindFilterType) (*models.FindScenesResultType, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
scenes, total := qb.Query(sceneFilter, filter)
|
||||
return &models.FindScenesResultType{
|
||||
Count: total,
|
||||
Scenes: scenes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (*models.FindScenesResultType, error) {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
|
||||
scenes, total := qb.QueryByPathRegex(filter)
|
||||
return &models.FindScenesResultType{
|
||||
Count: total,
|
||||
Scenes: scenes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config models.SceneParserInput) (*models.SceneParserResultType, error) {
|
||||
parser := manager.NewSceneFilenameParser(filter, config)
|
||||
|
||||
result, count, err := parser.Parse()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.SceneParserResultType{
|
||||
Count: count,
|
||||
Results: result,
|
||||
}, nil
|
||||
return scene, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindSceneByHash(ctx context.Context, input models.SceneHashInput) (*models.Scene, error) {
|
||||
var scene *models.Scene
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Scene()
|
||||
var err error
|
||||
if input.Checksum != nil {
|
||||
scene, err = qb.FindByChecksum(*input.Checksum)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if scene == nil && input.Oshash != nil {
|
||||
scene, err = qb.FindByOSHash(*input.Oshash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return scene, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var scenes []*models.Scene
|
||||
var total int
|
||||
var err error
|
||||
|
||||
if len(sceneIDs) > 0 {
|
||||
scenes, err = repo.Scene().FindMany(sceneIDs)
|
||||
if err == nil {
|
||||
total = len(scenes)
|
||||
}
|
||||
} else {
|
||||
scenes, total, err = repo.Scene().Query(sceneFilter, filter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindScenesResultType{
|
||||
Count: total,
|
||||
Scenes: scenes,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
|
||||
sceneFilter := &models.SceneFilterType{}
|
||||
|
||||
if filter != nil && filter.Q != nil {
|
||||
sceneFilter.Path = &models.StringCriterionInput{
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
Value: "(?i)" + *filter.Q,
|
||||
}
|
||||
}
|
||||
|
||||
// make a copy of the filter if provided, nilling out Q
|
||||
var queryFilter *models.FindFilterType
|
||||
if filter != nil {
|
||||
f := *filter
|
||||
queryFilter = &f
|
||||
queryFilter.Q = nil
|
||||
}
|
||||
|
||||
scenes, total, err := repo.Scene().Query(sceneFilter, queryFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindScenesResultType{
|
||||
Count: total,
|
||||
Scenes: scenes,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config models.SceneParserInput) (ret *models.SceneParserResultType, err error) {
|
||||
parser := manager.NewSceneFilenameParser(filter, config)
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
result, count, err := parser.Parse(repo)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.SceneParserResultType{
|
||||
Count: count,
|
||||
Results: result,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int) (ret [][]*models.Scene, err error) {
|
||||
dist := 0
|
||||
if distance != nil {
|
||||
dist = *distance
|
||||
}
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Scene().FindDuplicates(dist)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -2,14 +2,25 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (*models.FindSceneMarkersResultType, error) {
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
sceneMarkers, total := qb.Query(sceneMarkerFilter, filter)
|
||||
return &models.FindSceneMarkersResultType{
|
||||
Count: total,
|
||||
SceneMarkers: sceneMarkers,
|
||||
}, nil
|
||||
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (ret *models.FindSceneMarkersResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
sceneMarkers, total, err := repo.SceneMarker().Query(sceneMarkerFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ret = &models.FindSceneMarkersResultType{
|
||||
Count: total,
|
||||
SceneMarkers: sceneMarkers,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -2,31 +2,55 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindStudio(ctx context.Context, id string) (*models.Studio, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
idInt, _ := strconv.Atoi(id)
|
||||
return qb.Find(idInt, nil)
|
||||
func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.Studio, err error) {
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
ret, err = repo.Studio().Find(idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (*models.FindStudiosResultType, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
studios, total := qb.Query(studioFilter, filter)
|
||||
return &models.FindStudiosResultType{
|
||||
Count: total,
|
||||
Studios: studios,
|
||||
}, nil
|
||||
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (ret *models.FindStudiosResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
studios, total, err := repo.Studio().Query(studioFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindStudiosResultType{
|
||||
Count: total,
|
||||
Studios: studios,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllStudios(ctx context.Context) ([]*models.Studio, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.All()
|
||||
}
|
||||
func (r *queryResolver) AllStudios(ctx context.Context) (ret []*models.Studio, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Studio().All()
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllStudiosSlim(ctx context.Context) ([]*models.Studio, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.AllSlim()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -7,27 +7,49 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindTag(ctx context.Context, id string) (*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
idInt, _ := strconv.Atoi(id)
|
||||
return qb.Find(idInt, nil)
|
||||
func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag, err error) {
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().Find(idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType) (*models.FindTagsResultType, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
tags, total := qb.Query(tagFilter, filter)
|
||||
return &models.FindTagsResultType{
|
||||
Count: total,
|
||||
Tags: tags,
|
||||
}, nil
|
||||
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType) (ret *models.FindTagsResultType, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
tags, total, err := repo.Tag().Query(tagFilter, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = &models.FindTagsResultType{
|
||||
Count: total,
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllTags(ctx context.Context) ([]*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
return qb.All()
|
||||
}
|
||||
func (r *queryResolver) AllTags(ctx context.Context) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
ret, err = repo.Tag().All()
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (r *queryResolver) AllTagsSlim(ctx context.Context) ([]*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
return qb.AllSlim()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -17,3 +17,7 @@ func (r *queryResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateSt
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) SystemStatus(ctx context.Context) (*models.SystemStatus, error) {
|
||||
return manager.GetInstance().GetSystemStatus(), nil
|
||||
}
|
||||
|
||||
@@ -7,16 +7,19 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*models.SceneStreamEndpoint, error) {
|
||||
// find the scene
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
idInt, _ := strconv.Atoi(*id)
|
||||
scene, err := qb.Find(idInt)
|
||||
|
||||
if err != nil {
|
||||
var scene *models.Scene
|
||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
idInt, _ := strconv.Atoi(*id)
|
||||
var err error
|
||||
scene, err = repo.Scene().Find(idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -27,5 +30,5 @@ func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*models
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
builder := urlbuilders.NewSceneURLBuilder(baseURL, scene.ID)
|
||||
|
||||
return manager.GetSceneStreamPaths(scene, builder.GetStreamURL())
|
||||
return manager.GetSceneStreamPaths(scene, builder.GetStreamURL(), config.GetInstance().GetMaxStreamingTranscodeSize())
|
||||
}
|
||||
|
||||
@@ -88,14 +88,14 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models
|
||||
return manager.GetInstance().ScraperCache.ScrapeMovieURL(url)
|
||||
}
|
||||
|
||||
func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.StashBoxQueryInput) ([]*models.ScrapedScene, error) {
|
||||
boxes := config.GetStashBoxes()
|
||||
func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.StashBoxSceneQueryInput) ([]*models.ScrapedScene, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
||||
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex])
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
|
||||
|
||||
if len(input.SceneIds) > 0 {
|
||||
return client.FindStashBoxScenesByFingerprints(input.SceneIds)
|
||||
@@ -107,3 +107,23 @@ func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.Sta
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) QueryStashBoxPerformer(ctx context.Context, input models.StashBoxPerformerQueryInput) ([]*models.StashBoxPerformerQueryResult, error) {
|
||||
boxes := config.GetInstance().GetStashBoxes()
|
||||
|
||||
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
||||
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
|
||||
}
|
||||
|
||||
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
|
||||
|
||||
if len(input.PerformerIds) > 0 {
|
||||
return client.FindStashBoxPerformersByNames(input.PerformerIds)
|
||||
}
|
||||
|
||||
if input.Q != nil {
|
||||
return client.QueryStashBoxPerformer(*input.Q)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import (
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type imageRoutes struct{}
|
||||
type imageRoutes struct {
|
||||
txnManager models.TransactionManager
|
||||
}
|
||||
|
||||
func (rs imageRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
@@ -57,12 +59,16 @@ func ImageCtx(next http.Handler) http.Handler {
|
||||
imageID, _ := strconv.Atoi(imageIdentifierQueryParam)
|
||||
|
||||
var image *models.Image
|
||||
qb := models.NewImageQueryBuilder()
|
||||
if imageID == 0 {
|
||||
image, _ = qb.FindByChecksum(imageIdentifierQueryParam)
|
||||
} else {
|
||||
image, _ = qb.Find(imageID)
|
||||
}
|
||||
manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
qb := repo.Image()
|
||||
if imageID == 0 {
|
||||
image, _ = qb.FindByChecksum(imageIdentifierQueryParam)
|
||||
} else {
|
||||
image, _ = qb.Find(imageID)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if image == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
|
||||
@@ -6,11 +6,14 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type movieRoutes struct{}
|
||||
type movieRoutes struct {
|
||||
txnManager models.TransactionManager
|
||||
}
|
||||
|
||||
func (rs movieRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
@@ -26,11 +29,16 @@ func (rs movieRoutes) Routes() chi.Router {
|
||||
|
||||
func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
|
||||
movie := r.Context().Value(movieKey).(*models.Movie)
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
image, _ := qb.GetFrontImage(movie.ID, nil)
|
||||
|
||||
defaultParam := r.URL.Query().Get("default")
|
||||
if len(image) == 0 || defaultParam == "true" {
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
image, _ = repo.Movie().GetFrontImage(movie.ID)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if len(image) == 0 {
|
||||
_, image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
|
||||
}
|
||||
|
||||
@@ -39,11 +47,16 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
|
||||
movie := r.Context().Value(movieKey).(*models.Movie)
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
image, _ := qb.GetBackImage(movie.ID, nil)
|
||||
|
||||
defaultParam := r.URL.Query().Get("default")
|
||||
if len(image) == 0 || defaultParam == "true" {
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
image, _ = repo.Movie().GetBackImage(movie.ID)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if len(image) == 0 {
|
||||
_, image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
|
||||
}
|
||||
|
||||
@@ -58,9 +71,12 @@ func MovieCtx(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
movie, err := qb.Find(movieID, nil)
|
||||
if err != nil {
|
||||
var movie *models.Movie
|
||||
if err := manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
movie, err = repo.Movie().Find(movieID)
|
||||
return err
|
||||
}); err != nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type performerRoutes struct{}
|
||||
type performerRoutes struct {
|
||||
txnManager models.TransactionManager
|
||||
}
|
||||
|
||||
func (rs performerRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
@@ -25,10 +28,16 @@ func (rs performerRoutes) Routes() chi.Router {
|
||||
|
||||
func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
performer := r.Context().Value(performerKey).(*models.Performer)
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
image, _ := qb.GetPerformerImage(performer.ID, nil)
|
||||
|
||||
defaultParam := r.URL.Query().Get("default")
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
image, _ = repo.Performer().GetImage(performer.ID)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if len(image) == 0 || defaultParam == "true" {
|
||||
image, _ = getRandomPerformerImageUsingName(performer.Name.String, performer.Gender.String)
|
||||
}
|
||||
@@ -44,9 +53,12 @@ func PerformerCtx(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
performer, err := qb.Find(performerID)
|
||||
if err != nil {
|
||||
var performer *models.Performer
|
||||
if err := manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
performer, err = repo.Performer().Find(performerID)
|
||||
return err
|
||||
}); err != nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ import (
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type sceneRoutes struct{}
|
||||
type sceneRoutes struct {
|
||||
txnManager models.TransactionManager
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
@@ -53,7 +55,7 @@ func getSceneFileContainer(scene *models.Scene) ffmpeg.Container {
|
||||
container = ffmpeg.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)
|
||||
tmpVideoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
|
||||
if err != nil {
|
||||
logger.Errorf("[transcode] error reading video file: %s", err.Error())
|
||||
return ffmpeg.Container("")
|
||||
@@ -67,7 +69,7 @@ func getSceneFileContainer(scene *models.Scene) ffmpeg.Container {
|
||||
|
||||
func (rs sceneRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
fileNamingAlgo := config.GetVideoFileNamingAlgorithm()
|
||||
fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm()
|
||||
|
||||
filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.GetHash(fileNamingAlgo))
|
||||
manager.RegisterStream(filepath, &w)
|
||||
@@ -100,7 +102,7 @@ func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) {
|
||||
func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
|
||||
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path)
|
||||
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
|
||||
if err != nil {
|
||||
logger.Errorf("[stream] error reading video file: %s", err.Error())
|
||||
return
|
||||
@@ -136,7 +138,7 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
|
||||
|
||||
// needs to be transcoded
|
||||
|
||||
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path)
|
||||
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
|
||||
if err != nil {
|
||||
logger.Errorf("[stream] error reading video file: %s", err.Error())
|
||||
return
|
||||
@@ -156,7 +158,7 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
|
||||
|
||||
options := ffmpeg.GetTranscodeStreamOptions(*videoFile, videoCodec, audioCodec)
|
||||
options.StartTime = startTime
|
||||
options.MaxTranscodeSize = config.GetMaxStreamingTranscodeSize()
|
||||
options.MaxTranscodeSize = config.GetInstance().GetMaxStreamingTranscodeSize()
|
||||
if requestedSize != "" {
|
||||
options.MaxTranscodeSize = models.StreamingResolutionEnum(requestedSize)
|
||||
}
|
||||
@@ -176,64 +178,76 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
|
||||
|
||||
func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
filepath := manager.GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetVideoFileNamingAlgorithm()))
|
||||
filepath := manager.GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
|
||||
|
||||
// fall back to the scene image blob if the file isn't present
|
||||
screenshotExists, _ := utils.FileExists(filepath)
|
||||
if screenshotExists {
|
||||
http.ServeFile(w, r, filepath)
|
||||
} else {
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
cover, _ := qb.GetSceneCover(scene.ID, nil)
|
||||
var cover []byte
|
||||
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
cover, _ = repo.Scene().GetCover(scene.ID)
|
||||
return nil
|
||||
})
|
||||
utils.ServeImage(cover, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) Preview(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewPath(scene.GetHash(config.GetVideoFileNamingAlgorithm()))
|
||||
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
|
||||
utils.ServeFileNoCache(w, r, filepath)
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewImagePath(scene.GetHash(config.GetVideoFileNamingAlgorithm()))
|
||||
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewImagePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
|
||||
http.ServeFile(w, r, filepath)
|
||||
}
|
||||
|
||||
func getChapterVttTitle(marker *models.SceneMarker) string {
|
||||
func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.SceneMarker) string {
|
||||
if marker.Title != "" {
|
||||
return marker.Title
|
||||
}
|
||||
|
||||
qb := models.NewTagQueryBuilder()
|
||||
primaryTag, err := qb.Find(marker.PrimaryTagID, nil)
|
||||
if err != nil {
|
||||
// should not happen
|
||||
var ret string
|
||||
if err := rs.txnManager.WithReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||
qb := repo.Tag()
|
||||
primaryTag, err := qb.Find(marker.PrimaryTagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = primaryTag.Name
|
||||
|
||||
tags, err := qb.FindBySceneMarkerID(marker.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
ret += ", " + t.Name
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ret := primaryTag.Name
|
||||
|
||||
tags, err := qb.FindBySceneMarkerID(marker.ID, nil)
|
||||
if err != nil {
|
||||
// should not happen
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
ret += ", " + t.Name
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
sceneMarkers, err := qb.FindBySceneID(scene.ID, nil)
|
||||
if err != nil {
|
||||
panic("invalid scene markers for chapter vtt")
|
||||
var sceneMarkers []*models.SceneMarker
|
||||
if err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
sceneMarkers, err = repo.SceneMarker().FindBySceneID(scene.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vttLines := []string{"WEBVTT", ""}
|
||||
@@ -241,7 +255,7 @@ func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
|
||||
vttLines = append(vttLines, strconv.Itoa(i+1))
|
||||
time := utils.GetVTTTime(marker.Seconds)
|
||||
vttLines = append(vttLines, time+" --> "+time)
|
||||
vttLines = append(vttLines, getChapterVttTitle(marker))
|
||||
vttLines = append(vttLines, rs.getChapterVttTitle(r.Context(), marker))
|
||||
vttLines = append(vttLines, "")
|
||||
}
|
||||
vtt := strings.Join(vttLines, "\n")
|
||||
@@ -253,42 +267,48 @@ func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
|
||||
func (rs sceneRoutes) VttThumbs(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
w.Header().Set("Content-Type", "text/vtt")
|
||||
filepath := manager.GetInstance().Paths.Scene.GetSpriteVttFilePath(scene.GetHash(config.GetVideoFileNamingAlgorithm()))
|
||||
filepath := manager.GetInstance().Paths.Scene.GetSpriteVttFilePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
|
||||
http.ServeFile(w, r, filepath)
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) VttSprite(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
filepath := manager.GetInstance().Paths.Scene.GetSpriteImageFilePath(scene.GetHash(config.GetVideoFileNamingAlgorithm()))
|
||||
filepath := manager.GetInstance().Paths.Scene.GetSpriteImageFilePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
|
||||
http.ServeFile(w, r, filepath)
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
sceneMarker, err := qb.Find(sceneMarkerID)
|
||||
if err != nil {
|
||||
logger.Warn("Error when getting scene marker for stream")
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
var sceneMarker *models.SceneMarker
|
||||
if err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
sceneMarker, err = repo.SceneMarker().Find(sceneMarkerID)
|
||||
return err
|
||||
}); err != nil {
|
||||
logger.Warnf("Error when getting scene marker for stream: %s", err.Error())
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
return
|
||||
}
|
||||
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPath(scene.GetHash(config.GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
|
||||
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
|
||||
http.ServeFile(w, r, filepath)
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
sceneMarker, err := qb.Find(sceneMarkerID)
|
||||
if err != nil {
|
||||
logger.Warn("Error when getting scene marker for stream")
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
var sceneMarker *models.SceneMarker
|
||||
if err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
sceneMarker, err = repo.SceneMarker().Find(sceneMarkerID)
|
||||
return err
|
||||
}); err != nil {
|
||||
logger.Warnf("Error when getting scene marker for stream: %s", err.Error())
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
return
|
||||
}
|
||||
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPreviewImagePath(scene.GetHash(config.GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
|
||||
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPreviewImagePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
|
||||
|
||||
// If the image doesn't exist, send the placeholder
|
||||
exists, _ := utils.FileExists(filepath)
|
||||
@@ -310,17 +330,21 @@ func SceneCtx(next http.Handler) http.Handler {
|
||||
sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam)
|
||||
|
||||
var scene *models.Scene
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
if sceneID == 0 {
|
||||
// determine checksum/os by the length of the query param
|
||||
if len(sceneIdentifierQueryParam) == 32 {
|
||||
scene, _ = qb.FindByChecksum(sceneIdentifierQueryParam)
|
||||
manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
qb := repo.Scene()
|
||||
if sceneID == 0 {
|
||||
// determine checksum/os by the length of the query param
|
||||
if len(sceneIdentifierQueryParam) == 32 {
|
||||
scene, _ = qb.FindByChecksum(sceneIdentifierQueryParam)
|
||||
} else {
|
||||
scene, _ = qb.FindByOSHash(sceneIdentifierQueryParam)
|
||||
}
|
||||
} else {
|
||||
scene, _ = qb.FindByOSHash(sceneIdentifierQueryParam)
|
||||
scene, _ = qb.Find(sceneID)
|
||||
}
|
||||
} else {
|
||||
scene, _ = qb.Find(sceneID)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if scene == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
|
||||
@@ -6,11 +6,14 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type studioRoutes struct{}
|
||||
type studioRoutes struct {
|
||||
txnManager models.TransactionManager
|
||||
}
|
||||
|
||||
func (rs studioRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
@@ -25,11 +28,17 @@ func (rs studioRoutes) Routes() chi.Router {
|
||||
|
||||
func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
studio := r.Context().Value(studioKey).(*models.Studio)
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
image, _ := qb.GetStudioImage(studio.ID, nil)
|
||||
|
||||
defaultParam := r.URL.Query().Get("default")
|
||||
if len(image) == 0 || defaultParam == "true" {
|
||||
|
||||
var image []byte
|
||||
if defaultParam != "true" {
|
||||
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
image, _ = repo.Studio().GetImage(studio.ID)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if len(image) == 0 {
|
||||
_, image, _ = utils.ProcessBase64Image(models.DefaultStudioImage)
|
||||
}
|
||||
|
||||
@@ -44,9 +53,12 @@ func StudioCtx(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
studio, err := qb.Find(studioID, nil)
|
||||
if err != nil {
|
||||
var studio *models.Studio
|
||||
if err := manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
|
||||
var err error
|
||||
studio, err = repo.Studio().Find(studioID)
|
||||
return err
|
||||
}); err != nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user