Merge pull request #3217 from stashapp/develop

Merge to master for 0.18
This commit is contained in:
WithoutPants 2022-11-30 14:19:41 +11:00 committed by GitHub
commit be8f57d6ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
415 changed files with 18334 additions and 5901 deletions

View file

@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true
env:
COMPILER_IMAGE: stashapp/compiler:6
COMPILER_IMAGE: stashapp/compiler:7
jobs:
build:
@ -27,7 +27,7 @@ jobs:
run: docker pull $COMPILER_IMAGE
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v3
env:
cache-name: cache-node_modules
with:
@ -35,7 +35,7 @@ jobs:
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock') }}
- name: Cache UI build
uses: actions/cache@v2
uses: actions/cache@v3
id: cache-ui
env:
cache-name: cache-ui
@ -44,7 +44,7 @@ jobs:
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock', 'ui/v2.5/public/**', 'ui/v2.5/src/**', 'graphql/**/*.graphql') }}
- name: Cache go build
uses: actions/cache@v2
uses: actions/cache@v3
env:
# increment the number suffix to bump the cache
cache-name: cache-go-cache-1

View file

@ -9,7 +9,7 @@ on:
pull_request:
env:
COMPILER_IMAGE: stashapp/compiler:6
COMPILER_IMAGE: stashapp/compiler:7
jobs:
golangci:

8
.gitignore vendored
View file

@ -23,6 +23,12 @@ ui/v2.5/src/core/generated-*.tsx
# Jetbrains
####
####
# Visual Studio
####
/.vs
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
@ -57,4 +63,4 @@ node_modules
/stash
dist
.DS_Store
.DS_Store

View file

@ -14,11 +14,6 @@ else
SET := export
endif
IS_WIN_OS =
ifeq ($(OS),Windows_NT)
IS_WIN_OS = true
endif
# set LDFLAGS environment variable to any extra ldflags required
# set OUTPUT to generate a specific binary name
@ -29,9 +24,14 @@ endif
export CGO_ENABLED = 1
# including netgo causes name resolution to go through the Go resolver
# and isn't necessary for static builds on Windows
GO_BUILD_TAGS_WINDOWS := sqlite_omit_load_extension sqlite_stat4 osusergo
GO_BUILD_TAGS_DEFAULT = $(GO_BUILD_TAGS_WINDOWS) netgo
.PHONY: release pre-build
release: generate ui build-release
release: pre-ui generate ui build-release
pre-build:
ifndef BUILD_DATE
@ -47,14 +47,21 @@ ifndef STASH_VERSION
endif
ifndef OFFICIAL_BUILD
$(eval OFFICIAL_BUILD := false)
$(eval OFFICIAL_BUILD := false)
endif
ifndef GO_BUILD_TAGS
$(eval GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT))
endif
# NOTE: the build target still includes netgo because we cannot detect
# Windows easily from the Makefile.
build: pre-build
build:
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/internal/api.version=$(STASH_VERSION)' -X 'github.com/stashapp/stash/internal/api.buildstamp=$(BUILD_DATE)' -X 'github.com/stashapp/stash/internal/api.githash=$(GITHASH)')
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/internal/manager/config.officialBuild=$(OFFICIAL_BUILD)')
go build $(OUTPUT) -mod=vendor -v -tags "sqlite_omit_load_extension sqlite_stat4 osusergo netgo" $(GO_BUILD_FLAGS) -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS) $(PLATFORM_SPECIFIC_LDFLAGS)" ./cmd/stash
go build $(OUTPUT) -mod=vendor -v -tags "$(GO_BUILD_TAGS)" $(GO_BUILD_FLAGS) -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS) $(PLATFORM_SPECIFIC_LDFLAGS)" ./cmd/stash
# strips debug symbols from the release build
build-release: EXTRA_LDFLAGS := -s -w
@ -71,6 +78,7 @@ cross-compile-windows: export GOARCH := amd64
cross-compile-windows: export CC := x86_64-w64-mingw32-gcc
cross-compile-windows: export CXX := x86_64-w64-mingw32-g++
cross-compile-windows: OUTPUT := -o dist/stash-win.exe
cross-compile-windows: GO_BUILD_TAGS := $(GO_BUILD_TAGS_WINDOWS)
cross-compile-windows: build-release-static
cross-compile-macos-intel: export GOOS := darwin
@ -78,6 +86,7 @@ cross-compile-macos-intel: export GOARCH := amd64
cross-compile-macos-intel: export CC := o64-clang
cross-compile-macos-intel: export CXX := o64-clang++
cross-compile-macos-intel: OUTPUT := -o dist/stash-macos-intel
cross-compile-macos-intel: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
# can't use static build for OSX
cross-compile-macos-intel: build-release
@ -86,6 +95,7 @@ cross-compile-macos-applesilicon: export GOARCH := arm64
cross-compile-macos-applesilicon: export CC := oa64e-clang
cross-compile-macos-applesilicon: export CXX := oa64e-clang++
cross-compile-macos-applesilicon: OUTPUT := -o dist/stash-macos-applesilicon
cross-compile-macos-applesilicon: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
# can't use static build for OSX
cross-compile-macos-applesilicon: build-release
@ -106,17 +116,20 @@ cross-compile-macos:
cross-compile-freebsd: export GOOS := freebsd
cross-compile-freebsd: export GOARCH := amd64
cross-compile-freebsd: OUTPUT := -o dist/stash-freebsd
cross-compile-freebsd: GO_BUILD_TAGS += netgo
cross-compile-freebsd: build-release-static
cross-compile-linux: export GOOS := linux
cross-compile-linux: export GOARCH := amd64
cross-compile-linux: OUTPUT := -o dist/stash-linux
cross-compile-linux: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
cross-compile-linux: build-release-static
cross-compile-linux-arm64v8: export GOOS := linux
cross-compile-linux-arm64v8: export GOARCH := arm64
cross-compile-linux-arm64v8: export CC := aarch64-linux-gnu-gcc
cross-compile-linux-arm64v8: OUTPUT := -o dist/stash-linux-arm64v8
cross-compile-linux-arm64v8: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
cross-compile-linux-arm64v8: build-release-static
cross-compile-linux-arm32v7: export GOOS := linux
@ -124,6 +137,7 @@ cross-compile-linux-arm32v7: export GOARCH := arm
cross-compile-linux-arm32v7: export GOARM := 7
cross-compile-linux-arm32v7: export CC := arm-linux-gnueabihf-gcc
cross-compile-linux-arm32v7: OUTPUT := -o dist/stash-linux-arm32v7
cross-compile-linux-arm32v7: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
cross-compile-linux-arm32v7: build-release-static
cross-compile-linux-arm32v6: export GOOS := linux
@ -131,11 +145,13 @@ cross-compile-linux-arm32v6: export GOARCH := arm
cross-compile-linux-arm32v6: export GOARM := 6
cross-compile-linux-arm32v6: export CC := arm-linux-gnueabi-gcc
cross-compile-linux-arm32v6: OUTPUT := -o dist/stash-linux-arm32v6
cross-compile-linux-arm32v6: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
cross-compile-linux-arm32v6: build-release-static
cross-compile-all:
make cross-compile-windows
make cross-compile-macos
make cross-compile-macos-intel
make cross-compile-macos-applesilicon
make cross-compile-linux
make cross-compile-linux-arm64v8
make cross-compile-linux-arm32v7

View file

@ -45,9 +45,9 @@ Many community-maintained scrapers are available for download at the [Community
# Translation
[![Translate](https://translate.stashapp.cc/widgets/stash/-/stash-desktop-client/svg-badge.svg)](https://translate.stashapp.cc/engage/stash/)
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇪🇸 🇸🇪 🇹🇼 🇹🇷
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇪🇪 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇷🇺 🇪🇸 🇸🇪 🇹🇼 🇹🇷
Stash is available in 16 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks!
Stash is available in 18 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks!
# Support (FAQ)

View file

@ -16,7 +16,7 @@ ARG STASH_VERSION
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
# Build Backend
FROM golang:1.17-alpine as backend
FROM golang:1.19-alpine as backend
RUN apk add --no-cache make alpine-sdk
WORKDIR /stash
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/

View file

@ -12,7 +12,6 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-linux-arm32v6; \
FROM --platform=$TARGETPLATFORM alpine:latest AS app
COPY --from=binary /stash /usr/bin/
RUN apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg vips-tools ruby && pip install --no-cache-dir mechanicalsoup cloudscraper && gem install faraday
RUN ln -s /usr/bin/python3 /usr/bin/python
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999

View file

@ -1,4 +1,4 @@
FROM golang:1.17
FROM golang:1.19
LABEL maintainer="https://discord.gg/2TsNFKt"

View file

@ -1,6 +1,6 @@
user=stashapp
repo=compiler
version=6
version=7
latest:
docker build -t ${user}/${repo}:latest .

View file

@ -43,9 +43,10 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW. For examp
## Building a release
1. Run `make generate` to create generated files
2. Run `make ui` to compile the frontend
3. Run `make build` to build the executable for your current platform
1. Run `make pre-ui` to install UI dependencies
2. Run `make generate` to create generated files
3. Run `make ui` to compile the frontend
4. Run `make build` to build the executable for your current platform
## Cross compiling

2
go.mod
View file

@ -108,4 +108,4 @@ require (
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
go 1.17
go 1.19

6
go.sum
View file

@ -700,7 +700,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/snowflakedb/gosnowflake v1.4.3/go.mod h1:1kyg2XEduwti88V11PKRHImhXLK5WpGiayY6lFNYb98=
@ -773,7 +772,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
@ -958,7 +956,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1049,11 +1046,8 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs=
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

View file

@ -63,6 +63,8 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
showStudioAsText
css
cssEnabled
javascript
javascriptEnabled
customLocales
customLocalesEnabled
language

View file

@ -7,6 +7,7 @@ fragment VideoFileData on VideoFile {
id
path
size
mod_time
duration
video_codec
audio_codec
@ -24,6 +25,7 @@ fragment ImageFileData on ImageFile {
id
path
size
mod_time
width
height
fingerprints {
@ -36,6 +38,7 @@ fragment GalleryFileData on GalleryFile {
id
path
size
mod_time
fingerprints {
type
value

View file

@ -4,7 +4,7 @@ fragment SlimGalleryData on Gallery {
date
url
details
rating
rating100
organized
files {
...GalleryFileData

View file

@ -6,7 +6,7 @@ fragment GalleryData on Gallery {
date
url
details
rating
rating100
organized
files {
@ -16,9 +16,6 @@ fragment GalleryData on Gallery {
...FolderData
}
images {
...SlimImageData
}
cover {
...SlimImageData
}

View file

@ -1,7 +1,7 @@
fragment SlimImageData on Image {
id
title
rating
rating100
organized
o_counter

View file

@ -1,7 +1,7 @@
fragment ImageData on Image {
id
title
rating
rating100
organized
o_counter
created_at

View file

@ -2,4 +2,5 @@ fragment SlimMovieData on Movie {
id
name
front_image_path
rating100
}

View file

@ -5,7 +5,7 @@ fragment MovieData on Movie {
aliases
duration
date
rating
rating100
director
studio {

View file

@ -13,7 +13,7 @@ fragment SlimPerformerData on Performer {
ethnicity
hair_color
eye_color
height
height_cm
fake_tits
career_length
tattoos
@ -26,7 +26,7 @@ fragment SlimPerformerData on Performer {
endpoint
stash_id
}
rating
rating100
death_date
weight
}

View file

@ -10,7 +10,7 @@ fragment PerformerData on Performer {
ethnicity
country
eye_color
height
height_cm
measurements
fake_tits
career_length
@ -33,7 +33,7 @@ fragment PerformerData on Performer {
stash_id
endpoint
}
rating
rating100
details
death_date
hair_color

View file

@ -1,14 +1,19 @@
fragment SlimSceneData on Scene {
id
title
code
details
director
url
date
rating
rating100
o_counter
organized
interactive
interactive_speed
resume_time
play_duration
play_count
files {
...VideoFileData
@ -20,7 +25,6 @@ fragment SlimSceneData on Scene {
stream
webp
vtt
chapters_vtt
sprite
funscript
interactive_heatmap

View file

@ -1,10 +1,12 @@
fragment SceneData on Scene {
id
title
code
details
director
url
date
rating
rating100
o_counter
organized
interactive
@ -15,6 +17,10 @@ fragment SceneData on Scene {
}
created_at
updated_at
resume_time
last_played_at
play_duration
play_count
files {
...VideoFileData
@ -26,7 +32,6 @@ fragment SceneData on Scene {
stream
webp
vtt
chapters_vtt
sprite
funscript
interactive_heatmap

View file

@ -105,7 +105,9 @@ fragment ScrapedSceneTagData on ScrapedTag {
fragment ScrapedSceneData on ScrapedScene {
title
code
details
director
url
date
image
@ -166,7 +168,9 @@ fragment ScrapedGalleryData on ScrapedGallery {
fragment ScrapedStashBoxSceneData on ScrapedScene {
title
code
details
director
url
date
image

View file

@ -10,6 +10,6 @@ fragment SlimStudioData on Studio {
id
}
details
rating
rating100
aliases
}

View file

@ -25,6 +25,6 @@ fragment StudioData on Studio {
endpoint
}
details
rating
rating100
aliases
}

View file

@ -1,3 +1,11 @@
mutation SceneCreate(
$input: SceneCreateInput!) {
sceneCreate(input: $input) {
...SceneData
}
}
mutation SceneUpdate(
$input: SceneUpdateInput!) {
@ -20,6 +28,14 @@ mutation ScenesUpdate($input : [SceneUpdateInput!]!) {
}
}
mutation SceneSaveActivity($id: ID!, $resume_time: Float, $playDuration: Float) {
sceneSaveActivity(id: $id, resume_time: $resume_time, playDuration: $playDuration)
}
mutation SceneIncrementPlayCount($id: ID!) {
sceneIncrementPlayCount(id: $id)
}
mutation SceneIncrementO($id: ID!) {
sceneIncrementO(id: $id)
}
@ -43,3 +59,13 @@ mutation ScenesDestroy($ids: [ID!]!, $delete_file: Boolean, $delete_generated :
mutation SceneGenerateScreenshot($id: ID!, $at: Float) {
sceneGenerateScreenshot(id: $id, at: $at)
}
mutation SceneAssignFile($input: AssignSceneFileInput!) {
sceneAssignFile(input: $input)
}
mutation SceneMerge($input: SceneMergeInput!) {
sceneMerge(input: $input) {
id
}
}

View file

@ -52,7 +52,9 @@ query ParseSceneFilenames($filter: FindFilterType!, $config: SceneParserInput!)
...SlimSceneData
}
title
code
details
director
url
date
rating

View file

@ -162,7 +162,9 @@ type Mutation {
setup(input: SetupInput!): Boolean!
migrate(input: MigrateInput!): Boolean!
sceneCreate(input: SceneCreateInput!): Scene
sceneUpdate(input: SceneUpdateInput!): Scene
sceneMerge(input: SceneMergeInput!): Scene
bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!]
sceneDestroy(input: SceneDestroyInput!): Boolean!
scenesDestroy(input: ScenesDestroyInput!): Boolean!
@ -175,6 +177,12 @@ type Mutation {
"""Resets the o-counter for a scene to 0. Returns the new value"""
sceneResetO(id: ID!): Int!
"""Sets the resume time point (if provided) and adds the provided duration to the scene's play duration"""
sceneSaveActivity(id: ID!, resume_time: Float, playDuration: Float): Boolean!
"""Increments the play count for the scene. Returns the new play count value."""
sceneIncrementPlayCount(id: ID!): Int!
"""Generates screenshot at specified time in seconds. Leave empty to generate default screenshot"""
sceneGenerateScreenshot(id: ID!, at: Float): String!
@ -182,6 +190,8 @@ type Mutation {
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean!
sceneAssignFile(input: AssignSceneFileInput!): Boolean!
imageUpdate(input: ImageUpdateInput!): Image
bulkImageUpdate(input: BulkImageUpdateInput!): [Image!]
imageDestroy(input: ImageDestroyInput!): Boolean!

View file

@ -264,6 +264,10 @@ input ConfigInterfaceInput {
css: String
cssEnabled: Boolean
"""Custom Javascript"""
javascript: String
javascriptEnabled: Boolean
"""Custom Locales"""
customLocales: String
customLocalesEnabled: Boolean
@ -330,6 +334,10 @@ type ConfigInterfaceResult {
css: String
cssEnabled: Boolean
"""Custom Javascript"""
javascript: String
javascriptEnabled: Boolean
"""Custom Locales"""
customLocales: String
customLocalesEnabled: Boolean

View file

@ -39,6 +39,14 @@ input PHashDuplicationCriterionInput {
distance: Int
}
input StashIDCriterionInput {
"""If present, this value is treated as a predicate.
That is, it will filter based on stash_ids with the matching endpoint"""
endpoint: String
stash_id: String
modifier: CriterionModifier!
}
input PerformerFilterType {
AND: PerformerFilterType
OR: PerformerFilterType
@ -60,7 +68,9 @@ input PerformerFilterType {
"""Filter by eye color"""
eye_color: StringCriterionInput
"""Filter by height"""
height: StringCriterionInput
height: StringCriterionInput @deprecated(reason: "Use height_cm instead")
"""Filter by height in cm"""
height_cm: IntCriterionInput
"""Filter by measurements"""
measurements: StringCriterionInput
"""Filter by fake tits value"""
@ -88,9 +98,13 @@ input PerformerFilterType {
"""Filter by gallery count"""
gallery_count: IntCriterionInput
"""Filter by StashID"""
stash_id: StringCriterionInput
stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead")
"""Filter by StashID"""
stash_id_endpoint: StashIDCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"""Filter by url"""
url: StringCriterionInput
"""Filter by hair color"""
@ -103,6 +117,14 @@ input PerformerFilterType {
studios: HierarchicalMultiCriterionInput
"""Filter by autotag ignore value"""
ignore_auto_tag: Boolean
"""Filter by birthdate"""
birthdate: DateCriterionInput
"""Filter by death date"""
death_date: DateCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input SceneMarkerFilterType {
@ -114,6 +136,16 @@ input SceneMarkerFilterType {
scene_tags: HierarchicalMultiCriterionInput
"""Filter to only include scene markers with these performers"""
performers: MultiCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
"""Filter by scene date"""
scene_date: DateCriterionInput
"""Filter by cscene reation time"""
scene_created_at: TimestampCriterionInput
"""Filter by lscene ast update time"""
scene_updated_at: TimestampCriterionInput
}
input SceneFilterType {
@ -121,8 +153,11 @@ input SceneFilterType {
OR: SceneFilterType
NOT: SceneFilterType
id: IntCriterionInput
title: StringCriterionInput
code: StringCriterionInput
details: StringCriterionInput
director: StringCriterionInput
"""Filter by file oshash"""
oshash: StringCriterionInput
@ -135,7 +170,9 @@ input SceneFilterType {
"""Filter by file count"""
file_count: IntCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"""Filter by organized"""
organized: Boolean
"""Filter by o-counter"""
@ -169,7 +206,9 @@ input SceneFilterType {
"""Filter by performer count"""
performer_count: IntCriterionInput
"""Filter by StashID"""
stash_id: StringCriterionInput
stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead")
"""Filter by StashID"""
stash_id_endpoint: StashIDCriterionInput
"""Filter by url"""
url: StringCriterionInput
"""Filter by interactive"""
@ -178,6 +217,18 @@ input SceneFilterType {
interactive_speed: IntCriterionInput
"""Filter by captions"""
captions: StringCriterionInput
"""Filter by resume time"""
resume_time: IntCriterionInput
"""Filter by play count"""
play_count: IntCriterionInput
"""Filter by play duration (in seconds)"""
play_duration: IntCriterionInput
"""Filter by date"""
date: DateCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input MovieFilterType {
@ -189,7 +240,9 @@ input MovieFilterType {
"""Filter by duration (in seconds)"""
duration: IntCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"""Filter to only include movies with this studio"""
studios: HierarchicalMultiCriterionInput
"""Filter to only include movies missing this property"""
@ -198,6 +251,12 @@ input MovieFilterType {
url: StringCriterionInput
"""Filter to only include movies where performer appears in a scene"""
performers: MultiCriterionInput
"""Filter by date"""
date: DateCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input StudioFilterType {
@ -210,11 +269,15 @@ input StudioFilterType {
"""Filter to only include studios with this parent studio"""
parents: MultiCriterionInput
"""Filter by StashID"""
stash_id: StringCriterionInput
stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead")
"""Filter by StashID"""
stash_id_endpoint: StashIDCriterionInput
"""Filter to only include studios missing this property"""
is_missing: String
"""Filter by rating"""
rating: IntCriterionInput
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"""Filter by scene count"""
scene_count: IntCriterionInput
"""Filter by image count"""
@ -227,6 +290,10 @@ input StudioFilterType {
aliases: StringCriterionInput
"""Filter by autotag ignore value"""
ignore_auto_tag: Boolean
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input GalleryFilterType {
@ -234,6 +301,7 @@ input GalleryFilterType {
OR: GalleryFilterType
NOT: GalleryFilterType
id: IntCriterionInput
title: StringCriterionInput
details: StringCriterionInput
@ -248,7 +316,9 @@ input GalleryFilterType {
"""Filter to include/exclude galleries that were created from zip"""
is_zip: Boolean
"""Filter by rating"""
rating: IntCriterionInput
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"""Filter by organized"""
organized: Boolean
"""Filter by average image resolution"""
@ -273,6 +343,12 @@ input GalleryFilterType {
image_count: IntCriterionInput
"""Filter by url"""
url: StringCriterionInput
"""Filter by date"""
date: DateCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input TagFilterType {
@ -286,6 +362,9 @@ input TagFilterType {
"""Filter by tag aliases"""
aliases: StringCriterionInput
"""Filter by tag description"""
description: StringCriterionInput
"""Filter to only include tags missing this property"""
is_missing: String
@ -318,6 +397,12 @@ input TagFilterType {
"""Filter by autotag ignore value"""
ignore_auto_tag: Boolean
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input ImageFilterType {
@ -327,6 +412,8 @@ input ImageFilterType {
title: StringCriterionInput
""" Filter by image id"""
id: IntCriterionInput
"""Filter by file checksum"""
checksum: StringCriterionInput
"""Filter by path"""
@ -334,7 +421,9 @@ input ImageFilterType {
"""Filter by file count"""
file_count: IntCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"""Filter by organized"""
organized: Boolean
"""Filter by o-counter"""
@ -359,6 +448,10 @@ input ImageFilterType {
performer_favorite: Boolean
"""Filter to only include images with these galleries"""
galleries: MultiCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
enum CriterionModifier {
@ -415,6 +508,18 @@ input HierarchicalMultiCriterionInput {
depth: Int
}
input DateCriterionInput {
value: String!
value2: String
modifier: CriterionModifier!
}
input TimestampCriterionInput {
value: String!
value2: String
modifier: CriterionModifier!
}
enum FilterMode {
SCENES,
PERFORMERS,

View file

@ -7,7 +7,10 @@ type Gallery {
url: String
date: String
details: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean!
created_at: Time!
updated_at: Time!
@ -23,7 +26,7 @@ type Gallery {
performers: [Performer!]!
"""The images in the gallery"""
images: [Image!]! # Resolver
images: [Image!]! @deprecated(reason: "Use findImages")
cover: Image
}
@ -32,7 +35,10 @@ input GalleryCreateInput {
url: String
date: String
details: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
scene_ids: [ID!]
studio_id: ID
@ -47,7 +53,10 @@ input GalleryUpdateInput {
url: String
date: String
details: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
scene_ids: [ID!]
studio_id: ID
@ -63,7 +72,10 @@ input BulkGalleryUpdateInput {
url: String
date: String
details: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
scene_ids: BulkUpdateIds
studio_id: ID

View file

@ -2,7 +2,10 @@ type Image {
id: ID!
checksum: String @deprecated(reason: "Use files.fingerprints")
title: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
o_counter: Int
organized: Boolean!
path: String! @deprecated(reason: "Use files.path")
@ -37,7 +40,10 @@ input ImageUpdateInput {
clientMutationId: String
id: ID!
title: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
studio_id: ID
@ -52,7 +58,10 @@ input BulkImageUpdateInput {
clientMutationId: String
ids: [ID!]
title: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
studio_id: ID

View file

@ -6,7 +6,10 @@ type Movie {
"""Duration in seconds"""
duration: Int
date: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
studio: Studio
director: String
synopsis: String
@ -26,7 +29,10 @@ input MovieCreateInput {
"""Duration in seconds"""
duration: Int
date: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
studio_id: ID
director: String
synopsis: String
@ -43,7 +49,10 @@ input MovieUpdateInput {
aliases: String
duration: Int
date: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
studio_id: ID
director: String
synopsis: String
@ -57,7 +66,10 @@ input MovieUpdateInput {
input BulkMovieUpdateInput {
clientMutationId: String
ids: [ID!]
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
studio_id: ID
director: String
}

View file

@ -19,7 +19,8 @@ type Performer {
ethnicity: String
country: String
eye_color: String
height: String
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
career_length: String
@ -36,7 +37,10 @@ type Performer {
gallery_count: Int # Resolver
scenes: [Scene!]!
stash_ids: [StashID!]!
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
death_date: String
hair_color: String
@ -55,7 +59,9 @@ input PerformerCreateInput {
ethnicity: String
country: String
eye_color: String
height: String
# height must be parsable into an integer
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
career_length: String
@ -69,7 +75,10 @@ input PerformerCreateInput {
"""This should be a URL or a base64 encoded data URL"""
image: String
stash_ids: [StashIDInput!]
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
death_date: String
hair_color: String
@ -86,7 +95,9 @@ input PerformerUpdateInput {
ethnicity: String
country: String
eye_color: String
height: String
# height must be parsable into an integer
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
career_length: String
@ -100,7 +111,10 @@ input PerformerUpdateInput {
"""This should be a URL or a base64 encoded data URL"""
image: String
stash_ids: [StashIDInput!]
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
death_date: String
hair_color: String
@ -117,7 +131,9 @@ input BulkPerformerUpdateInput {
ethnicity: String
country: String
eye_color: String
height: String
# height must be parsable into an integer
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
career_length: String
@ -128,7 +144,10 @@ input BulkPerformerUpdateInput {
instagram: String
favorite: Boolean
tag_ids: BulkUpdateIds
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
death_date: String
hair_color: String

View file

@ -15,7 +15,7 @@ type ScenePathsType {
stream: String # Resolver
webp: String # Resolver
vtt: String # Resolver
chapters_vtt: String # Resolver
chapters_vtt: String @deprecated
sprite: String # Resolver
funscript: String # Resolver
interactive_heatmap: String # Resolver
@ -37,10 +37,15 @@ type Scene {
checksum: String @deprecated(reason: "Use files.fingerprints")
oshash: String @deprecated(reason: "Use files.fingerprints")
title: String
code: String
details: String
director: String
url: String
date: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean!
o_counter: Int
path: String! @deprecated(reason: "Use files.path")
@ -51,6 +56,14 @@ type Scene {
created_at: Time!
updated_at: Time!
file_mod_time: Time
"""The last time play count was updated"""
last_played_at: Time
"""The time index a scene was left at"""
resume_time: Float
"""The total time a scene has spent playing"""
play_duration: Float
"""The number ot times a scene has been played"""
play_count: Int
file: SceneFileType! @deprecated(reason: "Use files")
files: [VideoFile!]!
@ -73,14 +86,17 @@ input SceneMovieInput {
scene_index: Int
}
input SceneUpdateInput {
clientMutationId: String
id: ID!
input SceneCreateInput {
title: String
code: String
details: String
director: String
url: String
date: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
studio_id: ID
gallery_ids: [ID!]
@ -91,6 +107,42 @@ input SceneUpdateInput {
cover_image: String
stash_ids: [StashIDInput!]
"""The first id will be assigned as primary. Files will be reassigned from
existing scenes if applicable. Files must not already be primary for another scene"""
file_ids: [ID!]
}
input SceneUpdateInput {
clientMutationId: String
id: ID!
title: String
code: String
details: String
director: String
url: String
date: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
o_counter: Int
organized: Boolean
studio_id: ID
gallery_ids: [ID!]
performer_ids: [ID!]
movies: [SceneMovieInput!]
tag_ids: [ID!]
"""This should be a URL or a base64 encoded data URL"""
cover_image: String
stash_ids: [StashIDInput!]
"""The time index a scene was left at"""
resume_time: Float
"""The total time a scene has spent playing"""
play_duration: Float
"""The number ot times a scene has been played"""
play_count: Int
primary_file_id: ID
}
@ -109,10 +161,15 @@ input BulkSceneUpdateInput {
clientMutationId: String
ids: [ID!]
title: String
code: String
details: String
director: String
url: String
date: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
studio_id: ID
gallery_ids: BulkUpdateIds
@ -157,10 +214,15 @@ type SceneMovieID {
type SceneParserResult {
scene: Scene!
title: String
code: String
details: String
director: String
url: String
date: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
studio_id: ID
gallery_ids: [ID!]
performer_ids: [ID!]
@ -183,3 +245,17 @@ type SceneStreamEndpoint {
mime_type: String
label: String
}
input AssignSceneFileInput {
scene_id: ID!
file_id: ID!
}
input SceneMergeInput {
"""If destination scene has no files, then the primary file of the
first source scene will be assigned as primary"""
source: [ID!]!
destination: ID!
# values defined here will override values in the destination
values: SceneUpdateInput
}

View file

@ -61,7 +61,9 @@ type ScrapedTag {
type ScrapedScene {
title: String
code: String
details: String
director: String
url: String
date: String
@ -82,7 +84,9 @@ type ScrapedScene {
input ScrapedSceneInput {
title: String
code: String
details: String
director: String
url: String
date: String

View file

@ -13,7 +13,10 @@ type Studio {
image_count: Int # Resolver
gallery_count: Int # Resolver
stash_ids: [StashID!]!
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
created_at: Time!
updated_at: Time!
@ -28,7 +31,10 @@ input StudioCreateInput {
"""This should be a URL or a base64 encoded data URL"""
image: String
stash_ids: [StashIDInput!]
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
aliases: [String!]
ignore_auto_tag: Boolean
@ -42,7 +48,10 @@ input StudioUpdateInput {
"""This should be a URL or a base64 encoded data URL"""
image: String
stash_ids: [StashIDInput!]
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
aliases: [String!]
ignore_auto_tag: Boolean

View file

@ -94,7 +94,9 @@ fragment FingerprintFragment on Fingerprint {
fragment SceneFragment on Scene {
id
title
code
details
director
duration
date
urls {

View file

@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"strconv"
"strings"
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/models"
@ -19,19 +20,35 @@ func getArgumentMap(ctx context.Context) map[string]interface{} {
}
func getUpdateInputMap(ctx context.Context) map[string]interface{} {
return getNamedUpdateInputMap(ctx, updateInputField)
}
func getNamedUpdateInputMap(ctx context.Context, field string) map[string]interface{} {
args := getArgumentMap(ctx)
input := args[updateInputField]
var ret map[string]interface{}
if input != nil {
ret, _ = input.(map[string]interface{})
// field can be qualified
fields := strings.Split(field, ".")
currArgs := args
for _, f := range fields {
v, found := currArgs[f]
if !found {
currArgs = nil
break
}
currArgs, _ = v.(map[string]interface{})
if currArgs == nil {
break
}
}
if ret == nil {
ret = make(map[string]interface{})
if currArgs != nil {
return currArgs
}
return ret
return make(map[string]interface{})
}
func getUpdateInputMaps(ctx context.Context) []map[string]interface{} {
@ -90,6 +107,14 @@ func (t changesetTranslator) nullString(value *string, field string) *sql.NullSt
return ret
}
func (t changesetTranslator) string(value *string, field string) string {
if value == nil {
return ""
}
return *value
}
func (t changesetTranslator) optionalString(value *string, field string) models.OptionalString {
if !t.hasField(field) {
return models.OptionalString{}
@ -128,6 +153,27 @@ func (t changesetTranslator) optionalDate(value *string, field string) models.Op
return models.NewOptionalDate(models.NewDate(*value))
}
func (t changesetTranslator) datePtr(value *string, field string) *models.Date {
if value == nil {
return nil
}
d := models.NewDate(*value)
return &d
}
func (t changesetTranslator) intPtrFromString(value *string, field string) (*int, error) {
if value == nil || *value == "" {
return nil, nil
}
vv, err := strconv.Atoi(*value)
if err != nil {
return nil, fmt.Errorf("converting %v to int: %w", *value, err)
}
return &vv, nil
}
func (t changesetTranslator) nullInt64(value *int, field string) *sql.NullInt64 {
if !t.hasField(field) {
return nil
@ -143,6 +189,56 @@ func (t changesetTranslator) nullInt64(value *int, field string) *sql.NullInt64
return ret
}
func (t changesetTranslator) ratingConversion(legacyValue *int, rating100Value *int) *sql.NullInt64 {
const (
legacyField = "rating"
rating100Field = "rating100"
)
legacyRating := t.nullInt64(legacyValue, legacyField)
if legacyRating != nil {
if legacyRating.Valid {
legacyRating.Int64 = int64(models.Rating5To100(int(legacyRating.Int64)))
}
return legacyRating
}
return t.nullInt64(rating100Value, rating100Field)
}
func (t changesetTranslator) ratingConversionInt(legacyValue *int, rating100Value *int) *int {
const (
legacyField = "rating"
rating100Field = "rating100"
)
legacyRating := t.optionalInt(legacyValue, legacyField)
if legacyRating.Set && !(legacyRating.Null) {
ret := int(models.Rating5To100(int(legacyRating.Value)))
return &ret
}
o := t.optionalInt(rating100Value, rating100Field)
if o.Set && !(o.Null) {
return &o.Value
}
return nil
}
func (t changesetTranslator) ratingConversionOptional(legacyValue *int, rating100Value *int) models.OptionalInt {
const (
legacyField = "rating"
rating100Field = "rating100"
)
legacyRating := t.optionalInt(legacyValue, legacyField)
if legacyRating.Set && !(legacyRating.Null) {
legacyRating.Value = int(models.Rating5To100(int(legacyRating.Value)))
return legacyRating
}
return t.optionalInt(rating100Value, rating100Field)
}
func (t changesetTranslator) optionalInt(value *int, field string) models.OptionalInt {
if !t.hasField(field) {
return models.OptionalInt{}
@ -185,19 +281,12 @@ func (t changesetTranslator) optionalIntFromString(value *string, field string)
return models.NewOptionalInt(vv), nil
}
func (t changesetTranslator) nullBool(value *bool, field string) *sql.NullBool {
if !t.hasField(field) {
return nil
func (t changesetTranslator) bool(value *bool, field string) bool {
if value == nil {
return false
}
ret := &sql.NullBool{}
if value != nil {
ret.Bool = *value
ret.Valid = true
}
return ret
return *value
}
func (t changesetTranslator) optionalBool(value *bool, field string) models.OptionalBool {
@ -207,3 +296,11 @@ func (t changesetTranslator) optionalBool(value *bool, field string) models.Opti
return models.NewOptionalBoolPtr(value)
}
func (t changesetTranslator) optionalFloat64(value *float64, field string) models.OptionalFloat64 {
if !t.hasField(field) {
return models.OptionalFloat64{}
}
return models.NewOptionalFloat64Ptr(value)
}

View file

@ -10,6 +10,7 @@ import (
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/hash"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)
type imageBox struct {
@ -86,7 +87,7 @@ func initialiseCustomImages() {
}
}
func getRandomPerformerImageUsingName(name, gender, customPath string) ([]byte, error) {
func getRandomPerformerImageUsingName(name string, gender models.GenderEnum, customPath string) ([]byte, error) {
var box *imageBox
// If we have a custom path, we should return a new box in the given path.
@ -95,10 +96,10 @@ func getRandomPerformerImageUsingName(name, gender, customPath string) ([]byte,
}
if box == nil {
switch strings.ToUpper(gender) {
case "FEMALE":
switch gender {
case models.GenderEnumFemale:
box = performerBox
case "MALE":
case models.GenderEnumMale:
box = performerBoxMale
default:
box = performerBox

View file

@ -26,6 +26,14 @@ var matcher = language.NewMatcher([]language.Tag{
language.MustParse("da-DK"),
language.MustParse("pl-PL"),
language.MustParse("ko-KR"),
language.MustParse("cs-CZ"),
language.MustParse("bn-BD"),
language.MustParse("et-EE"),
language.MustParse("fa-IR"),
language.MustParse("hu-HU"),
language.MustParse("ro-RO"),
language.MustParse("th-TH"),
language.MustParse("uk-UA"),
})
// newCollator parses a locale into a collator

View file

@ -95,8 +95,12 @@ func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) err
return txn.WithTxn(ctx, r.txnManager, fn)
}
func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithReadTxn(ctx, r.txnManager, fn)
}
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.Wall(ctx, q)
return err
}); err != nil {
@ -106,7 +110,7 @@ func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*model
}
func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models.Scene, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.Wall(ctx, q)
return err
}); err != nil {
@ -117,7 +121,7 @@ func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models
}
func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *string) (ret []*models.MarkerStringsResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.GetMarkerStrings(ctx, q, sort)
return err
}); err != nil {
@ -129,7 +133,7 @@ func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *stri
func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
var ret StatsResultType
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
repo := r.repository
scenesQB := repo.Scene
imageQB := repo.Image
@ -205,7 +209,7 @@ func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([
var keys []int
tags := make(map[int]*SceneMarkerTag)
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneMarkers, err := r.repository.SceneMarker.FindBySceneID(ctx, sceneID)
if err != nil {
return err

View file

@ -73,7 +73,7 @@ func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*Fol
var ret *file.Folder
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Folder.Find(ctx, *obj.FolderID)
@ -123,8 +123,9 @@ func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery)
return nil, nil
}
// Images is deprecated, slow and shouldn't be used
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret []*models.Image, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
// #2376 - sort images by path
@ -143,25 +144,10 @@ func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret
}
func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *models.Image, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
// doing this via Query is really slow, so stick with FindByGalleryID
imgs, err := r.repository.Image.FindByGalleryID(ctx, 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
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
// find cover.jpg first
ret, err = image.FindGalleryCover(ctx, r.repository.Image, obj.ID)
return err
}); err != nil {
return nil, err
}
@ -179,7 +165,7 @@ func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*strin
func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (string, error) {
if !obj.Files.PrimaryLoaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPrimaryFile(ctx, r.repository.File)
}); err != nil {
return "", err
@ -189,9 +175,21 @@ func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (st
return obj.PrimaryChecksum(), nil
}
func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *galleryResolver) Rating100(ctx context.Context, obj *models.Gallery) (*int, error) {
return obj.Rating, nil
}
func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) {
if !obj.SceneIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadSceneIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
@ -213,7 +211,7 @@ func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
@ -227,7 +225,7 @@ func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) {
if !obj.PerformerIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPerformerIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
@ -240,7 +238,7 @@ func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (
}
func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (ret int, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Image.CountByGalleryID(ctx, obj.ID)
return err

View file

@ -132,7 +132,7 @@ func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*ImagePat
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret []*models.Gallery, err error) {
if !obj.GalleryIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadGalleryIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err
@ -144,6 +144,18 @@ func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret [
return ret, firstError(errs)
}
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *imageResolver) Rating100(ctx context.Context, obj *models.Image) (*int, error) {
return obj.Rating, nil
}
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *models.Studio, err error) {
if obj.StudioID == nil {
return nil, nil
@ -154,7 +166,7 @@ func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *mod
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err
@ -168,7 +180,7 @@ func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*mod
func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret []*models.Performer, err error) {
if !obj.PerformerIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPerformerIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err

View file

@ -48,6 +48,14 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e
}
func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, error) {
if obj.Rating.Valid {
rating := models.Rating100To5(int(obj.Rating.Int64))
return &rating, nil
}
return nil, nil
}
func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
@ -86,7 +94,7 @@ func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (
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.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
img, err = r.repository.Movie.GetBackImage(ctx, obj.ID)
if err != nil {
@ -109,7 +117,7 @@ func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Scene.CountByMovieID(ctx, obj.ID)
return err
}); err != nil {
@ -120,7 +128,7 @@ func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret
}
func (r *movieResolver) Scenes(ctx context.Context, obj *models.Movie) (ret []*models.Scene, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Scene.FindByMovieID(ctx, obj.ID)
return err

View file

@ -2,7 +2,7 @@ package api
import (
"context"
"time"
"strconv"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/gallery"
@ -10,131 +10,26 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *performerResolver) Name(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Name.Valid {
return &obj.Name.String, nil
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Height != nil {
ret := strconv.Itoa(*obj.Height)
return &ret, nil
}
return nil, nil
}
func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.URL.Valid {
return &obj.URL.String, nil
}
return nil, nil
}
func (r *performerResolver) Gender(ctx context.Context, obj *models.Performer) (*models.GenderEnum, error) {
var ret models.GenderEnum
if obj.Gender.Valid {
ret = models.GenderEnum(obj.Gender.String)
if ret.IsValid() {
return &ret, nil
}
}
return nil, nil
}
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Twitter.Valid {
return &obj.Twitter.String, nil
}
return nil, nil
}
func (r *performerResolver) Instagram(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Instagram.Valid {
return &obj.Instagram.String, nil
}
return nil, nil
func (r *performerResolver) HeightCm(ctx context.Context, obj *models.Performer) (*int, error) {
return obj.Height, nil
}
func (r *performerResolver) Birthdate(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Birthdate.Valid {
return &obj.Birthdate.String, nil
if obj.Birthdate != nil {
ret := obj.Birthdate.String()
return &ret, nil
}
return nil, nil
}
func (r *performerResolver) Ethnicity(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Ethnicity.Valid {
return &obj.Ethnicity.String, nil
}
return nil, nil
}
func (r *performerResolver) Country(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Country.Valid {
return &obj.Country.String, nil
}
return nil, nil
}
func (r *performerResolver) EyeColor(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.EyeColor.Valid {
return &obj.EyeColor.String, nil
}
return nil, nil
}
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Height.Valid {
return &obj.Height.String, nil
}
return nil, nil
}
func (r *performerResolver) Measurements(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Measurements.Valid {
return &obj.Measurements.String, nil
}
return nil, nil
}
func (r *performerResolver) FakeTits(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.FakeTits.Valid {
return &obj.FakeTits.String, nil
}
return nil, nil
}
func (r *performerResolver) CareerLength(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.CareerLength.Valid {
return &obj.CareerLength.String, nil
}
return nil, nil
}
func (r *performerResolver) Tattoos(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Tattoos.Valid {
return &obj.Tattoos.String, nil
}
return nil, nil
}
func (r *performerResolver) Piercings(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Piercings.Valid {
return &obj.Piercings.String, nil
}
return nil, nil
}
func (r *performerResolver) Aliases(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Aliases.Valid {
return &obj.Aliases.String, nil
}
return nil, nil
}
func (r *performerResolver) Favorite(ctx context.Context, obj *models.Performer) (bool, error) {
if obj.Favorite.Valid {
return obj.Favorite.Bool, nil
}
return false, nil
}
func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer) (*string, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
imagePath := urlbuilders.NewPerformerURLBuilder(baseURL, obj).GetPerformerImageURL()
@ -142,7 +37,7 @@ func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer
}
func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
@ -154,7 +49,7 @@ func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (re
func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Scene.CountByPerformerID(ctx, obj.ID)
return err
}); err != nil {
@ -166,7 +61,7 @@ func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performe
func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = image.CountByPerformerID(ctx, r.repository.Image, obj.ID)
return err
}); err != nil {
@ -178,7 +73,7 @@ func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performe
func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = gallery.CountByPerformerID(ctx, r.repository.Gallery, obj.ID)
return err
}); err != nil {
@ -189,7 +84,7 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
}
func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (ret []*models.Scene, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
@ -201,7 +96,7 @@ func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) {
var ret []models.StashID
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Performer.GetStashIDs(ctx, obj.ID)
return err
@ -213,52 +108,27 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer)
}
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
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) Rating100(ctx context.Context, obj *models.Performer) (*int, error) {
return obj.Rating, nil
}
func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.DeathDate.Valid {
return &obj.DeathDate.String, nil
if obj.DeathDate != nil {
ret := obj.DeathDate.String()
return &ret, 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
}
func (r *performerResolver) CreatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *performerResolver) UpdatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
@ -270,7 +140,7 @@ func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
return err
}); err != nil {

View file

@ -29,6 +29,8 @@ func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (
obj.Files.SetPrimary(ret)
return ret, nil
} else {
_ = obj.LoadPrimaryFile(ctx, r.repository.File)
}
return nil, nil
@ -139,6 +141,18 @@ func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoF
return ret, nil
}
func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *sceneResolver) Rating100(ctx context.Context, obj *models.Scene) (*int, error) {
return obj.Rating, nil
}
func resolveFingerprints(f *file.BaseFile) []*Fingerprint {
ret := make([]*Fingerprint, len(f.Fingerprints))
@ -192,7 +206,7 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
}
func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (ret []*models.SceneMarker, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.FindBySceneID(ctx, obj.ID)
return err
}); err != nil {
@ -211,7 +225,7 @@ func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []
return nil, nil
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.File.GetCaptions(ctx, primaryFile.Base().ID)
return err
}); err != nil {
@ -223,7 +237,7 @@ func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
if !obj.GalleryIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadGalleryIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
@ -245,7 +259,7 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) {
if !obj.Movies.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
return obj.LoadMovies(ctx, qb)
@ -276,7 +290,7 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
@ -290,7 +304,7 @@ func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*mod
func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) {
if !obj.PerformerIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPerformerIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
@ -313,7 +327,7 @@ func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID {
}
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadStashIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err

View file

@ -13,7 +13,7 @@ func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker
panic("Invalid scene id")
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneID := int(obj.SceneID.Int64)
ret, err = r.repository.Scene.Find(ctx, sceneID)
return err
@ -25,7 +25,7 @@ func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker
}
func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneMarker) (ret *models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.Find(ctx, obj.PrimaryTagID)
return err
}); err != nil {
@ -36,7 +36,7 @@ func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneM
}
func (r *sceneMarkerResolver) Tags(ctx context.Context, obj *models.SceneMarker) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindBySceneMarkerID(ctx, obj.ID)
return err
}); err != nil {

View file

@ -30,7 +30,7 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj).GetStudioImageURL()
var hasImage bool
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
hasImage, err = r.repository.Studio.HasImage(ctx, obj.ID)
return err
@ -47,7 +47,7 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
}
func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) (ret []string, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.GetAliases(ctx, obj.ID)
return err
}); err != nil {
@ -59,7 +59,7 @@ func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) (ret [
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Scene.CountByStudioID(ctx, obj.ID)
return err
}); err != nil {
@ -71,7 +71,7 @@ func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (re
func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = image.CountByStudioID(ctx, r.repository.Image, obj.ID)
return err
}); err != nil {
@ -83,7 +83,7 @@ func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (re
func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = gallery.CountByStudioID(ctx, r.repository.Gallery, obj.ID)
return err
}); err != nil {
@ -102,7 +102,7 @@ func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (
}
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (ret []*models.Studio, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.FindChildren(ctx, obj.ID)
return err
}); err != nil {
@ -114,7 +114,7 @@ func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*models.StashID, error) {
var ret []models.StashID
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Studio.GetStashIDs(ctx, obj.ID)
return err
@ -126,6 +126,14 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*m
}
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
if obj.Rating.Valid {
rating := models.Rating100To5(int(obj.Rating.Int64))
return &rating, nil
}
return nil, nil
}
func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
@ -149,7 +157,7 @@ func (r *studioResolver) UpdatedAt(ctx context.Context, obj *models.Studio) (*ti
}
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
return err
}); err != nil {
@ -161,7 +169,7 @@ func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Movie.CountByStudioID(ctx, obj.ID)
return err
}); err != nil {

View file

@ -18,7 +18,7 @@ func (r *tagResolver) Description(ctx context.Context, obj *models.Tag) (*string
}
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByChildTagID(ctx, obj.ID)
return err
}); err != nil {
@ -29,7 +29,7 @@ func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*mode
}
func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByParentTagID(ctx, obj.ID)
return err
}); err != nil {
@ -40,7 +40,7 @@ func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*mod
}
func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []string, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.GetAliases(ctx, obj.ID)
return err
}); err != nil {
@ -52,7 +52,7 @@ func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []strin
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
var count int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
count, err = r.repository.Scene.CountByTagID(ctx, obj.ID)
return err
}); err != nil {
@ -64,7 +64,7 @@ func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (ret *int
func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
var count int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
count, err = r.repository.SceneMarker.CountByTagID(ctx, obj.ID)
return err
}); err != nil {
@ -76,7 +76,7 @@ func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (re
func (r *tagResolver) ImageCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = image.CountByTagID(ctx, r.repository.Image, obj.ID)
return err
}); err != nil {
@ -88,7 +88,7 @@ func (r *tagResolver) ImageCount(ctx context.Context, obj *models.Tag) (ret *int
func (r *tagResolver) GalleryCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = gallery.CountByTagID(ctx, r.repository.Gallery, obj.ID)
return err
}); err != nil {
@ -100,7 +100,7 @@ func (r *tagResolver) GalleryCount(ctx context.Context, obj *models.Tag) (ret *i
func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
var count int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
count, err = r.repository.Performer.CountByTagID(ctx, obj.ID)
return err
}); err != nil {

View file

@ -58,7 +58,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
validateDir := func(key string, value string, optional bool) error {
if err := checkConfigOverride(config.Metadata); err != nil {
if err := checkConfigOverride(key); err != nil {
return err
}
@ -365,6 +365,12 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
setBool(config.CSSEnabled, input.CSSEnabled)
if input.Javascript != nil {
c.SetJavascript(*input.Javascript)
}
setBool(config.JavascriptEnabled, input.JavascriptEnabled)
if input.CustomLocales != nil {
c.SetCustomLocales(*input.CustomLocales)
}

View file

@ -68,7 +68,13 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
d := models.NewDate(*input.Date)
newGallery.Date = &d
}
newGallery.Rating = input.Rating
if input.Rating100 != nil {
newGallery.Rating = input.Rating100
} else if input.Rating != nil {
rating := models.Rating5To100(*input.Rating)
newGallery.Rating = &rating
}
if input.StudioID != nil {
studioID, _ := strconv.Atoi(*input.StudioID)
@ -177,8 +183,8 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
if input.Title != nil {
// ensure title is not empty
if *input.Title == "" {
return nil, errors.New("title must not be empty")
if *input.Title == "" && originalGallery.IsUserCreated() {
return nil, errors.New("title must not be empty for user-created galleries")
}
updatedGallery.Title = models.NewOptionalString(*input.Title)
@ -187,7 +193,7 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
updatedGallery.Details = translator.optionalString(input.Details, "details")
updatedGallery.URL = translator.optionalString(input.URL, "url")
updatedGallery.Date = translator.optionalDate(input.Date, "date")
updatedGallery.Rating = translator.optionalInt(input.Rating, "rating")
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
@ -262,8 +268,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
updatedGallery.Details = translator.optionalString(input.Details, "details")
updatedGallery.URL = translator.optionalString(input.URL, "url")
updatedGallery.Date = translator.optionalDate(input.Date, "date")
updatedGallery.Rating = translator.optionalInt(input.Rating, "rating")
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
var err error
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {

View file

@ -103,7 +103,7 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
updatedImage := models.NewImagePartial()
updatedImage.Title = translator.optionalString(input.Title, "title")
updatedImage.Rating = translator.optionalInt(input.Rating, "rating")
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
@ -189,7 +189,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
}
updatedImage.Title = translator.optionalString(input.Title, "title")
updatedImage.Rating = translator.optionalInt(input.Rating, "rating")
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)

View file

@ -76,9 +76,11 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
newMovie.Date = models.SQLiteDate{String: *input.Date, Valid: true}
}
if input.Rating != nil {
rating := int64(*input.Rating)
newMovie.Rating = sql.NullInt64{Int64: rating, Valid: true}
if input.Rating100 != nil {
newMovie.Rating = sql.NullInt64{Int64: int64(*input.Rating100), Valid: true}
} else if input.Rating != nil {
rating := models.Rating5To100(*input.Rating)
newMovie.Rating = sql.NullInt64{Int64: int64(rating), Valid: true}
}
if input.StudioID != nil {
@ -166,7 +168,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
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.Rating = translator.ratingConversion(input.Rating, input.Rating100)
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedMovie.Director = translator.nullString(input.Director, "director")
updatedMovie.Synopsis = translator.nullString(input.Synopsis, "synopsis")
@ -239,7 +241,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
updatedMovie.Rating = translator.nullInt64(input.Rating, "rating")
updatedMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100)
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedMovie.Director = translator.nullString(input.Director, "director")

View file

@ -2,7 +2,6 @@ package api
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
@ -54,78 +53,85 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
// Populate a new performer from the input
currentTime := time.Now()
newPerformer := models.Performer{
Name: input.Name,
Checksum: checksum,
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
CreatedAt: currentTime,
UpdatedAt: currentTime,
}
newPerformer.Name = sql.NullString{String: input.Name, Valid: true}
if input.URL != nil {
newPerformer.URL = sql.NullString{String: *input.URL, Valid: true}
newPerformer.URL = *input.URL
}
if input.Gender != nil {
newPerformer.Gender = sql.NullString{String: input.Gender.String(), Valid: true}
newPerformer.Gender = *input.Gender
}
if input.Birthdate != nil {
newPerformer.Birthdate = models.SQLiteDate{String: *input.Birthdate, Valid: true}
d := models.NewDate(*input.Birthdate)
newPerformer.Birthdate = &d
}
if input.Ethnicity != nil {
newPerformer.Ethnicity = sql.NullString{String: *input.Ethnicity, Valid: true}
newPerformer.Ethnicity = *input.Ethnicity
}
if input.Country != nil {
newPerformer.Country = sql.NullString{String: *input.Country, Valid: true}
newPerformer.Country = *input.Country
}
if input.EyeColor != nil {
newPerformer.EyeColor = sql.NullString{String: *input.EyeColor, Valid: true}
newPerformer.EyeColor = *input.EyeColor
}
if input.Height != nil {
newPerformer.Height = sql.NullString{String: *input.Height, Valid: true}
// prefer height_cm over height
if input.HeightCm != nil {
newPerformer.Height = input.HeightCm
} else if input.Height != nil {
h, err := strconv.Atoi(*input.Height)
if err != nil {
return nil, fmt.Errorf("invalid height: %s", *input.Height)
}
newPerformer.Height = &h
}
if input.Measurements != nil {
newPerformer.Measurements = sql.NullString{String: *input.Measurements, Valid: true}
newPerformer.Measurements = *input.Measurements
}
if input.FakeTits != nil {
newPerformer.FakeTits = sql.NullString{String: *input.FakeTits, Valid: true}
newPerformer.FakeTits = *input.FakeTits
}
if input.CareerLength != nil {
newPerformer.CareerLength = sql.NullString{String: *input.CareerLength, Valid: true}
newPerformer.CareerLength = *input.CareerLength
}
if input.Tattoos != nil {
newPerformer.Tattoos = sql.NullString{String: *input.Tattoos, Valid: true}
newPerformer.Tattoos = *input.Tattoos
}
if input.Piercings != nil {
newPerformer.Piercings = sql.NullString{String: *input.Piercings, Valid: true}
newPerformer.Piercings = *input.Piercings
}
if input.Aliases != nil {
newPerformer.Aliases = sql.NullString{String: *input.Aliases, Valid: true}
newPerformer.Aliases = *input.Aliases
}
if input.Twitter != nil {
newPerformer.Twitter = sql.NullString{String: *input.Twitter, Valid: true}
newPerformer.Twitter = *input.Twitter
}
if input.Instagram != nil {
newPerformer.Instagram = sql.NullString{String: *input.Instagram, Valid: true}
newPerformer.Instagram = *input.Instagram
}
if input.Favorite != nil {
newPerformer.Favorite = sql.NullBool{Bool: *input.Favorite, Valid: true}
} else {
newPerformer.Favorite = sql.NullBool{Bool: false, Valid: true}
newPerformer.Favorite = *input.Favorite
}
if input.Rating != nil {
newPerformer.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
newPerformer.Rating = sql.NullInt64{Valid: false}
if input.Rating100 != nil {
newPerformer.Rating = input.Rating100
} else if input.Rating != nil {
rating := models.Rating5To100(*input.Rating)
newPerformer.Rating = &rating
}
if input.Details != nil {
newPerformer.Details = sql.NullString{String: *input.Details, Valid: true}
newPerformer.Details = *input.Details
}
if input.DeathDate != nil {
newPerformer.DeathDate = models.SQLiteDate{String: *input.DeathDate, Valid: true}
d := models.NewDate(*input.DeathDate)
newPerformer.DeathDate = &d
}
if input.HairColor != nil {
newPerformer.HairColor = sql.NullString{String: *input.HairColor, Valid: true}
newPerformer.HairColor = *input.HairColor
}
if input.Weight != nil {
weight := int64(*input.Weight)
newPerformer.Weight = sql.NullInt64{Int64: weight, Valid: true}
newPerformer.Weight = input.Weight
}
if input.IgnoreAutoTag != nil {
newPerformer.IgnoreAutoTag = *input.IgnoreAutoTag
@ -138,24 +144,23 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
}
// Start the transaction and save the performer
var performer *models.Performer
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
performer, err = qb.Create(ctx, newPerformer)
err = qb.Create(ctx, &newPerformer)
if err != nil {
return err
}
if len(input.TagIds) > 0 {
if err := r.updatePerformerTags(ctx, performer.ID, input.TagIds); err != nil {
if err := r.updatePerformerTags(ctx, newPerformer.ID, input.TagIds); err != nil {
return err
}
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(ctx, performer.ID, imageData); err != nil {
if err := qb.UpdateImage(ctx, newPerformer.ID, imageData); err != nil {
return err
}
}
@ -163,7 +168,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
// Save the stash_ids
if input.StashIds != nil {
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, performer.ID, stashIDJoins); err != nil {
if err := qb.UpdateStashIDs(ctx, newPerformer.ID, stashIDJoins); err != nil {
return err
}
}
@ -173,17 +178,14 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, performer.ID, plugin.PerformerCreatePost, input, nil)
return r.getPerformer(ctx, performer.ID)
r.hookExecutor.ExecutePostHooks(ctx, newPerformer.ID, plugin.PerformerCreatePost, input, nil)
return r.getPerformer(ctx, newPerformer.ID)
}
func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerUpdateInput) (*models.Performer, error) {
// Populate performer from the input
performerID, _ := strconv.Atoi(input.ID)
updatedPerformer := models.PerformerPartial{
ID: performerID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
}
updatedPerformer := models.NewPerformerPartial()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
@ -203,54 +205,62 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
// generate checksum from performer name rather than image
checksum := md5.FromString(*input.Name)
updatedPerformer.Name = &sql.NullString{String: *input.Name, Valid: true}
updatedPerformer.Checksum = &checksum
updatedPerformer.Name = models.NewOptionalString(*input.Name)
updatedPerformer.Checksum = models.NewOptionalString(checksum)
}
updatedPerformer.URL = translator.nullString(input.URL, "url")
updatedPerformer.URL = translator.optionalString(input.URL, "url")
if translator.hasField("gender") {
if input.Gender != nil {
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
updatedPerformer.Gender = models.NewOptionalString(input.Gender.String())
} else {
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
updatedPerformer.Gender = models.NewOptionalStringPtr(nil)
}
}
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")
updatedPerformer.IgnoreAutoTag = input.IgnoreAutoTag
updatedPerformer.Birthdate = translator.optionalDate(input.Birthdate, "birthdate")
updatedPerformer.Country = translator.optionalString(input.Country, "country")
updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color")
updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements")
// prefer height_cm over height
if translator.hasField("height_cm") {
updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm")
} else if translator.hasField("height") {
updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height")
if err != nil {
return nil, err
}
}
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedPerformer.Details = translator.optionalString(input.Details, "details")
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
// Start the transaction and save the p
var p *models.Performer
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
// need to get existing performer
existing, err := qb.Find(ctx, updatedPerformer.ID)
existing, err := qb.Find(ctx, performerID)
if err != nil {
return err
}
if existing == nil {
return fmt.Errorf("performer with id %d not found", updatedPerformer.ID)
return fmt.Errorf("performer with id %d not found", performerID)
}
if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil {
@ -259,26 +269,26 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
}
}
p, err = qb.Update(ctx, updatedPerformer)
_, err = qb.UpdatePartial(ctx, performerID, updatedPerformer)
if err != nil {
return err
}
// Save the tags
if translator.hasField("tag_ids") {
if err := r.updatePerformerTags(ctx, p.ID, input.TagIds); err != nil {
if err := r.updatePerformerTags(ctx, performerID, input.TagIds); err != nil {
return err
}
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(ctx, p.ID, imageData); err != nil {
if err := qb.UpdateImage(ctx, performerID, imageData); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyImage(ctx, p.ID); err != nil {
if err := qb.DestroyImage(ctx, performerID); err != nil {
return err
}
}
@ -296,8 +306,8 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, p.ID, plugin.PerformerUpdatePost, input, translator.getFields())
return r.getPerformer(ctx, p.ID)
r.hookExecutor.ExecutePostHooks(ctx, performerID, plugin.PerformerUpdatePost, input, translator.getFields())
return r.getPerformer(ctx, performerID)
}
func (r *mutationResolver) updatePerformerTags(ctx context.Context, performerID int, tagsIDs []string) error {
@ -315,43 +325,48 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
}
// Populate performer from the input
updatedTime := time.Now()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedPerformer := models.PerformerPartial{
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
updatedPerformer := models.NewPerformerPartial()
updatedPerformer.URL = translator.optionalString(input.URL, "url")
updatedPerformer.Birthdate = translator.optionalDate(input.Birthdate, "birthdate")
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.optionalString(input.Country, "country")
updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color")
// prefer height_cm over height
if translator.hasField("height_cm") {
updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm")
} else if translator.hasField("height") {
updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height")
if err != nil {
return nil, err
}
}
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")
updatedPerformer.IgnoreAutoTag = input.IgnoreAutoTag
updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements")
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedPerformer.Details = translator.optionalString(input.Details, "details")
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
if translator.hasField("gender") {
if input.Gender != nil {
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
updatedPerformer.Gender = models.NewOptionalString(input.Gender.String())
} else {
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
updatedPerformer.Gender = models.NewOptionalStringPtr(nil)
}
}
@ -378,7 +393,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
return err
}
performer, err := qb.Update(ctx, updatedPerformer)
performer, err := qb.UpdatePartial(ctx, performerID, updatedPerformer)
if err != nil {
return err
}

View file

@ -3,6 +3,7 @@ package api
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"time"
@ -30,6 +31,79 @@ func (r *mutationResolver) getScene(ctx context.Context, id int) (ret *models.Sc
return ret, nil
}
func (r *mutationResolver) SceneCreate(ctx context.Context, input SceneCreateInput) (ret *models.Scene, err error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
performerIDs, err := stringslice.StringSliceToIntSlice(input.PerformerIds)
if err != nil {
return nil, fmt.Errorf("converting performer ids: %w", err)
}
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
galleryIDs, err := stringslice.StringSliceToIntSlice(input.GalleryIds)
if err != nil {
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
moviesScenes, err := models.MoviesScenesFromInput(input.Movies)
if err != nil {
return nil, fmt.Errorf("converting movies scenes: %w", err)
}
fileIDsInt, err := stringslice.StringSliceToIntSlice(input.FileIds)
if err != nil {
return nil, fmt.Errorf("converting file ids: %w", err)
}
fileIDs := make([]file.ID, len(fileIDsInt))
for i, v := range fileIDsInt {
fileIDs[i] = file.ID(v)
}
newScene := models.Scene{
Title: translator.string(input.Title, "title"),
Code: translator.string(input.Code, "code"),
Details: translator.string(input.Details, "details"),
Director: translator.string(input.Director, "director"),
URL: translator.string(input.URL, "url"),
Date: translator.datePtr(input.Date, "date"),
Rating: translator.ratingConversionInt(input.Rating, input.Rating100),
Organized: translator.bool(input.Organized, "organized"),
PerformerIDs: models.NewRelatedIDs(performerIDs),
TagIDs: models.NewRelatedIDs(tagIDs),
GalleryIDs: models.NewRelatedIDs(galleryIDs),
Movies: models.NewRelatedMovies(moviesScenes),
StashIDs: models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)),
}
newScene.StudioID, err = translator.intPtrFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
var coverImageData []byte
if input.CoverImage != nil && *input.CoverImage != "" {
var err error
coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage)
if err != nil {
return nil, err
}
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.Resolver.sceneService.Create(ctx, &newScene, fileIDs, coverImageData)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (ret *models.Scene, err error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
@ -90,32 +164,19 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
return newRet, nil
}
func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUpdateInput, translator changesetTranslator) (*models.Scene, error) {
// Populate scene from the input
sceneID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
qb := r.repository.Scene
s, err := qb.Find(ctx, sceneID)
if err != nil {
return nil, err
}
if s == nil {
return nil, fmt.Errorf("scene with id %d not found", sceneID)
}
var coverImageData []byte
func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTranslator) (*models.ScenePartial, error) {
updatedScene := models.NewScenePartial()
updatedScene.Title = translator.optionalString(input.Title, "title")
updatedScene.Code = translator.optionalString(input.Code, "code")
updatedScene.Details = translator.optionalString(input.Details, "details")
updatedScene.Director = translator.optionalString(input.Director, "director")
updatedScene.URL = translator.optionalString(input.URL, "url")
updatedScene.Date = translator.optionalDate(input.Date, "date")
updatedScene.Rating = translator.optionalInt(input.Rating, "rating")
updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedScene.OCounter = translator.optionalInt(input.OCounter, "o_counter")
updatedScene.PlayCount = translator.optionalInt(input.PlayCount, "play_count")
updatedScene.PlayDuration = translator.optionalFloat64(input.PlayDuration, "play_duration")
var err error
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
@ -131,36 +192,6 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp
converted := file.ID(primaryFileID)
updatedScene.PrimaryFileID = &converted
// if file hash has changed, we should migrate generated files
// after commit
if err := s.LoadFiles(ctx, r.repository.Scene); err != nil {
return nil, err
}
// ensure that new primary file is associated with scene
var f *file.VideoFile
for _, ff := range s.Files.List() {
if ff.ID == converted {
f = ff
}
}
if f == nil {
return nil, fmt.Errorf("file with id %d not associated with scene", converted)
}
fileNamingAlgorithm := config.GetInstance().GetVideoFileNamingAlgorithm()
oldHash := scene.GetHash(s.Files.Primary(), fileNamingAlgorithm)
newHash := scene.GetHash(f, fileNamingAlgorithm)
if oldHash != "" && newHash != "" && oldHash != newHash {
// perform migration after commit
txn.AddPostCommitHook(ctx, func(ctx context.Context) error {
scene.MigrateHash(manager.GetInstance().Paths, oldHash, newHash)
return nil
})
}
}
if translator.hasField("performer_ids") {
@ -200,39 +231,107 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp
}
}
return &updatedScene, nil
}
func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUpdateInput, translator changesetTranslator) (*models.Scene, error) {
// Populate scene from the input
sceneID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
qb := r.repository.Scene
s, err := qb.Find(ctx, sceneID)
if err != nil {
return nil, err
}
if s == nil {
return nil, fmt.Errorf("scene with id %d not found", sceneID)
}
var coverImageData []byte
updatedScene, err := scenePartialFromInput(input, translator)
if err != nil {
return nil, err
}
// ensure that title is set where scene has no file
if updatedScene.Title.Set && updatedScene.Title.Value == "" {
if err := s.LoadFiles(ctx, r.repository.Scene); err != nil {
return nil, err
}
if len(s.Files.List()) == 0 {
return nil, errors.New("title must be set if scene has no files")
}
}
if updatedScene.PrimaryFileID != nil {
newPrimaryFileID := *updatedScene.PrimaryFileID
// if file hash has changed, we should migrate generated files
// after commit
if err := s.LoadFiles(ctx, r.repository.Scene); err != nil {
return nil, err
}
// ensure that new primary file is associated with scene
var f *file.VideoFile
for _, ff := range s.Files.List() {
if ff.ID == newPrimaryFileID {
f = ff
}
}
if f == nil {
return nil, fmt.Errorf("file with id %d not associated with scene", newPrimaryFileID)
}
}
if input.CoverImage != nil && *input.CoverImage != "" {
var err error
coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage)
if err != nil {
return nil, err
}
// update the cover after updating the scene
}
s, err = qb.UpdatePartial(ctx, sceneID, updatedScene)
s, err = qb.UpdatePartial(ctx, sceneID, *updatedScene)
if err != nil {
return nil, err
}
// update cover table
if len(coverImageData) > 0 {
if err := qb.UpdateCover(ctx, sceneID, coverImageData); err != nil {
return nil, err
}
}
// only update the cover image if provided and everything else was successful
if coverImageData != nil {
err = scene.SetScreenshot(manager.GetInstance().Paths, s.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
if err != nil {
return nil, err
}
if err := r.sceneUpdateCoverImage(ctx, s, coverImageData); err != nil {
return nil, err
}
return s, nil
}
func (r *mutationResolver) sceneUpdateCoverImage(ctx context.Context, s *models.Scene, coverImageData []byte) error {
if len(coverImageData) > 0 {
qb := r.repository.Scene
// update cover table
if err := qb.UpdateCover(ctx, s.ID, coverImageData); err != nil {
return err
}
if s.Path != "" {
// update the file-based screenshot after commit
txn.AddPostCommitHook(ctx, func(ctx context.Context) error {
return scene.SetScreenshot(manager.GetInstance().Paths, s.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
})
}
}
return nil
}
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneUpdateInput) ([]*models.Scene, error) {
sceneIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
@ -246,10 +345,12 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
updatedScene := models.NewScenePartial()
updatedScene.Title = translator.optionalString(input.Title, "title")
updatedScene.Code = translator.optionalString(input.Code, "code")
updatedScene.Details = translator.optionalString(input.Details, "details")
updatedScene.Director = translator.optionalString(input.Director, "director")
updatedScene.URL = translator.optionalString(input.URL, "url")
updatedScene.Date = translator.optionalDate(input.Date, "date")
updatedScene.Rating = translator.optionalInt(input.Rating, "rating")
updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
@ -482,6 +583,84 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
return true, nil
}
func (r *mutationResolver) SceneAssignFile(ctx context.Context, input AssignSceneFileInput) (bool, error) {
sceneID, err := strconv.Atoi(input.SceneID)
if err != nil {
return false, fmt.Errorf("converting scene ID: %w", err)
}
fileIDInt, err := strconv.Atoi(input.FileID)
if err != nil {
return false, fmt.Errorf("converting file ID: %w", err)
}
fileID := file.ID(fileIDInt)
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.Resolver.sceneService.AssignFile(ctx, sceneID, fileID)
}); err != nil {
return false, fmt.Errorf("assigning file to scene: %w", err)
}
return true, nil
}
func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput) (*models.Scene, error) {
srcIDs, err := stringslice.StringSliceToIntSlice(input.Source)
if err != nil {
return nil, fmt.Errorf("converting source IDs: %w", err)
}
destID, err := strconv.Atoi(input.Destination)
if err != nil {
return nil, fmt.Errorf("converting destination ID %s: %w", input.Destination, err)
}
var values *models.ScenePartial
if input.Values != nil {
translator := changesetTranslator{
inputMap: getNamedUpdateInputMap(ctx, "input.values"),
}
values, err = scenePartialFromInput(*input.Values, translator)
if err != nil {
return nil, err
}
} else {
v := models.NewScenePartial()
values = &v
}
var coverImageData []byte
if input.Values.CoverImage != nil && *input.Values.CoverImage != "" {
var err error
coverImageData, err = utils.ProcessImageInput(ctx, *input.Values.CoverImage)
if err != nil {
return nil, err
}
}
var ret *models.Scene
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.Resolver.sceneService.Merge(ctx, srcIDs, destID, *values); err != nil {
return err
}
ret, err = r.Resolver.repository.Scene.Find(ctx, destID)
if err == nil && ret != nil {
err = r.sceneUpdateCoverImage(ctx, ret, coverImageData)
}
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) getSceneMarker(ctx context.Context, id int) (ret *models.SceneMarker, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.Find(ctx, id)
@ -679,6 +858,42 @@ func (r *mutationResolver) changeMarker(ctx context.Context, changeType int, cha
return sceneMarker, nil
}
func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, resumeTime *float64, playDuration *float64) (ret bool, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.SaveActivity(ctx, sceneID, resumeTime, playDuration)
return err
}); err != nil {
return false, err
}
return ret, nil
}
func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id string) (ret int, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return 0, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.IncrementWatchCount(ctx, sceneID)
return err
}); err != nil {
return 0, err
}
return ret, nil
}
func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (ret int, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {

View file

@ -51,7 +51,7 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
}
var res *string
err = r.withTxn(ctx, func(ctx context.Context) error {
err = r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
scene, err := qb.Find(ctx, id)
if err != nil {
@ -82,7 +82,7 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
}
var res *string
err = r.withTxn(ctx, func(ctx context.Context) error {
err = r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
performer, err := qb.Find(ctx, id)
if err != nil {

View file

@ -58,11 +58,18 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateI
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.Rating100 != nil {
newStudio.Rating = sql.NullInt64{
Int64: int64(*input.Rating100),
Valid: true,
}
} else if input.Rating != nil {
newStudio.Rating = sql.NullInt64{
Int64: int64(models.Rating5To100(*input.Rating)),
Valid: true,
}
}
if input.Details != nil {
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
}
@ -150,7 +157,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI
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")
updatedStudio.Rating = translator.ratingConversion(input.Rating, input.Rating100)
updatedStudio.IgnoreAutoTag = input.IgnoreAutoTag
// Start the transaction and save the studio

View file

@ -142,6 +142,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
showStudioAsText := config.GetShowStudioAsText()
css := config.GetCSS()
cssEnabled := config.GetCSSEnabled()
javascript := config.GetJavascript()
javascriptEnabled := config.GetJavascriptEnabled()
customLocales := config.GetCustomLocales()
customLocalesEnabled := config.GetCustomLocalesEnabled()
language := config.GetLanguage()
@ -166,6 +168,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
ContinuePlaylistDefault: &continuePlaylistDefault,
CSS: &css,
CSSEnabled: &cssEnabled,
Javascript: &javascript,
JavascriptEnabled: &javascriptEnabled,
CustomLocales: &customLocales,
CustomLocalesEnabled: &customLocalesEnabled,
Language: &language,

View file

@ -13,7 +13,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Gallery.Find(ctx, idInt)
return err
}); err != nil {
@ -24,7 +24,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
}
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *FindGalleriesResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
galleries, total, err := r.repository.Gallery.Query(ctx, galleryFilter, filter)
if err != nil {
return err

View file

@ -12,7 +12,7 @@ import (
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
var image *models.Image
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
var err error
@ -47,7 +47,7 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
}
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *FindImagesResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
fields := graphql.CollectAllFields(ctx)

View file

@ -13,7 +13,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.Find(ctx, idInt)
return err
}); err != nil {
@ -24,7 +24,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
}
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *FindMoviesResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
movies, total, err := r.repository.Movie.Query(ctx, movieFilter, filter)
if err != nil {
return err
@ -44,7 +44,7 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi
}
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Movie, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.All(ctx)
return err
}); err != nil {

View file

@ -13,7 +13,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Performer.Find(ctx, idInt)
return err
}); err != nil {
@ -24,7 +24,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
}
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *FindPerformersResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
performers, total, err := r.repository.Performer.Query(ctx, performerFilter, filter)
if err != nil {
return err
@ -43,7 +43,7 @@ func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *mod
}
func (r *queryResolver) AllPerformers(ctx context.Context) (ret []*models.Performer, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Performer.All(ctx)
return err
}); err != nil {

View file

@ -13,7 +13,7 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SavedFilter.Find(ctx, idInt)
return err
}); err != nil {
@ -23,7 +23,7 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo
}
func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.FilterMode) (ret []*models.SavedFilter, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
if mode != nil {
ret, err = r.repository.SavedFilter.FindByMode(ctx, *mode)
} else {
@ -37,7 +37,7 @@ func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.Filte
}
func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
return err
}); err != nil {

View file

@ -12,7 +12,7 @@ import (
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
var scene *models.Scene
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
var err error
if id != nil {
@ -43,7 +43,7 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str
func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInput) (*models.Scene, error) {
var scene *models.Scene
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
if input.Checksum != nil {
scenes, err := qb.FindByChecksum(ctx, *input.Checksum)
@ -74,7 +74,7 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInpu
}
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var scenes []*models.Scene
var err error
@ -135,7 +135,7 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
}
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneFilter := &models.SceneFilterType{}
@ -192,7 +192,7 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config manager.SceneParserInput) (ret *SceneParserResultType, err error) {
parser := manager.NewSceneFilenameParser(filter, config)
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
result, count, err := parser.Parse(ctx, manager.SceneFilenameParserRepository{
Scene: r.repository.Scene,
Performer: r.repository.Performer,
@ -223,7 +223,7 @@ func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int)
if distance != nil {
dist = *distance
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.FindDuplicates(ctx, dist)
return err
}); err != nil {

View file

@ -7,7 +7,7 @@ import (
)
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (ret *FindSceneMarkersResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneMarkers, total, err := r.repository.SceneMarker.Query(ctx, sceneMarkerFilter, filter)
if err != nil {
return err

View file

@ -13,7 +13,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Studio.Find(ctx, idInt)
return err
@ -25,7 +25,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
}
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (ret *FindStudiosResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
studios, total, err := r.repository.Studio.Query(ctx, studioFilter, filter)
if err != nil {
return err
@ -45,7 +45,7 @@ func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.St
}
func (r *queryResolver) AllStudios(ctx context.Context) (ret []*models.Studio, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.All(ctx)
return err
}); err != nil {

View file

@ -13,7 +13,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.Find(ctx, idInt)
return err
}); err != nil {
@ -24,7 +24,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
}
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType) (ret *FindTagsResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
tags, total, err := r.repository.Tag.Query(ctx, tagFilter, filter)
if err != nil {
return err
@ -44,7 +44,7 @@ func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilte
}
func (r *queryResolver) AllTags(ctx context.Context) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.All(ctx)
return err
}); err != nil {

View file

@ -14,7 +14,7 @@ import (
func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*manager.SceneStreamEndpoint, error) {
// find the scene
var scene *models.Scene
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
idInt, _ := strconv.Atoi(*id)
var err error
scene, err = r.repository.Scene.Find(ctx, idInt)

View file

@ -0,0 +1,35 @@
package api
import (
"net/http"
"strings"
"github.com/go-chi/chi"
"github.com/stashapp/stash/internal/manager/config"
)
type customRoutes struct {
servedFolders config.URLMap
}
func (rs customRoutes) Routes() chi.Router {
r := chi.NewRouter()
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.Replace(r.URL.Path, "/custom", "", 1)
// http.FileServer redirects to / if the path ends with index.html
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/index.html")
// map the path to the applicable filesystem location
var dir string
r.URL.Path, dir = rs.servedFolders.GetFilesystemLocation(r.URL.Path)
if dir != "" {
http.FileServer(http.Dir(dir)).ServeHTTP(w, r)
} else {
http.NotFound(w, r)
}
})
return r
}

View file

@ -143,7 +143,7 @@ func (rs imageRoutes) ImageCtx(next http.Handler) http.Handler {
imageID, _ := strconv.Atoi(imageIdentifierQueryParam)
var image *models.Image
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
qb := rs.imageFinder
if imageID == 0 {
images, _ := qb.FindByChecksum(ctx, imageIdentifierQueryParam)

View file

@ -41,7 +41,7 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
image, _ = rs.movieFinder.GetFrontImage(ctx, movie.ID)
return nil
})
@ -67,7 +67,7 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
image, _ = rs.movieFinder.GetBackImage(ctx, movie.ID)
return nil
})
@ -97,7 +97,7 @@ func (rs movieRoutes) MovieCtx(next http.Handler) http.Handler {
}
var movie *models.Movie
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
movie, _ = rs.movieFinder.Find(ctx, movieID)
return nil
})

View file

@ -41,7 +41,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
var image []byte
if defaultParam != "true" {
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
image, _ = rs.performerFinder.GetImage(ctx, performer.ID)
return nil
})
@ -54,7 +54,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
}
if len(image) == 0 || defaultParam == "true" {
image, _ = getRandomPerformerImageUsingName(performer.Name.String, performer.Gender.String, config.GetInstance().GetCustomPerformerImageLocation())
image, _ = getRandomPerformerImageUsingName(performer.Name, performer.Gender, config.GetInstance().GetCustomPerformerImageLocation())
}
if err := utils.ServeImage(image, w, r); err != nil {
@ -71,7 +71,7 @@ func (rs performerRoutes) PerformerCtx(next http.Handler) http.Handler {
}
var performer *models.Performer
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
performer, err = rs.performerFinder.Find(ctx, performerID)
return err

View file

@ -264,7 +264,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce
}
var title string
if err := txn.WithTxn(ctx, rs.txnManager, func(ctx context.Context) error {
if err := txn.WithReadTxn(ctx, rs.txnManager, func(ctx context.Context) error {
qb := rs.tagFinder
primaryTag, err := qb.Find(ctx, marker.PrimaryTagID)
if err != nil {
@ -293,7 +293,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce
func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
var sceneMarkers []*models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarkers, err = rs.sceneMarkerFinder.FindBySceneID(ctx, scene.ID)
return err
@ -349,7 +349,7 @@ func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang strin
s := r.Context().Value(sceneKey).(*models.Scene)
var captions []*models.VideoCaption
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
primaryFile := s.Files.Primary()
if primaryFile == nil {
@ -377,14 +377,14 @@ func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang strin
sub, err := video.ReadSubs(caption.Path(s.Path))
if err != nil {
logger.Warnf("error while reading subs: %v", err)
http.Error(w, readTxnErr.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var b bytes.Buffer
err = sub.WriteToWebVTT(&b)
if err != nil {
http.Error(w, readTxnErr.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -423,7 +423,7 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request)
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@ -450,7 +450,7 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@ -487,7 +487,7 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@ -528,7 +528,7 @@ func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler {
sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam)
var scene *models.Scene
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
qb := rs.sceneFinder
if sceneID == 0 {
var scenes []*models.Scene

View file

@ -41,7 +41,7 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
var image []byte
if defaultParam != "true" {
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
image, _ = rs.studioFinder.GetImage(ctx, studio.ID)
return nil
})
@ -71,7 +71,7 @@ func (rs studioRoutes) StudioCtx(next http.Handler) http.Handler {
}
var studio *models.Studio
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
studio, err = rs.studioFinder.Find(ctx, studioID)
return err

View file

@ -41,7 +41,7 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) {
var image []byte
if defaultParam != "true" {
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
image, _ = rs.tagFinder.GetImage(ctx, tag.ID)
return nil
})
@ -71,7 +71,7 @@ func (rs tagRoutes) TagCtx(next http.Handler) http.Handler {
}
var tag *models.Tag
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
tag, err = rs.tagFinder.Find(ctx, tagID)
return err

View file

@ -181,6 +181,21 @@ func Start() error {
http.ServeFile(w, r, fn)
})
r.HandleFunc("/javascript", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript")
if !c.GetJavascriptEnabled() {
return
}
// search for custom.js in current directory, then $HOME/.stash
fn := c.GetJavascriptPath()
exists, _ := fsutil.FileExists(fn)
if !exists {
return
}
http.ServeFile(w, r, fn)
})
r.HandleFunc("/customlocales", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if c.GetCustomLocalesEnabled() {
@ -216,18 +231,9 @@ func Start() error {
// Serve static folders
customServedFolders := c.GetCustomServedFolders()
if customServedFolders != nil {
r.HandleFunc("/custom/*", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.Replace(r.URL.Path, "/custom", "", 1)
// map the path to the applicable filesystem location
var dir string
r.URL.Path, dir = customServedFolders.GetFilesystemLocation(r.URL.Path)
if dir != "" {
http.FileServer(http.Dir(dir)).ServeHTTP(w, r)
} else {
http.NotFound(w, r)
}
})
r.Mount("/custom", customRoutes{
servedFolders: customServedFolders,
}.Routes())
}
customUILocation := c.GetCustomUILocation()

View file

@ -1,8 +1,9 @@
package urlbuilders
import (
"github.com/stashapp/stash/pkg/models"
"strconv"
"github.com/stashapp/stash/pkg/models"
)
type PerformerURLBuilder struct {
@ -15,7 +16,7 @@ func NewPerformerURLBuilder(baseURL string, performer *models.Performer) Perform
return PerformerURLBuilder{
BaseURL: baseURL,
PerformerID: strconv.Itoa(performer.ID),
UpdatedAt: strconv.FormatInt(performer.UpdatedAt.Timestamp.Unix(), 10),
UpdatedAt: strconv.FormatInt(performer.UpdatedAt.Unix(), 10),
}
}

View file

@ -22,14 +22,14 @@ func TestGalleryPerformers(t *testing.T) {
const performerID = 2
performer := models.Performer{
ID: performerID,
Name: models.NullString(performerName),
Name: performerName,
}
const reversedPerformerName = "name performer"
const reversedPerformerID = 3
reversedPerformer := models.Performer{
ID: reversedPerformerID,
Name: models.NullString(reversedPerformerName),
Name: reversedPerformerName,
}
testTables := generateTestTable(performerName, galleryExt)

View file

@ -19,14 +19,14 @@ func TestImagePerformers(t *testing.T) {
const performerID = 2
performer := models.Performer{
ID: performerID,
Name: models.NullString(performerName),
Name: performerName,
}
const reversedPerformerName = "name performer"
const reversedPerformerID = 3
reversedPerformer := models.Performer{
ID: reversedPerformerID,
Name: models.NullString(reversedPerformerName),
Name: reversedPerformerName,
}
testTables := generateTestTable(performerName, imageExt)

View file

@ -87,11 +87,10 @@ func createPerformer(ctx context.Context, pqb models.PerformerWriter) error {
// create the performer
performer := models.Performer{
Checksum: testName,
Name: sql.NullString{Valid: true, String: testName},
Favorite: sql.NullBool{Valid: true, Bool: false},
Name: testName,
}
_, err := pqb.Create(ctx, performer)
err := pqb.Create(ctx, &performer)
if err != nil {
return err
}
@ -480,6 +479,10 @@ func withTxn(f func(ctx context.Context) error) error {
return txn.WithTxn(context.TODO(), db, f)
}
func withDB(f func(ctx context.Context) error) error {
return txn.WithDatabase(context.TODO(), db, f)
}
func populateDB() error {
if err := withTxn(func(ctx context.Context) error {
err := createPerformer(ctx, r.Performer)
@ -539,9 +542,13 @@ func TestParsePerformerScenes(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, p := range performers {
if err := withTxn(func(ctx context.Context) error {
return PerformerScenes(ctx, p, nil, r.Scene, nil)
if err := withDB(func(ctx context.Context) error {
return tagger.PerformerScenes(ctx, p, nil, r.Scene)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -586,14 +593,18 @@ func TestParseStudioScenes(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range studios {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Studio.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return StudioScenes(ctx, s, nil, aliases, r.Scene, nil)
return tagger.StudioScenes(ctx, s, nil, aliases, r.Scene)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -642,14 +653,18 @@ func TestParseTagScenes(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range tags {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Tag.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return TagScenes(ctx, s, nil, aliases, r.Scene, nil)
return tagger.TagScenes(ctx, s, nil, aliases, r.Scene)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -694,9 +709,13 @@ func TestParsePerformerImages(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, p := range performers {
if err := withTxn(func(ctx context.Context) error {
return PerformerImages(ctx, p, nil, r.Image, nil)
if err := withDB(func(ctx context.Context) error {
return tagger.PerformerImages(ctx, p, nil, r.Image)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -742,14 +761,18 @@ func TestParseStudioImages(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range studios {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Studio.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return StudioImages(ctx, s, nil, aliases, r.Image, nil)
return tagger.StudioImages(ctx, s, nil, aliases, r.Image)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -798,14 +821,18 @@ func TestParseTagImages(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range tags {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Tag.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return TagImages(ctx, s, nil, aliases, r.Image, nil)
return tagger.TagImages(ctx, s, nil, aliases, r.Image)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -851,9 +878,13 @@ func TestParsePerformerGalleries(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, p := range performers {
if err := withTxn(func(ctx context.Context) error {
return PerformerGalleries(ctx, p, nil, r.Gallery, nil)
if err := withDB(func(ctx context.Context) error {
return tagger.PerformerGalleries(ctx, p, nil, r.Gallery)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -899,14 +930,18 @@ func TestParseStudioGalleries(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range studios {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Studio.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return StudioGalleries(ctx, s, nil, aliases, r.Gallery, nil)
return tagger.StudioGalleries(ctx, s, nil, aliases, r.Gallery)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -955,14 +990,18 @@ func TestParseTagGalleries(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range tags {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Tag.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return TagGalleries(ctx, s, nil, aliases, r.Gallery, nil)
return tagger.TagGalleries(ctx, s, nil, aliases, r.Gallery)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}

View file

@ -9,6 +9,7 @@ import (
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
"github.com/stashapp/stash/pkg/txn"
)
type SceneQueryPerformerUpdater interface {
@ -33,14 +34,14 @@ func getPerformerTagger(p *models.Performer, cache *match.Cache) tagger {
return tagger{
ID: p.ID,
Type: "performer",
Name: p.Name.String,
Name: p.Name,
cache: cache,
}
}
// PerformerScenes searches for scenes whose path matches the provided performer name and tags the scene with the performer.
func PerformerScenes(ctx context.Context, p *models.Performer, paths []string, rw SceneQueryPerformerUpdater, cache *match.Cache) error {
t := getPerformerTagger(p, cache)
func (tagger *Tagger) PerformerScenes(ctx context.Context, p *models.Performer, paths []string, rw SceneQueryPerformerUpdater) error {
t := getPerformerTagger(p, tagger.Cache)
return t.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
@ -52,7 +53,9 @@ func PerformerScenes(ctx context.Context, p *models.Performer, paths []string, r
return false, nil
}
if err := scene.AddPerformer(ctx, rw, o, p.ID); err != nil {
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
return scene.AddPerformer(ctx, rw, o, p.ID)
}); err != nil {
return false, err
}
@ -61,8 +64,8 @@ func PerformerScenes(ctx context.Context, p *models.Performer, paths []string, r
}
// PerformerImages searches for images whose path matches the provided performer name and tags the image with the performer.
func PerformerImages(ctx context.Context, p *models.Performer, paths []string, rw ImageQueryPerformerUpdater, cache *match.Cache) error {
t := getPerformerTagger(p, cache)
func (tagger *Tagger) PerformerImages(ctx context.Context, p *models.Performer, paths []string, rw ImageQueryPerformerUpdater) error {
t := getPerformerTagger(p, tagger.Cache)
return t.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) {
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
@ -74,7 +77,9 @@ func PerformerImages(ctx context.Context, p *models.Performer, paths []string, r
return false, nil
}
if err := image.AddPerformer(ctx, rw, o, p.ID); err != nil {
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
return image.AddPerformer(ctx, rw, o, p.ID)
}); err != nil {
return false, err
}
@ -83,8 +88,8 @@ func PerformerImages(ctx context.Context, p *models.Performer, paths []string, r
}
// PerformerGalleries searches for galleries whose path matches the provided performer name and tags the gallery with the performer.
func PerformerGalleries(ctx context.Context, p *models.Performer, paths []string, rw GalleryQueryPerformerUpdater, cache *match.Cache) error {
t := getPerformerTagger(p, cache)
func (tagger *Tagger) PerformerGalleries(ctx context.Context, p *models.Performer, paths []string, rw GalleryQueryPerformerUpdater) error {
t := getPerformerTagger(p, tagger.Cache)
return t.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
@ -96,7 +101,9 @@ func PerformerGalleries(ctx context.Context, p *models.Performer, paths []string
return false, nil
}
if err := gallery.AddPerformer(ctx, rw, o, p.ID); err != nil {
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
return gallery.AddPerformer(ctx, rw, o, p.ID)
}); err != nil {
return false, err
}

View file

@ -9,6 +9,7 @@ import (
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/scene"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestPerformerScenes(t *testing.T) {
@ -60,11 +61,13 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
performer := models.Performer{
ID: performerID,
Name: models.NullString(performerName),
Name: performerName,
}
organized := false
perPage := models.PerPageAll
perPage := 1000
sort := "id"
direction := models.SortDirectionEnumAsc
expectedSceneFilter := &models.SceneFilterType{
Organized: &organized,
@ -75,15 +78,17 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
}
expectedFindFilter := &models.FindFilterType{
PerPage: &perPage,
PerPage: &perPage,
Sort: &sort,
Direction: &direction,
}
mockSceneReader.On("Query", testCtx, scene.QueryOptions(expectedSceneFilter, expectedFindFilter, false)).
mockSceneReader.On("Query", mock.Anything, scene.QueryOptions(expectedSceneFilter, expectedFindFilter, false)).
Return(mocks.SceneQueryResult(scenes, len(scenes)), nil).Once()
for i := range matchingPaths {
sceneID := i + 1
mockSceneReader.On("UpdatePartial", testCtx, sceneID, models.ScenePartial{
mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, models.ScenePartial{
PerformerIDs: &models.UpdateIDs{
IDs: []int{performerID},
Mode: models.RelationshipUpdateModeAdd,
@ -91,7 +96,11 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
}).Return(nil, nil).Once()
}
err := PerformerScenes(testCtx, &performer, nil, mockSceneReader, nil)
tagger := Tagger{
TxnManager: &mocks.TxnManager{},
}
err := tagger.PerformerScenes(testCtx, &performer, nil, mockSceneReader)
assert := assert.New(t)
@ -140,11 +149,13 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) {
performer := models.Performer{
ID: performerID,
Name: models.NullString(performerName),
Name: performerName,
}
organized := false
perPage := models.PerPageAll
perPage := 1000
sort := "id"
direction := models.SortDirectionEnumAsc
expectedImageFilter := &models.ImageFilterType{
Organized: &organized,
@ -155,15 +166,17 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) {
}
expectedFindFilter := &models.FindFilterType{
PerPage: &perPage,
PerPage: &perPage,
Sort: &sort,
Direction: &direction,
}
mockImageReader.On("Query", testCtx, image.QueryOptions(expectedImageFilter, expectedFindFilter, false)).
mockImageReader.On("Query", mock.Anything, image.QueryOptions(expectedImageFilter, expectedFindFilter, false)).
Return(mocks.ImageQueryResult(images, len(images)), nil).Once()
for i := range matchingPaths {
imageID := i + 1
mockImageReader.On("UpdatePartial", testCtx, imageID, models.ImagePartial{
mockImageReader.On("UpdatePartial", mock.Anything, imageID, models.ImagePartial{
PerformerIDs: &models.UpdateIDs{
IDs: []int{performerID},
Mode: models.RelationshipUpdateModeAdd,
@ -171,7 +184,11 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) {
}).Return(nil, nil).Once()
}
err := PerformerImages(testCtx, &performer, nil, mockImageReader, nil)
tagger := Tagger{
TxnManager: &mocks.TxnManager{},
}
err := tagger.PerformerImages(testCtx, &performer, nil, mockImageReader)
assert := assert.New(t)
@ -221,11 +238,13 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) {
performer := models.Performer{
ID: performerID,
Name: models.NullString(performerName),
Name: performerName,
}
organized := false
perPage := models.PerPageAll
perPage := 1000
sort := "id"
direction := models.SortDirectionEnumAsc
expectedGalleryFilter := &models.GalleryFilterType{
Organized: &organized,
@ -236,14 +255,16 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) {
}
expectedFindFilter := &models.FindFilterType{
PerPage: &perPage,
PerPage: &perPage,
Sort: &sort,
Direction: &direction,
}
mockGalleryReader.On("Query", testCtx, expectedGalleryFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
mockGalleryReader.On("Query", mock.Anything, expectedGalleryFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
for i := range matchingPaths {
galleryID := i + 1
mockGalleryReader.On("UpdatePartial", testCtx, galleryID, models.GalleryPartial{
mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, models.GalleryPartial{
PerformerIDs: &models.UpdateIDs{
IDs: []int{performerID},
Mode: models.RelationshipUpdateModeAdd,
@ -251,7 +272,11 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) {
}).Return(nil, nil).Once()
}
err := PerformerGalleries(testCtx, &performer, nil, mockGalleryReader, nil)
tagger := Tagger{
TxnManager: &mocks.TxnManager{},
}
err := tagger.PerformerGalleries(testCtx, &performer, nil, mockGalleryReader)
assert := assert.New(t)

View file

@ -152,14 +152,14 @@ func TestScenePerformers(t *testing.T) {
const performerID = 2
performer := models.Performer{
ID: performerID,
Name: models.NullString(performerName),
Name: performerName,
}
const reversedPerformerName = "name performer"
const reversedPerformerID = 3
reversedPerformer := models.Performer{
ID: reversedPerformerID,
Name: models.NullString(reversedPerformerName),
Name: reversedPerformerName,
}
testTables := generateTestTable(performerName, sceneExt)

View file

@ -8,8 +8,12 @@ import (
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/txn"
)
// the following functions aren't used in Tagger because they assume
// use within a transaction
func addSceneStudio(ctx context.Context, sceneWriter scene.PartialUpdater, o *models.Scene, studioID int) (bool, error) {
// don't set if already set
if o.StudioID != nil {
@ -86,12 +90,28 @@ type SceneFinderUpdater interface {
}
// StudioScenes searches for scenes whose path matches the provided studio name and tags the scene with the studio, if studio is not already set on the scene.
func StudioScenes(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw SceneFinderUpdater, cache *match.Cache) error {
t := getStudioTagger(p, aliases, cache)
func (tagger *Tagger) StudioScenes(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw SceneFinderUpdater) error {
t := getStudioTagger(p, aliases, tagger.Cache)
for _, tt := range t {
if err := tt.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
return addSceneStudio(ctx, rw, o, p.ID)
// don't set if already set
if o.StudioID != nil {
return false, nil
}
// set the studio id
scenePartial := models.ScenePartial{
StudioID: models.NewOptionalInt(p.ID),
}
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
_, err := rw.UpdatePartial(ctx, o.ID, scenePartial)
return err
}); err != nil {
return false, err
}
return true, nil
}); err != nil {
return err
}
@ -107,12 +127,28 @@ type ImageFinderUpdater interface {
}
// StudioImages searches for images whose path matches the provided studio name and tags the image with the studio, if studio is not already set on the image.
func StudioImages(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw ImageFinderUpdater, cache *match.Cache) error {
t := getStudioTagger(p, aliases, cache)
func (tagger *Tagger) StudioImages(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw ImageFinderUpdater) error {
t := getStudioTagger(p, aliases, tagger.Cache)
for _, tt := range t {
if err := tt.tagImages(ctx, paths, rw, func(i *models.Image) (bool, error) {
return addImageStudio(ctx, rw, i, p.ID)
// don't set if already set
if i.StudioID != nil {
return false, nil
}
// set the studio id
imagePartial := models.ImagePartial{
StudioID: models.NewOptionalInt(p.ID),
}
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
_, err := rw.UpdatePartial(ctx, i.ID, imagePartial)
return err
}); err != nil {
return false, err
}
return true, nil
}); err != nil {
return err
}
@ -128,12 +164,28 @@ type GalleryFinderUpdater interface {
}
// StudioGalleries searches for galleries whose path matches the provided studio name and tags the gallery with the studio, if studio is not already set on the gallery.
func StudioGalleries(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw GalleryFinderUpdater, cache *match.Cache) error {
t := getStudioTagger(p, aliases, cache)
func (tagger *Tagger) StudioGalleries(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw GalleryFinderUpdater) error {
t := getStudioTagger(p, aliases, tagger.Cache)
for _, tt := range t {
if err := tt.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
return addGalleryStudio(ctx, rw, o, p.ID)
// don't set if already set
if o.StudioID != nil {
return false, nil
}
// set the studio id
galleryPartial := models.GalleryPartial{
StudioID: models.NewOptionalInt(p.ID),
}
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
_, err := rw.UpdatePartial(ctx, o.ID, galleryPartial)
return err
}); err != nil {
return false, err
}
return true, nil
}); err != nil {
return err
}

View file

@ -9,6 +9,7 @@ import (
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/scene"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type testStudioCase struct {
@ -110,7 +111,9 @@ func testStudioScenes(t *testing.T, tc testStudioCase) {
}
organized := false
perPage := models.PerPageAll
perPage := 1000
sort := "id"
direction := models.SortDirectionEnumAsc
expectedSceneFilter := &models.SceneFilterType{
Organized: &organized,
@ -121,7 +124,9 @@ func testStudioScenes(t *testing.T, tc testStudioCase) {
}
expectedFindFilter := &models.FindFilterType{
PerPage: &perPage,
PerPage: &perPage,
Sort: &sort,
Direction: &direction,
}
// if alias provided, then don't find by name
@ -140,19 +145,23 @@ func testStudioScenes(t *testing.T, tc testStudioCase) {
},
}
mockSceneReader.On("Query", testCtx, scene.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
mockSceneReader.On("Query", mock.Anything, scene.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
Return(mocks.SceneQueryResult(scenes, len(scenes)), nil).Once()
}
for i := range matchingPaths {
sceneID := i + 1
expectedStudioID := studioID
mockSceneReader.On("UpdatePartial", testCtx, sceneID, models.ScenePartial{
mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, models.ScenePartial{
StudioID: models.NewOptionalInt(expectedStudioID),
}).Return(nil, nil).Once()
}
err := StudioScenes(testCtx, &studio, nil, aliases, mockSceneReader, nil)
tagger := Tagger{
TxnManager: &mocks.TxnManager{},
}
err := tagger.StudioScenes(testCtx, &studio, nil, aliases, mockSceneReader)
assert := assert.New(t)
@ -201,7 +210,9 @@ func testStudioImages(t *testing.T, tc testStudioCase) {
}
organized := false
perPage := models.PerPageAll
perPage := 1000
sort := "id"
direction := models.SortDirectionEnumAsc
expectedImageFilter := &models.ImageFilterType{
Organized: &organized,
@ -212,11 +223,13 @@ func testStudioImages(t *testing.T, tc testStudioCase) {
}
expectedFindFilter := &models.FindFilterType{
PerPage: &perPage,
PerPage: &perPage,
Sort: &sort,
Direction: &direction,
}
// if alias provided, then don't find by name
onNameQuery := mockImageReader.On("Query", testCtx, image.QueryOptions(expectedImageFilter, expectedFindFilter, false))
onNameQuery := mockImageReader.On("Query", mock.Anything, image.QueryOptions(expectedImageFilter, expectedFindFilter, false))
if aliasName == "" {
onNameQuery.Return(mocks.ImageQueryResult(images, len(images)), nil).Once()
} else {
@ -230,19 +243,23 @@ func testStudioImages(t *testing.T, tc testStudioCase) {
},
}
mockImageReader.On("Query", testCtx, image.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
mockImageReader.On("Query", mock.Anything, image.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
Return(mocks.ImageQueryResult(images, len(images)), nil).Once()
}
for i := range matchingPaths {
imageID := i + 1
expectedStudioID := studioID
mockImageReader.On("UpdatePartial", testCtx, imageID, models.ImagePartial{
mockImageReader.On("UpdatePartial", mock.Anything, imageID, models.ImagePartial{
StudioID: models.NewOptionalInt(expectedStudioID),
}).Return(nil, nil).Once()
}
err := StudioImages(testCtx, &studio, nil, aliases, mockImageReader, nil)
tagger := Tagger{
TxnManager: &mocks.TxnManager{},
}
err := tagger.StudioImages(testCtx, &studio, nil, aliases, mockImageReader)
assert := assert.New(t)
@ -291,7 +308,9 @@ func testStudioGalleries(t *testing.T, tc testStudioCase) {
}
organized := false
perPage := models.PerPageAll
perPage := 1000
sort := "id"
direction := models.SortDirectionEnumAsc
expectedGalleryFilter := &models.GalleryFilterType{
Organized: &organized,
@ -302,11 +321,13 @@ func testStudioGalleries(t *testing.T, tc testStudioCase) {
}
expectedFindFilter := &models.FindFilterType{
PerPage: &perPage,
PerPage: &perPage,
Sort: &sort,
Direction: &direction,
}
// if alias provided, then don't find by name
onNameQuery := mockGalleryReader.On("Query", testCtx, expectedGalleryFilter, expectedFindFilter)
onNameQuery := mockGalleryReader.On("Query", mock.Anything, expectedGalleryFilter, expectedFindFilter)
if aliasName == "" {
onNameQuery.Return(galleries, len(galleries), nil).Once()
} else {
@ -320,18 +341,22 @@ func testStudioGalleries(t *testing.T, tc testStudioCase) {
},
}
mockGalleryReader.On("Query", testCtx, expectedAliasFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
mockGalleryReader.On("Query", mock.Anything, expectedAliasFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
}
for i := range matchingPaths {
galleryID := i + 1
expectedStudioID := studioID
mockGalleryReader.On("UpdatePartial", testCtx, galleryID, models.GalleryPartial{
mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, models.GalleryPartial{
StudioID: models.NewOptionalInt(expectedStudioID),
}).Return(nil, nil).Once()
}
err := StudioGalleries(testCtx, &studio, nil, aliases, mockGalleryReader, nil)
tagger := Tagger{
TxnManager: &mocks.TxnManager{},
}
err := tagger.StudioGalleries(testCtx, &studio, nil, aliases, mockGalleryReader)
assert := assert.New(t)

View file

@ -9,6 +9,7 @@ import (
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
"github.com/stashapp/stash/pkg/txn"
)
type SceneQueryTagUpdater interface {
@ -50,8 +51,8 @@ func getTagTaggers(p *models.Tag, aliases []string, cache *match.Cache) []tagger
}
// TagScenes searches for scenes whose path matches the provided tag name and tags the scene with the tag.
func TagScenes(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw SceneQueryTagUpdater, cache *match.Cache) error {
t := getTagTaggers(p, aliases, cache)
func (tagger *Tagger) TagScenes(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw SceneQueryTagUpdater) error {
t := getTagTaggers(p, aliases, tagger.Cache)
for _, tt := range t {
if err := tt.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
@ -64,7 +65,9 @@ func TagScenes(ctx context.Context, p *models.Tag, paths []string, aliases []str
return false, nil
}
if err := scene.AddTag(ctx, rw, o, p.ID); err != nil {
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
return scene.AddTag(ctx, rw, o, p.ID)
}); err != nil {
return false, err
}
@ -77,8 +80,8 @@ func TagScenes(ctx context.Context, p *models.Tag, paths []string, aliases []str
}
// TagImages searches for images whose path matches the provided tag name and tags the image with the tag.
func TagImages(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw ImageQueryTagUpdater, cache *match.Cache) error {
t := getTagTaggers(p, aliases, cache)
func (tagger *Tagger) TagImages(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw ImageQueryTagUpdater) error {
t := getTagTaggers(p, aliases, tagger.Cache)
for _, tt := range t {
if err := tt.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) {
@ -91,7 +94,9 @@ func TagImages(ctx context.Context, p *models.Tag, paths []string, aliases []str
return false, nil
}
if err := image.AddTag(ctx, rw, o, p.ID); err != nil {
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
return image.AddTag(ctx, rw, o, p.ID)
}); err != nil {
return false, err
}
@ -104,8 +109,8 @@ func TagImages(ctx context.Context, p *models.Tag, paths []string, aliases []str
}
// TagGalleries searches for galleries whose path matches the provided tag name and tags the gallery with the tag.
func TagGalleries(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw GalleryQueryTagUpdater, cache *match.Cache) error {
t := getTagTaggers(p, aliases, cache)
func (tagger *Tagger) TagGalleries(ctx context.Context, p *models.Tag, paths []string, aliases []string, rw GalleryQueryTagUpdater) error {
t := getTagTaggers(p, aliases, tagger.Cache)
for _, tt := range t {
if err := tt.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
@ -118,7 +123,9 @@ func TagGalleries(ctx context.Context, p *models.Tag, paths []string, aliases []
return false, nil
}
if err := gallery.AddTag(ctx, rw, o, p.ID); err != nil {
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
return gallery.AddTag(ctx, rw, o, p.ID)
}); err != nil {
return false, err
}

View file

@ -9,6 +9,7 @@ import (
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/scene"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type testTagCase struct {
@ -111,7 +112,9 @@ func testTagScenes(t *testing.T, tc testTagCase) {
}
organized := false
perPage := models.PerPageAll
perPage := 1000
sort := "id"
direction := models.SortDirectionEnumAsc
expectedSceneFilter := &models.SceneFilterType{
Organized: &organized,
@ -122,7 +125,9 @@ func testTagScenes(t *testing.T, tc testTagCase) {
}
expectedFindFilter := &models.FindFilterType{
PerPage: &perPage,
PerPage: &perPage,
Sort: &sort,
Direction: &direction,
}
// if alias provided, then don't find by name
@ -140,13 +145,13 @@ func testTagScenes(t *testing.T, tc testTagCase) {
},
}
mockSceneReader.On("Query", testCtx, scene.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
mockSceneReader.On("Query", mock.Anything, scene.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
Return(mocks.SceneQueryResult(scenes, len(scenes)), nil).Once()
}
for i := range matchingPaths {
sceneID := i + 1
mockSceneReader.On("UpdatePartial", testCtx, sceneID, models.ScenePartial{
mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, models.ScenePartial{
TagIDs: &models.UpdateIDs{
IDs: []int{tagID},
Mode: models.RelationshipUpdateModeAdd,
@ -154,7 +159,11 @@ func testTagScenes(t *testing.T, tc testTagCase) {
}).Return(nil, nil).Once()
}
err := TagScenes(testCtx, &tag, nil, aliases, mockSceneReader, nil)
tagger := Tagger{
TxnManager: &mocks.TxnManager{},
}
err := tagger.TagScenes(testCtx, &tag, nil, aliases, mockSceneReader)
assert := assert.New(t)
@ -204,7 +213,9 @@ func testTagImages(t *testing.T, tc testTagCase) {
}
organized := false
perPage := models.PerPageAll
perPage := 1000
sort := "id"
direction := models.SortDirectionEnumAsc
expectedImageFilter := &models.ImageFilterType{
Organized: &organized,
@ -215,7 +226,9 @@ func testTagImages(t *testing.T, tc testTagCase) {
}
expectedFindFilter := &models.FindFilterType{
PerPage: &perPage,
PerPage: &perPage,
Sort: &sort,
Direction: &direction,
}
// if alias provided, then don't find by name
@ -233,14 +246,14 @@ func testTagImages(t *testing.T, tc testTagCase) {
},
}
mockImageReader.On("Query", testCtx, image.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
mockImageReader.On("Query", mock.Anything, image.QueryOptions(expectedAliasFilter, expectedFindFilter, false)).
Return(mocks.ImageQueryResult(images, len(images)), nil).Once()
}
for i := range matchingPaths {
imageID := i + 1
mockImageReader.On("UpdatePartial", testCtx, imageID, models.ImagePartial{
mockImageReader.On("UpdatePartial", mock.Anything, imageID, models.ImagePartial{
TagIDs: &models.UpdateIDs{
IDs: []int{tagID},
Mode: models.RelationshipUpdateModeAdd,
@ -248,7 +261,11 @@ func testTagImages(t *testing.T, tc testTagCase) {
}).Return(nil, nil).Once()
}
err := TagImages(testCtx, &tag, nil, aliases, mockImageReader, nil)
tagger := Tagger{
TxnManager: &mocks.TxnManager{},
}
err := tagger.TagImages(testCtx, &tag, nil, aliases, mockImageReader)
assert := assert.New(t)
@ -299,7 +316,9 @@ func testTagGalleries(t *testing.T, tc testTagCase) {
}
organized := false
perPage := models.PerPageAll
perPage := 1000
sort := "id"
direction := models.SortDirectionEnumAsc
expectedGalleryFilter := &models.GalleryFilterType{
Organized: &organized,
@ -310,7 +329,9 @@ func testTagGalleries(t *testing.T, tc testTagCase) {
}
expectedFindFilter := &models.FindFilterType{
PerPage: &perPage,
PerPage: &perPage,
Sort: &sort,
Direction: &direction,
}
// if alias provided, then don't find by name
@ -328,13 +349,13 @@ func testTagGalleries(t *testing.T, tc testTagCase) {
},
}
mockGalleryReader.On("Query", testCtx, expectedAliasFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
mockGalleryReader.On("Query", mock.Anything, expectedAliasFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
}
for i := range matchingPaths {
galleryID := i + 1
mockGalleryReader.On("UpdatePartial", testCtx, galleryID, models.GalleryPartial{
mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, models.GalleryPartial{
TagIDs: &models.UpdateIDs{
IDs: []int{tagID},
Mode: models.RelationshipUpdateModeAdd,
@ -343,7 +364,11 @@ func testTagGalleries(t *testing.T, tc testTagCase) {
}
err := TagGalleries(testCtx, &tag, nil, aliases, mockGalleryReader, nil)
tagger := Tagger{
TxnManager: &mocks.TxnManager{},
}
err := tagger.TagGalleries(testCtx, &tag, nil, aliases, mockGalleryReader)
assert := assert.New(t)

View file

@ -23,8 +23,14 @@ import (
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/txn"
)
type Tagger struct {
TxnManager txn.Manager
Cache *match.Cache
}
type tagger struct {
ID int
Type string
@ -58,11 +64,11 @@ func (t *tagger) tagPerformers(ctx context.Context, performerReader match.Perfor
added, err := addFunc(t.ID, p.ID)
if err != nil {
return t.addError("performer", p.Name.String, err)
return t.addError("performer", p.Name, err)
}
if added {
t.addLog("performer", p.Name.String)
t.addLog("performer", p.Name)
}
}
@ -112,12 +118,7 @@ func (t *tagger) tagTags(ctx context.Context, tagReader match.TagAutoTagQueryer,
}
func (t *tagger) tagScenes(ctx context.Context, paths []string, sceneReader scene.Queryer, addFunc addSceneLinkFunc) error {
others, err := match.PathToScenes(ctx, t.Name, paths, sceneReader)
if err != nil {
return err
}
for _, p := range others {
return match.PathToScenesFn(ctx, t.Name, paths, sceneReader, func(ctx context.Context, p *models.Scene) error {
added, err := addFunc(p)
if err != nil {
@ -127,18 +128,13 @@ func (t *tagger) tagScenes(ctx context.Context, paths []string, sceneReader scen
if added {
t.addLog("scene", p.DisplayName())
}
}
return nil
return nil
})
}
func (t *tagger) tagImages(ctx context.Context, paths []string, imageReader image.Queryer, addFunc addImageLinkFunc) error {
others, err := match.PathToImages(ctx, t.Name, paths, imageReader)
if err != nil {
return err
}
for _, p := range others {
return match.PathToImagesFn(ctx, t.Name, paths, imageReader, func(ctx context.Context, p *models.Image) error {
added, err := addFunc(p)
if err != nil {
@ -148,18 +144,13 @@ func (t *tagger) tagImages(ctx context.Context, paths []string, imageReader imag
if added {
t.addLog("image", p.DisplayName())
}
}
return nil
return nil
})
}
func (t *tagger) tagGalleries(ctx context.Context, paths []string, galleryReader gallery.Queryer, addFunc addGalleryLinkFunc) error {
others, err := match.PathToGalleries(ctx, t.Name, paths, galleryReader)
if err != nil {
return err
}
for _, p := range others {
return match.PathToGalleriesFn(ctx, t.Name, paths, galleryReader, func(ctx context.Context, p *models.Gallery) error {
added, err := addFunc(p)
if err != nil {
@ -169,7 +160,7 @@ func (t *tagger) tagGalleries(ctx context.Context, paths []string, galleryReader
if added {
t.addLog("gallery", p.DisplayName())
}
}
return nil
return nil
})
}

View file

@ -360,7 +360,7 @@ func (me *contentDirectoryService) handleBrowseMetadata(obj object, host string)
} else {
var scene *models.Scene
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
scene, err = me.repository.SceneFinder.Find(ctx, sceneID)
if scene != nil {
err = scene.LoadPrimaryFile(ctx, me.repository.FileFinder)
@ -443,7 +443,7 @@ func getRootObjects() []interface{} {
func (me *contentDirectoryService) getVideos(sceneFilter *models.SceneFilterType, parentID string, host string) []interface{} {
var objs []interface{}
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
sort := "title"
findFilter := &models.FindFilterType{
PerPage: &pageSize,
@ -486,7 +486,7 @@ func (me *contentDirectoryService) getVideos(sceneFilter *models.SceneFilterType
func (me *contentDirectoryService) getPageVideos(sceneFilter *models.SceneFilterType, parentID string, page int, host string) []interface{} {
var objs []interface{}
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
pager := scenePager{
sceneFilter: sceneFilter,
parentID: parentID,
@ -527,7 +527,7 @@ func (me *contentDirectoryService) getAllScenes(host string) []interface{} {
func (me *contentDirectoryService) getStudios() []interface{} {
var objs []interface{}
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
studios, err := me.repository.StudioFinder.All(ctx)
if err != nil {
return err
@ -566,7 +566,7 @@ func (me *contentDirectoryService) getStudioScenes(paths []string, host string)
func (me *contentDirectoryService) getTags() []interface{} {
var objs []interface{}
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
tags, err := me.repository.TagFinder.All(ctx)
if err != nil {
return err
@ -605,14 +605,14 @@ func (me *contentDirectoryService) getTagScenes(paths []string, host string) []i
func (me *contentDirectoryService) getPerformers() []interface{} {
var objs []interface{}
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
performers, err := me.repository.PerformerFinder.All(ctx)
if err != nil {
return err
}
for _, s := range performers {
objs = append(objs, makeStorageFolder("performers/"+strconv.Itoa(s.ID), s.Name.String, "performers"))
objs = append(objs, makeStorageFolder("performers/"+strconv.Itoa(s.ID), s.Name, "performers"))
}
return nil
@ -644,7 +644,7 @@ func (me *contentDirectoryService) getPerformerScenes(paths []string, host strin
func (me *contentDirectoryService) getMovies() []interface{} {
var objs []interface{}
if err := txn.WithTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
movies, err := me.repository.MovieFinder.All(ctx)
if err != nil {
return err

View file

@ -439,7 +439,7 @@ func (me *Server) serveIcon(w http.ResponseWriter, r *http.Request) {
}
var scene *models.Scene
err := txn.WithTxn(r.Context(), me.txnManager, func(ctx context.Context) error {
err := txn.WithReadTxn(r.Context(), me.txnManager, func(ctx context.Context) error {
idInt, err := strconv.Atoi(sceneId)
if err != nil {
return nil
@ -579,7 +579,7 @@ func (me *Server) initMux(mux *http.ServeMux) {
mux.HandleFunc(resPath, func(w http.ResponseWriter, r *http.Request) {
sceneId := r.URL.Query().Get("scene")
var scene *models.Scene
err := txn.WithTxn(r.Context(), me.txnManager, func(ctx context.Context) error {
err := txn.WithReadTxn(r.Context(), me.txnManager, func(ctx context.Context) error {
sceneIdInt, err := strconv.Atoi(sceneId)
if err != nil {
return nil

View file

@ -280,6 +280,16 @@ func getScenePartial(scene *models.Scene, scraped *scraper.ScrapedScene, fieldOp
partial.URL = models.NewOptionalString(*scraped.URL)
}
}
if scraped.Director != nil && (scene.Director != *scraped.Director) {
if shouldSetSingleValueField(fieldOptions["director"], scene.Director != "") {
partial.Director = models.NewOptionalString(*scraped.Director)
}
}
if scraped.Code != nil && (scene.Code != *scraped.Code) {
if shouldSetSingleValueField(fieldOptions["code"], scene.Code != "") {
partial.Code = models.NewOptionalString(*scraped.Code)
}
}
if setOrganized && !scene.Organized {
// just reuse the boolean since we know it's true

View file

@ -2,7 +2,6 @@ package identify
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
@ -12,7 +11,7 @@ import (
)
type PerformerCreator interface {
Create(ctx context.Context, newPerformer models.Performer) (*models.Performer, error)
Create(ctx context.Context, newPerformer *models.Performer) error
UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error
}
@ -33,13 +32,14 @@ func getPerformerID(ctx context.Context, endpoint string, w PerformerCreator, p
}
func createMissingPerformer(ctx context.Context, endpoint string, w PerformerCreator, p *models.ScrapedPerformer) (*int, error) {
created, err := w.Create(ctx, scrapedToPerformerInput(p))
performerInput := scrapedToPerformerInput(p)
err := w.Create(ctx, &performerInput)
if err != nil {
return nil, fmt.Errorf("error creating performer: %w", err)
}
if endpoint != "" && p.RemoteSiteID != nil {
if err := w.UpdateStashIDs(ctx, created.ID, []models.StashID{
if err := w.UpdateStashIDs(ctx, performerInput.ID, []models.StashID{
{
Endpoint: endpoint,
StashID: *p.RemoteSiteID,
@ -49,65 +49,75 @@ func createMissingPerformer(ctx context.Context, endpoint string, w PerformerCre
}
}
return &created.ID, nil
return &performerInput.ID, nil
}
func scrapedToPerformerInput(performer *models.ScrapedPerformer) models.Performer {
currentTime := time.Now()
ret := models.Performer{
Name: sql.NullString{String: *performer.Name, Valid: true},
Name: *performer.Name,
Checksum: md5.FromString(*performer.Name),
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
Favorite: sql.NullBool{Bool: false, Valid: true},
CreatedAt: currentTime,
UpdatedAt: currentTime,
}
if performer.Birthdate != nil {
ret.Birthdate = models.SQLiteDate{String: *performer.Birthdate, Valid: true}
d := models.NewDate(*performer.Birthdate)
ret.Birthdate = &d
}
if performer.DeathDate != nil {
ret.DeathDate = models.SQLiteDate{String: *performer.DeathDate, Valid: true}
d := models.NewDate(*performer.DeathDate)
ret.DeathDate = &d
}
if performer.Gender != nil {
ret.Gender = sql.NullString{String: *performer.Gender, Valid: true}
ret.Gender = models.GenderEnum(*performer.Gender)
}
if performer.Ethnicity != nil {
ret.Ethnicity = sql.NullString{String: *performer.Ethnicity, Valid: true}
ret.Ethnicity = *performer.Ethnicity
}
if performer.Country != nil {
ret.Country = sql.NullString{String: *performer.Country, Valid: true}
ret.Country = *performer.Country
}
if performer.EyeColor != nil {
ret.EyeColor = sql.NullString{String: *performer.EyeColor, Valid: true}
ret.EyeColor = *performer.EyeColor
}
if performer.HairColor != nil {
ret.HairColor = sql.NullString{String: *performer.HairColor, Valid: true}
ret.HairColor = *performer.HairColor
}
if performer.Height != nil {
ret.Height = sql.NullString{String: *performer.Height, Valid: true}
h, err := strconv.Atoi(*performer.Height) // height is stored as an int
if err == nil {
ret.Height = &h
}
}
if performer.Weight != nil {
h, err := strconv.Atoi(*performer.Weight)
if err == nil {
ret.Weight = &h
}
}
if performer.Measurements != nil {
ret.Measurements = sql.NullString{String: *performer.Measurements, Valid: true}
ret.Measurements = *performer.Measurements
}
if performer.FakeTits != nil {
ret.FakeTits = sql.NullString{String: *performer.FakeTits, Valid: true}
ret.FakeTits = *performer.FakeTits
}
if performer.CareerLength != nil {
ret.CareerLength = sql.NullString{String: *performer.CareerLength, Valid: true}
ret.CareerLength = *performer.CareerLength
}
if performer.Tattoos != nil {
ret.Tattoos = sql.NullString{String: *performer.Tattoos, Valid: true}
ret.Tattoos = *performer.Tattoos
}
if performer.Piercings != nil {
ret.Piercings = sql.NullString{String: *performer.Piercings, Valid: true}
ret.Piercings = *performer.Piercings
}
if performer.Aliases != nil {
ret.Aliases = sql.NullString{String: *performer.Aliases, Valid: true}
ret.Aliases = *performer.Aliases
}
if performer.Twitter != nil {
ret.Twitter = sql.NullString{String: *performer.Twitter, Valid: true}
ret.Twitter = *performer.Twitter
}
if performer.Instagram != nil {
ret.Instagram = sql.NullString{String: *performer.Instagram, Valid: true}
ret.Instagram = *performer.Instagram
}
return ret

View file

@ -1,15 +1,16 @@
package identify
import (
"database/sql"
"errors"
"reflect"
"strconv"
"testing"
"time"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
@ -24,9 +25,10 @@ func Test_getPerformerID(t *testing.T) {
name := "name"
mockPerformerReaderWriter := mocks.PerformerReaderWriter{}
mockPerformerReaderWriter.On("Create", testCtx, mock.Anything).Return(&models.Performer{
ID: validStoredID,
}, nil)
mockPerformerReaderWriter.On("Create", testCtx, mock.Anything).Run(func(args mock.Arguments) {
p := args.Get(1).(*models.Performer)
p.ID = validStoredID
}).Return(nil)
type args struct {
endpoint string
@ -132,14 +134,16 @@ func Test_createMissingPerformer(t *testing.T) {
performerID := 1
mockPerformerReaderWriter := mocks.PerformerReaderWriter{}
mockPerformerReaderWriter.On("Create", testCtx, mock.MatchedBy(func(p models.Performer) bool {
return p.Name.String == validName
})).Return(&models.Performer{
ID: performerID,
}, nil)
mockPerformerReaderWriter.On("Create", testCtx, mock.MatchedBy(func(p models.Performer) bool {
return p.Name.String == invalidName
})).Return(nil, errors.New("error creating performer"))
mockPerformerReaderWriter.On("Create", testCtx, mock.MatchedBy(func(p *models.Performer) bool {
return p.Name == validName
})).Run(func(args mock.Arguments) {
p := args.Get(1).(*models.Performer)
p.ID = performerID
}).Return(nil)
mockPerformerReaderWriter.On("Create", testCtx, mock.MatchedBy(func(p *models.Performer) bool {
return p.Name == invalidName
})).Return(errors.New("error creating performer"))
mockPerformerReaderWriter.On("UpdateStashIDs", testCtx, performerID, []models.StashID{
{
@ -230,7 +234,7 @@ func Test_scrapedToPerformerInput(t *testing.T) {
md5 := "b068931cc450442b63f5b3d276ea4297"
var stringValues []string
for i := 0; i < 16; i++ {
for i := 0; i < 17; i++ {
stringValues = append(stringValues, strconv.Itoa(i))
}
@ -241,6 +245,16 @@ func Test_scrapedToPerformerInput(t *testing.T) {
return &ret
}
nextIntVal := func() *int {
ret := upTo
upTo = (upTo + 1) % len(stringValues)
return &ret
}
dateToDatePtr := func(d models.Date) *models.Date {
return &d
}
tests := []struct {
name string
performer *models.ScrapedPerformer
@ -258,6 +272,7 @@ func Test_scrapedToPerformerInput(t *testing.T) {
EyeColor: nextVal(),
HairColor: nextVal(),
Height: nextVal(),
Weight: nextVal(),
Measurements: nextVal(),
FakeTits: nextVal(),
CareerLength: nextVal(),
@ -268,34 +283,25 @@ func Test_scrapedToPerformerInput(t *testing.T) {
Instagram: nextVal(),
},
models.Performer{
Name: models.NullString(name),
Checksum: md5,
Favorite: sql.NullBool{
Bool: false,
Valid: true,
},
Birthdate: models.SQLiteDate{
String: *nextVal(),
Valid: true,
},
DeathDate: models.SQLiteDate{
String: *nextVal(),
Valid: true,
},
Gender: models.NullString(*nextVal()),
Ethnicity: models.NullString(*nextVal()),
Country: models.NullString(*nextVal()),
EyeColor: models.NullString(*nextVal()),
HairColor: models.NullString(*nextVal()),
Height: models.NullString(*nextVal()),
Measurements: models.NullString(*nextVal()),
FakeTits: models.NullString(*nextVal()),
CareerLength: models.NullString(*nextVal()),
Tattoos: models.NullString(*nextVal()),
Piercings: models.NullString(*nextVal()),
Aliases: models.NullString(*nextVal()),
Twitter: models.NullString(*nextVal()),
Instagram: models.NullString(*nextVal()),
Name: name,
Checksum: md5,
Birthdate: dateToDatePtr(models.NewDate(*nextVal())),
DeathDate: dateToDatePtr(models.NewDate(*nextVal())),
Gender: models.GenderEnum(*nextVal()),
Ethnicity: *nextVal(),
Country: *nextVal(),
EyeColor: *nextVal(),
HairColor: *nextVal(),
Height: nextIntVal(),
Weight: nextIntVal(),
Measurements: *nextVal(),
FakeTits: *nextVal(),
CareerLength: *nextVal(),
Tattoos: *nextVal(),
Piercings: *nextVal(),
Aliases: *nextVal(),
Twitter: *nextVal(),
Instagram: *nextVal(),
},
},
{
@ -304,12 +310,8 @@ func Test_scrapedToPerformerInput(t *testing.T) {
Name: &name,
},
models.Performer{
Name: models.NullString(name),
Name: name,
Checksum: md5,
Favorite: sql.NullBool{
Bool: false,
Valid: true,
},
},
},
}
@ -318,12 +320,10 @@ func Test_scrapedToPerformerInput(t *testing.T) {
got := scrapedToPerformerInput(tt.performer)
// clear created/updated dates
got.CreatedAt = models.SQLiteTimestamp{}
got.CreatedAt = time.Time{}
got.UpdatedAt = got.CreatedAt
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("scrapedToPerformerInput() = %v, want %v", got, tt.want)
}
assert.Equal(t, tt.want, got)
})
}
}

View file

@ -139,6 +139,7 @@ const (
ContinuePlaylistDefault = "continue_playlist_default"
ShowStudioAsText = "show_studio_as_text"
CSSEnabled = "cssEnabled"
JavascriptEnabled = "javascriptEnabled"
CustomLocalesEnabled = "customLocalesEnabled"
ShowScrubber = "show_scrubber"
@ -465,7 +466,15 @@ func (i *Instance) getStringMapString(key string) map[string]string {
i.RLock()
defer i.RUnlock()
return i.viper(key).GetStringMapString(key)
ret := i.viper(key).GetStringMapString(key)
// GetStringMapString returns an empty map regardless of whether the
// key exists or not.
if len(ret) == 0 {
return nil
}
return ret
}
type StashConfig struct {
@ -1077,6 +1086,49 @@ func (i *Instance) GetCSSEnabled() bool {
return i.getBool(CSSEnabled)
}
func (i *Instance) GetJavascriptPath() string {
// use custom.js in the same directory as the config file
configFileUsed := i.GetConfigFile()
configDir := filepath.Dir(configFileUsed)
fn := filepath.Join(configDir, "custom.js")
return fn
}
func (i *Instance) GetJavascript() string {
fn := i.GetJavascriptPath()
exists, _ := fsutil.FileExists(fn)
if !exists {
return ""
}
buf, err := os.ReadFile(fn)
if err != nil {
return ""
}
return string(buf)
}
func (i *Instance) SetJavascript(javascript string) {
fn := i.GetJavascriptPath()
i.Lock()
defer i.Unlock()
buf := []byte(javascript)
if err := os.WriteFile(fn, buf, 0777); err != nil {
logger.Warnf("error while writing %v bytes to %v: %v", len(buf), fn, err)
}
}
func (i *Instance) GetJavascriptEnabled() bool {
return i.getBool(JavascriptEnabled)
}
func (i *Instance) GetCustomLocalesPath() string {
// use custom-locales.json in the same directory as the config file
configFileUsed := i.GetConfigFile()

View file

@ -86,6 +86,8 @@ func TestConcurrentConfigAccess(t *testing.T) {
i.Set(ImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay)
i.GetCSSPath()
i.GetCSS()
i.GetJavascriptPath()
i.GetJavascript()
i.GetCustomLocalesPath()
i.GetCustomLocales()
i.Set(CSSEnabled, i.GetCSSEnabled())

View file

@ -32,15 +32,19 @@ func toSnakeCase(v string) string {
func fromSnakeCase(v string) string {
var buf bytes.Buffer
leadingUnderscore := true
capvar := false
for i, c := range v {
switch {
case c == '_' && i > 0:
case c == '_' && !leadingUnderscore && i > 0:
capvar = true
case c == '_' && leadingUnderscore:
buf.WriteRune(c)
case capvar:
buf.WriteRune(unicode.ToUpper(c))
capvar = false
default:
leadingUnderscore = false
buf.WriteRune(c)
}
}
@ -54,7 +58,13 @@ func toSnakeCaseMap(m map[string]interface{}) map[string]interface{} {
for key, val := range m {
adjKey := toSnakeCase(key)
nm[adjKey] = val
switch v := val.(type) {
case map[string]interface{}:
nm[adjKey] = toSnakeCaseMap(v)
default:
nm[adjKey] = val
}
}
return nm
@ -68,13 +78,15 @@ func convertMapValue(val interface{}) interface{} {
case map[interface{}]interface{}:
ret := cast.ToStringMap(v)
for k, vv := range ret {
ret[k] = convertMapValue(vv)
adjKey := fromSnakeCase(k)
ret[adjKey] = convertMapValue(vv)
}
return ret
case map[string]interface{}:
ret := make(map[string]interface{})
for k, vv := range v {
ret[k] = convertMapValue(vv)
adjKey := fromSnakeCase(k)
ret[adjKey] = convertMapValue(vv)
}
return ret
case []interface{}:

View file

@ -26,10 +26,13 @@ type SceneParserInput struct {
type SceneParserResult struct {
Scene *models.Scene `json:"scene"`
Title *string `json:"title"`
Code *string `json:"code"`
Details *string `json:"details"`
Director *string `json:"director"`
URL *string `json:"url"`
Date *string `json:"date"`
Rating *int `json:"rating"`
Rating100 *int `json:"rating100"`
StudioID *string `json:"studio_id"`
GalleryIds []string `json:"gallery_ids"`
PerformerIds []string `json:"performer_ids"`
@ -111,6 +114,7 @@ func initParserFields() {
ret["d"] = newParserField("d", `(?:\.|-|_)`, false)
ret["rating"] = newParserField("rating", `\d`, true)
ret["rating100"] = newParserField("rating100", `\d`, true)
ret["performer"] = newParserField("performer", ".*", true)
ret["studio"] = newParserField("studio", ".*", true)
ret["movie"] = newParserField("movie", ".*", true)
@ -254,6 +258,10 @@ func validateRating(rating int) bool {
return rating >= 1 && rating <= 5
}
func validateRating100(rating100 int) bool {
return rating100 >= 1 && rating100 <= 100
}
func validateDate(dateStr string) bool {
splits := strings.Split(dateStr, "-")
if len(splits) != 3 {
@ -345,6 +353,13 @@ func (h *sceneHolder) setField(field parserField, value interface{}) {
case "rating":
rating, _ := strconv.Atoi(value.(string))
if validateRating(rating) {
// convert to 1-100 scale
rating = models.Rating5To100(rating)
h.result.Rating = &rating
}
case "rating100":
rating, _ := strconv.Atoi(value.(string))
if validateRating100(rating) {
h.result.Rating = &rating
}
case "performer":

Some files were not shown because too many files have changed in this diff Show more