mirror of
https://github.com/stashapp/stash.git
synced 2026-05-09 05:05:29 +02:00
Compare commits
61 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01a7583364 | ||
|
|
9234979084 | ||
|
|
46f72e5574 | ||
|
|
3afe29215d | ||
|
|
db4eabea81 | ||
|
|
1ec5583931 | ||
|
|
2b29207f1e | ||
|
|
98fd0267d0 | ||
|
|
3f83a84afb | ||
|
|
af6491a36f | ||
|
|
cea3c0383f | ||
|
|
2c98ad4d78 | ||
|
|
103181a6d2 | ||
|
|
8e070717e5 | ||
|
|
6004ed52af | ||
|
|
0b811e13b9 | ||
|
|
083ba25d04 | ||
|
|
a33cca6033 | ||
|
|
22d2dbc46b | ||
|
|
2c8a0ad192 | ||
|
|
443de78260 | ||
|
|
ada05a59d0 | ||
|
|
fb1a548be1 | ||
|
|
26cd867a6a | ||
|
|
f26ae0724b | ||
|
|
4de2351e7c | ||
|
|
82d12145cc | ||
|
|
968a97aa45 | ||
|
|
f920bd8b8e | ||
|
|
9b5c0b0e48 | ||
|
|
034ae1a141 | ||
|
|
3af546db92 | ||
|
|
3b90e5191a | ||
|
|
60ce007c02 | ||
|
|
f81053ae7d | ||
|
|
98074e3b57 | ||
|
|
57ddec93e0 | ||
|
|
5edd299b10 | ||
|
|
672147deaf | ||
|
|
6aaf3fe1b7 | ||
|
|
5b857663f1 | ||
|
|
dd8126206c | ||
|
|
059aa96b51 | ||
|
|
02dba484cc | ||
|
|
3feabcbf8b | ||
|
|
b4bcb5fe6a | ||
|
|
fcf1a47920 | ||
|
|
512cf03be9 | ||
|
|
9b346e42f3 | ||
|
|
7f20e91687 | ||
|
|
a2cfd090b5 | ||
|
|
692086d138 | ||
|
|
c648fc3a89 | ||
|
|
6db4988042 | ||
|
|
e405871749 | ||
|
|
299e3c2a42 | ||
|
|
93f4cfdba1 | ||
|
|
0ed2992a72 | ||
|
|
3c06df402b | ||
|
|
1a8f7e8494 | ||
|
|
e6e87d64d6 |
130 changed files with 3097 additions and 3093 deletions
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -6,6 +6,15 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this bug report!
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
- type: checkboxes
|
||||||
|
id: confirm-troubleshooting
|
||||||
|
attributes:
|
||||||
|
label: Have you enabled troubleshooting mode?
|
||||||
|
description: |
|
||||||
|
To ensure the bug is not caused by custom modifications or plugins make sure to enable troubleshooting mode before filing the report. In Stash go to Settings and click **Troubleshooting mode** and then retest to see if the bug still occurs.
|
||||||
|
options:
|
||||||
|
- label: I confirm that the troubleshooting mode is enabled.
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|
@ -61,4 +70,4 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
label: Relevant log output
|
label: Relevant log output
|
||||||
description: Please copy and paste any relevant log output from Settings > Logs. This will be automatically formatted into code, so no need for backticks.
|
description: Please copy and paste any relevant log output from Settings > Logs. This will be automatically formatted into code, so no need for backticks.
|
||||||
render: shell
|
render: shell
|
||||||
|
|
|
||||||
2
.github/workflows/build-compiler.yml
vendored
2
.github/workflows/build-compiler.yml
vendored
|
|
@ -4,7 +4,7 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
COMPILER_IMAGE: ghcr.io/stashapp/compiler:13
|
COMPILER_IMAGE: ghcr.io/stashapp/compiler:14
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-compiler:
|
build-compiler:
|
||||||
|
|
|
||||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
|
|
@ -15,7 +15,7 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
COMPILER_IMAGE: ghcr.io/stashapp/compiler:13
|
COMPILER_IMAGE: ghcr.io/stashapp/compiler:14
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Job 1: Generate code and build UI
|
# Job 1: Generate code and build UI
|
||||||
|
|
@ -30,6 +30,8 @@ jobs:
|
||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: 'go.mod'
|
||||||
|
|
||||||
# pnpm version is read from the packageManager field in package.json
|
# pnpm version is read from the packageManager field in package.json
|
||||||
# very broken (4.3, 4.4)
|
# very broken (4.3, 4.4)
|
||||||
|
|
@ -46,7 +48,7 @@ jobs:
|
||||||
cache-dependency-path: ui/v2.5/pnpm-lock.yaml
|
cache-dependency-path: ui/v2.5/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install UI dependencies
|
- name: Install UI dependencies
|
||||||
run: cd ui/v2.5 && pnpm install --frozen-lockfile
|
run: make pre-ui
|
||||||
|
|
||||||
- name: Generate
|
- name: Generate
|
||||||
run: make generate
|
run: make generate
|
||||||
|
|
|
||||||
6
.github/workflows/golangci-lint.yml
vendored
6
.github/workflows/golangci-lint.yml
vendored
|
|
@ -17,6 +17,8 @@ jobs:
|
||||||
# no tags or depth needed for lint
|
# no tags or depth needed for lint
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: 'go.mod'
|
||||||
|
|
||||||
# generate-backend runs natively (just go generate + touch-ui) — no Docker needed
|
# generate-backend runs natively (just go generate + touch-ui) — no Docker needed
|
||||||
- name: Generate Backend
|
- name: Generate Backend
|
||||||
|
|
@ -25,4 +27,6 @@ jobs:
|
||||||
## WARN
|
## WARN
|
||||||
## using v1, update in a later PR
|
## using v1, update in a later PR
|
||||||
- name: Run golangci-lint
|
- name: Run golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v8
|
||||||
|
with:
|
||||||
|
version: v2.11.4
|
||||||
165
.golangci.yml
165
.golangci.yml
|
|
@ -1,87 +1,100 @@
|
||||||
# options for analysis running
|
version: "2"
|
||||||
run:
|
|
||||||
timeout: 5m
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
default: none
|
||||||
enable:
|
enable:
|
||||||
# Default set of linters from golangci-lint
|
|
||||||
- errcheck
|
|
||||||
- gosimple
|
|
||||||
- govet
|
|
||||||
- ineffassign
|
|
||||||
- staticcheck
|
|
||||||
- typecheck
|
|
||||||
- unused
|
|
||||||
# Linters added by the stash project.
|
|
||||||
# - contextcheck
|
|
||||||
- copyloopvar
|
- copyloopvar
|
||||||
- dogsled
|
- dogsled
|
||||||
|
- errcheck
|
||||||
- errchkjson
|
- errchkjson
|
||||||
- errorlint
|
- errorlint
|
||||||
# - exhaustive
|
|
||||||
- gocritic
|
- gocritic
|
||||||
# - goerr113
|
- govet
|
||||||
- gofmt
|
- ineffassign
|
||||||
# - gomnd
|
|
||||||
# - ifshort
|
|
||||||
- misspell
|
- misspell
|
||||||
# - nakedret
|
|
||||||
- noctx
|
# TODO - fix these in a later PR
|
||||||
|
# - noctx
|
||||||
|
|
||||||
- revive
|
- revive
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- sqlclosecheck
|
- sqlclosecheck
|
||||||
|
- staticcheck
|
||||||
# Project-specific linter overrides
|
- unused
|
||||||
linters-settings:
|
|
||||||
gofmt:
|
settings:
|
||||||
simplify: false
|
staticcheck:
|
||||||
|
checks:
|
||||||
errorlint:
|
- all
|
||||||
# Disable errorf because there are false positives, where you don't want to wrap
|
|
||||||
# an error.
|
# we specify (unnecessary) embedded fields for clarity in many places
|
||||||
errorf: false
|
- -QF1008
|
||||||
asserts: true
|
|
||||||
comparison: true
|
# there's lots of misnamed (eg intId instead of intID) fields in the code.
|
||||||
|
# it's not exactly world-ending, so I'm deferring fixing these for now
|
||||||
revive:
|
- -ST1003
|
||||||
ignore-generated-header: true
|
errorlint:
|
||||||
severity: error
|
errorf: false
|
||||||
confidence: 0.8
|
asserts: true
|
||||||
rules:
|
comparison: true
|
||||||
- name: blank-imports
|
revive:
|
||||||
disabled: true
|
confidence: 0.8
|
||||||
- name: context-as-argument
|
severity: error
|
||||||
- name: context-keys-type
|
rules:
|
||||||
- name: dot-imports
|
- name: blank-imports
|
||||||
- name: error-return
|
disabled: true
|
||||||
- name: error-strings
|
- name: context-as-argument
|
||||||
- name: error-naming
|
- name: context-keys-type
|
||||||
- name: exported
|
- name: dot-imports
|
||||||
disabled: true
|
- name: error-return
|
||||||
- name: if-return
|
- name: error-strings
|
||||||
disabled: true
|
- name: error-naming
|
||||||
- name: increment-decrement
|
- name: exported
|
||||||
- name: var-naming
|
disabled: true
|
||||||
disabled: true
|
- name: if-return
|
||||||
- name: var-declaration
|
disabled: true
|
||||||
- name: package-comments
|
- name: increment-decrement
|
||||||
- name: range
|
- name: var-naming
|
||||||
- name: receiver-naming
|
disabled: true
|
||||||
- name: time-naming
|
- name: var-declaration
|
||||||
- name: unexported-return
|
- name: package-comments
|
||||||
disabled: true
|
- name: range
|
||||||
- name: indent-error-flow
|
- name: receiver-naming
|
||||||
disabled: true
|
- name: time-naming
|
||||||
- name: errorf
|
- name: unexported-return
|
||||||
- name: empty-block
|
disabled: true
|
||||||
disabled: true
|
- name: indent-error-flow
|
||||||
- name: superfluous-else
|
disabled: true
|
||||||
- name: unused-parameter
|
- name: errorf
|
||||||
disabled: true
|
- name: empty-block
|
||||||
- name: unreachable-code
|
disabled: true
|
||||||
- name: redefines-builtin-id
|
- name: superfluous-else
|
||||||
|
- name: unused-parameter
|
||||||
rowserrcheck:
|
disabled: true
|
||||||
packages:
|
- name: unreachable-code
|
||||||
- github.com/jmoiron/sqlx
|
- name: redefines-builtin-id
|
||||||
|
rowserrcheck:
|
||||||
|
packages:
|
||||||
|
- github.com/jmoiron/sqlx
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
settings:
|
||||||
|
gofmt:
|
||||||
|
simplify: false
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
|
|
||||||
28
Makefile
28
Makefile
|
|
@ -10,10 +10,12 @@ ifdef IS_WIN_SHELL
|
||||||
RM := del /s /q
|
RM := del /s /q
|
||||||
RMDIR := rmdir /s /q
|
RMDIR := rmdir /s /q
|
||||||
NOOP := @@
|
NOOP := @@
|
||||||
|
PREFIX := $(USERPROFILE)\\bin
|
||||||
else
|
else
|
||||||
RM := rm -f
|
RM := rm -f
|
||||||
RMDIR := rm -rf
|
RMDIR := rm -rf
|
||||||
NOOP := @:
|
NOOP := @:
|
||||||
|
PREFIX := $(HOME)/.local
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# set LDFLAGS environment variable to any extra ldflags required
|
# set LDFLAGS environment variable to any extra ldflags required
|
||||||
|
|
@ -40,9 +42,6 @@ GO_BUILD_FLAGS := $(GO_BUILD_FLAGS)
|
||||||
GO_BUILD_TAGS := $(GO_BUILD_TAGS)
|
GO_BUILD_TAGS := $(GO_BUILD_TAGS)
|
||||||
GO_BUILD_TAGS += sqlite_stat4 sqlite_math_functions
|
GO_BUILD_TAGS += sqlite_stat4 sqlite_math_functions
|
||||||
|
|
||||||
# set STASH_NOLEGACY environment variable or uncomment to disable legacy browser support
|
|
||||||
# STASH_NOLEGACY := true
|
|
||||||
|
|
||||||
# set STASH_SOURCEMAPS environment variable or uncomment to enable UI sourcemaps
|
# set STASH_SOURCEMAPS environment variable or uncomment to enable UI sourcemaps
|
||||||
# STASH_SOURCEMAPS := true
|
# STASH_SOURCEMAPS := true
|
||||||
|
|
||||||
|
|
@ -282,7 +281,7 @@ endif
|
||||||
generate: generate-backend generate-ui
|
generate: generate-backend generate-ui
|
||||||
|
|
||||||
.PHONY: generate-ui
|
.PHONY: generate-ui
|
||||||
generate-ui:
|
generate-ui: pre-ui
|
||||||
cd ui/v2.5 && npm run gqlgen
|
cd ui/v2.5 && npm run gqlgen
|
||||||
|
|
||||||
.PHONY: generate-backend
|
.PHONY: generate-backend
|
||||||
|
|
@ -365,18 +364,15 @@ ui-env: build-info
|
||||||
$(eval export VITE_APP_DATE := $(BUILD_DATE))
|
$(eval export VITE_APP_DATE := $(BUILD_DATE))
|
||||||
$(eval export VITE_APP_GITHASH := $(GITHASH))
|
$(eval export VITE_APP_GITHASH := $(GITHASH))
|
||||||
$(eval export VITE_APP_STASH_VERSION := $(STASH_VERSION))
|
$(eval export VITE_APP_STASH_VERSION := $(STASH_VERSION))
|
||||||
ifdef STASH_NOLEGACY
|
|
||||||
$(eval export VITE_APP_NOLEGACY := true)
|
|
||||||
endif
|
|
||||||
ifdef STASH_SOURCEMAPS
|
ifdef STASH_SOURCEMAPS
|
||||||
$(eval export VITE_APP_SOURCEMAPS := true)
|
$(eval export VITE_APP_SOURCEMAPS := true)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: ui
|
.PHONY: ui
|
||||||
ui: ui-only generate-login-locale
|
ui: pre-ui generate ui-only generate-login-locale
|
||||||
|
|
||||||
.PHONY: ui-only
|
.PHONY: ui-only
|
||||||
ui-only: ui-env
|
ui-only: ui-env generate ui
|
||||||
cd ui/v2.5 && npm run build
|
cd ui/v2.5 && npm run build
|
||||||
|
|
||||||
.PHONY: zip-ui
|
.PHONY: zip-ui
|
||||||
|
|
@ -394,7 +390,7 @@ fmt-ui:
|
||||||
|
|
||||||
# runs all of the frontend PR-acceptance steps
|
# runs all of the frontend PR-acceptance steps
|
||||||
.PHONY: validate-ui
|
.PHONY: validate-ui
|
||||||
validate-ui:
|
validate-ui: pre-ui generate
|
||||||
cd ui/v2.5 && npm run validate
|
cd ui/v2.5 && npm run validate
|
||||||
|
|
||||||
# these targets run the same steps as fmt-ui and validate-ui, but only on files that have changed
|
# these targets run the same steps as fmt-ui and validate-ui, but only on files that have changed
|
||||||
|
|
@ -444,4 +440,14 @@ start-compiler-container:
|
||||||
|
|
||||||
.PHONY: remove-compiler-container
|
.PHONY: remove-compiler-container
|
||||||
remove-compiler-container:
|
remove-compiler-container:
|
||||||
docker rm -f -v build
|
docker rm -f -v build
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install: build-release
|
||||||
|
ifdef IS_WIN_SHELL
|
||||||
|
@if not exist "$(PREFIX)" mkdir $(PREFIX)
|
||||||
|
@copy "dist\\stash-win.exe" "$(PREFIX)\\stash-win.exe"
|
||||||
|
else
|
||||||
|
@mkdir -p $(PREFIX)/bin
|
||||||
|
@install -m 755 $(STASH_OUTPUT) $(PREFIX)/bin/stash
|
||||||
|
endif
|
||||||
26
README.md
26
README.md
|
|
@ -9,7 +9,7 @@
|
||||||
[](https://github.com/stashapp/stash/releases/latest)
|
[](https://github.com/stashapp/stash/releases/latest)
|
||||||
[](https://github.com/stashapp/stash/labels/bounty)
|
[](https://github.com/stashapp/stash/labels/bounty)
|
||||||
|
|
||||||
### **Stash is a self-hosted webapp written in Go which organizes and serves your diverse content collection, catering to both your SFW and NSFW needs.**
|
<h3>Stash is a self-hosted webapp written in Go which organizes and serves your diverse content collection, catering to both your SFW and NSFW needs.</h3>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -28,15 +28,16 @@ For further information you can consult the [documentation](https://docs.stashap
|
||||||
Step-by-step instructions are available at [docs.stashapp.cc/installation](https://docs.stashapp.cc/installation/).
|
Step-by-step instructions are available at [docs.stashapp.cc/installation](https://docs.stashapp.cc/installation/).
|
||||||
|
|
||||||
> [!important]
|
> [!important]
|
||||||
>**Windows Users**
|
> **Windows Users**
|
||||||
>
|
>
|
||||||
>As of version 0.27.0, Stash no longer supports _Windows 7, 8, Server 2008 and Server 2012._
|
> As of version 0.27.0, Stash no longer supports _Windows 7, 8, Server 2008 and Server 2012._
|
||||||
>At least Windows 10 or Server 2016 is required.
|
> At least Windows 10 or Server 2016 is required.
|
||||||
>
|
>
|
||||||
>**macOS Users**
|
> **macOS Users**
|
||||||
>
|
>
|
||||||
> As of version 0.29.0, Stash requires _macOS 11 Big Sur_ or later.
|
> As of version 0.29.0, Stash requires _macOS 11 Big Sur_ or later.
|
||||||
> Stash can still be run through docker on older versions of macOS.
|
> As of version 0.32.0, Stash requires _macOS 12 Monterey_ or later.
|
||||||
|
> Stash can still be run through Docker on older versions of macOS.
|
||||||
|
|
||||||
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS | <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
|
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS | <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
|
||||||
:---:|:---:|:---:|:---:
|
:---:|:---:|:---:|:---:
|
||||||
|
|
@ -105,6 +106,19 @@ Need help or want to get involved? Start with the documentation, then reach out
|
||||||
- [Themes](https://docs.stashapp.cc/themes/)
|
- [Themes](https://docs.stashapp.cc/themes/)
|
||||||
- [Other projects](https://docs.stashapp.cc/other-projects/)
|
- [Other projects](https://docs.stashapp.cc/other-projects/)
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
- Go
|
||||||
|
- GraphQL API
|
||||||
|
- SQLite
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
- React
|
||||||
|
- TypeScript
|
||||||
|
|
||||||
# For Developers
|
# For Developers
|
||||||
|
|
||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ func recoverPanic() {
|
||||||
exitCode = 1
|
exitCode = 1
|
||||||
logger.Errorf("panic: %v\n%s", err, debug.Stack())
|
logger.Errorf("panic: %v\n%s", err, debug.Stack())
|
||||||
if desktop.IsDesktop() {
|
if desktop.IsDesktop() {
|
||||||
desktop.FatalError(fmt.Errorf("Panic: %v", err))
|
desktop.FatalError(fmt.Errorf("panic: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ ARG STASH_VERSION
|
||||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui-only
|
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui-only
|
||||||
|
|
||||||
# Build Backend
|
# Build Backend
|
||||||
FROM golang:1.24.3-alpine AS backend
|
FROM golang:1.25.9-alpine AS backend
|
||||||
RUN apk add --no-cache make alpine-sdk
|
RUN apk add --no-cache make alpine-sdk
|
||||||
WORKDIR /stash
|
WORKDIR /stash
|
||||||
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# This dockerfile should be built with `make docker-cuda-build` from the stash root.
|
# This dockerfile should be built with `make docker-cuda-build` from the stash root.
|
||||||
ARG CUDA_VERSION=12.8.0
|
ARG CUDA_VERSION=13.2.1
|
||||||
|
|
||||||
# Build Frontend
|
# Build Frontend
|
||||||
FROM node:20-alpine AS frontend
|
FROM node:24-alpine AS frontend
|
||||||
RUN apk add --no-cache make git
|
RUN apk add --no-cache make git
|
||||||
## cache node_modules separately
|
## cache node_modules separately
|
||||||
COPY ./ui/v2.5/package.json ./ui/v2.5/pnpm-lock.yaml /stash/ui/v2.5/
|
COPY ./ui/v2.5/package.json ./ui/v2.5/pnpm-lock.yaml /stash/ui/v2.5/
|
||||||
|
|
@ -19,7 +19,7 @@ ARG STASH_VERSION
|
||||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui-only
|
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui-only
|
||||||
|
|
||||||
# Build Backend
|
# Build Backend
|
||||||
FROM golang:1.24.3-bullseye AS backend
|
FROM golang:1.25.9-bullseye AS backend
|
||||||
RUN apt update && apt install -y build-essential golang
|
RUN apt update && apt install -y build-essential golang
|
||||||
WORKDIR /stash
|
WORKDIR /stash
|
||||||
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ WORKDIR /tmp/osxcross
|
||||||
ARG OSXCROSS_REVISION=5e1b71fcceb23952f3229995edca1b6231525b5b
|
ARG OSXCROSS_REVISION=5e1b71fcceb23952f3229995edca1b6231525b5b
|
||||||
ADD --checksum=sha256:d3f771bbc20612fea577b18a71be3af2eb5ad2dd44624196cf55de866d008647 https://codeload.github.com/tpoechtrager/osxcross/tar.gz/${OSXCROSS_REVISION} /tmp/osxcross.tar.gz
|
ADD --checksum=sha256:d3f771bbc20612fea577b18a71be3af2eb5ad2dd44624196cf55de866d008647 https://codeload.github.com/tpoechtrager/osxcross/tar.gz/${OSXCROSS_REVISION} /tmp/osxcross.tar.gz
|
||||||
|
|
||||||
ARG OSX_SDK_VERSION=11.3
|
ARG OSX_SDK_VERSION=12.3
|
||||||
ARG OSX_SDK_DOWNLOAD_FILE=MacOSX${OSX_SDK_VERSION}.sdk.tar.xz
|
ARG OSX_SDK_DOWNLOAD_FILE=MacOSX${OSX_SDK_VERSION}.sdk.tar.xz
|
||||||
ARG OSX_SDK_DOWNLOAD_URL=https://github.com/phracker/MacOSX-SDKs/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
|
ARG OSX_SDK_DOWNLOAD_URL=https://github.com/joseluisq/macosx-sdks/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
|
||||||
ADD --checksum=sha256:cd4f08a75577145b8f05245a2975f7c81401d75e9535dcffbb879ee1deefcbf4 ${OSX_SDK_DOWNLOAD_URL} /tmp/osxcross/tarballs/${OSX_SDK_DOWNLOAD_FILE}
|
ADD --checksum=sha256:3abd261ceb483c44295a6623fdffe5d44fc4ac2c872526576ec5ab5ad0f6e26c ${OSX_SDK_DOWNLOAD_URL} /tmp/osxcross/tarballs/${OSX_SDK_DOWNLOAD_FILE}
|
||||||
|
|
||||||
ENV UNATTENDED=yes \
|
ENV UNATTENDED=yes \
|
||||||
SDK_VERSION=${OSX_SDK_VERSION} \
|
SDK_VERSION=${OSX_SDK_VERSION} \
|
||||||
OSX_VERSION_MIN=10.10
|
OSX_VERSION_MIN=12.0
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt install -y --no-install-recommends \
|
apt install -y --no-install-recommends \
|
||||||
bash ca-certificates clang cmake git patch libssl-dev bzip2 cpio libbz2-dev libxml2-dev make python3 xz-utils zlib1g-dev
|
bash ca-certificates clang cmake git patch libssl-dev bzip2 cpio libbz2-dev libxml2-dev make python3 xz-utils zlib1g-dev
|
||||||
|
|
@ -46,7 +46,7 @@ RUN cd /opt/cross-freebsd/usr/lib && \
|
||||||
ln -s libc++.so libstdc++.so
|
ln -s libc++.so libstdc++.so
|
||||||
|
|
||||||
### BUILDER
|
### BUILDER
|
||||||
FROM golang:1.24.3 AS builder
|
FROM golang:1.25.9 AS builder
|
||||||
ENV PATH=/opt/osx-ndk-x86/bin:$PATH
|
ENV PATH=/opt/osx-ndk-x86/bin:$PATH
|
||||||
|
|
||||||
# copy in nodejs instead of using nodesource :thumbsup:
|
# copy in nodejs instead of using nodesource :thumbsup:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
host=ghcr.io
|
host=ghcr.io
|
||||||
user=stashapp
|
user=stashapp
|
||||||
repo=compiler
|
repo=compiler
|
||||||
version=13
|
version=14
|
||||||
|
|
||||||
VERSION_IMAGE = ${host}/${user}/${repo}:${version}
|
VERSION_IMAGE = ${host}/${user}/${repo}:${version}
|
||||||
LATEST_IMAGE = ${host}/${user}/${repo}:latest
|
LATEST_IMAGE = ${host}/${user}/${repo}:latest
|
||||||
|
|
|
||||||
24
go.mod
24
go.mod
|
|
@ -1,6 +1,6 @@
|
||||||
module github.com/stashapp/stash
|
module github.com/stashapp/stash
|
||||||
|
|
||||||
go 1.24.3
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.73
|
github.com/99designs/gqlgen v0.17.73
|
||||||
|
|
@ -15,6 +15,7 @@ require (
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
||||||
github.com/doug-martin/goqu/v9 v9.18.0
|
github.com/doug-martin/goqu/v9 v9.18.0
|
||||||
|
github.com/feederbox826/gosx-notifier v0.2.2
|
||||||
github.com/go-chi/chi/v5 v5.2.2
|
github.com/go-chi/chi/v5 v5.2.2
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
github.com/go-chi/httplog v0.3.1
|
github.com/go-chi/httplog v0.3.1
|
||||||
|
|
@ -30,7 +31,6 @@ require (
|
||||||
github.com/jinzhu/copier v0.4.0
|
github.com/jinzhu/copier v0.4.0
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/kermieisinthehouse/gosx-notifier v0.1.2
|
|
||||||
github.com/kermieisinthehouse/systray v1.2.4
|
github.com/kermieisinthehouse/systray v1.2.4
|
||||||
github.com/knadh/koanf/parsers/yaml v1.1.0
|
github.com/knadh/koanf/parsers/yaml v1.1.0
|
||||||
github.com/knadh/koanf/providers/env v1.1.0
|
github.com/knadh/koanf/providers/env v1.1.0
|
||||||
|
|
@ -56,12 +56,12 @@ require (
|
||||||
github.com/vektra/mockery/v2 v2.10.0
|
github.com/vektra/mockery/v2 v2.10.0
|
||||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
|
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
|
||||||
github.com/zencoder/go-dash/v3 v3.0.2
|
github.com/zencoder/go-dash/v3 v3.0.2
|
||||||
golang.org/x/crypto v0.45.0
|
golang.org/x/crypto v0.48.0
|
||||||
golang.org/x/image v0.18.0
|
golang.org/x/image v0.38.0
|
||||||
golang.org/x/net v0.47.0
|
golang.org/x/net v0.50.0
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.41.0
|
||||||
golang.org/x/term v0.37.0
|
golang.org/x/term v0.40.0
|
||||||
golang.org/x/text v0.31.0
|
golang.org/x/text v0.35.0
|
||||||
golang.org/x/time v0.10.0
|
golang.org/x/time v0.10.0
|
||||||
gopkg.in/guregu/null.v4 v4.0.0
|
gopkg.in/guregu/null.v4 v4.0.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
|
|
@ -70,7 +70,7 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||||
github.com/antchfx/xpath v1.3.5 // indirect
|
github.com/antchfx/xpath v1.3.6 // indirect
|
||||||
github.com/asticode/go-astikit v0.20.0 // indirect
|
github.com/asticode/go-astikit v0.20.0 // indirect
|
||||||
github.com/asticode/go-astits v1.8.0 // indirect
|
github.com/asticode/go-astits v1.8.0 // indirect
|
||||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||||
|
|
@ -121,9 +121,9 @@ require (
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.3 // indirect
|
go.yaml.in/yaml/v3 v3.0.3 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
43
go.sum
43
go.sum
|
|
@ -87,8 +87,9 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
|
||||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
github.com/antchfx/htmlquery v1.3.5 h1:aYthDDClnG2a2xePf6tys/UyyM/kRcsFRm+ifhFKoU0=
|
github.com/antchfx/htmlquery v1.3.5 h1:aYthDDClnG2a2xePf6tys/UyyM/kRcsFRm+ifhFKoU0=
|
||||||
github.com/antchfx/htmlquery v1.3.5/go.mod h1:5oyIPIa3ovYGtLqMPNjBF2Uf25NPCKsMjCnQ8lvjaoA=
|
github.com/antchfx/htmlquery v1.3.5/go.mod h1:5oyIPIa3ovYGtLqMPNjBF2Uf25NPCKsMjCnQ8lvjaoA=
|
||||||
github.com/antchfx/xpath v1.3.5 h1:PqbXLC3TkfeZyakF5eeh3NTWEbYl4VHNVeufANzDbKQ=
|
|
||||||
github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
|
github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI=
|
||||||
|
github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||||
|
|
@ -187,6 +188,8 @@ github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
|
github.com/feederbox826/gosx-notifier v0.2.2 h1:26NkaJZ8Wzptx82R46c9pkVAcFwGSU7kxWrOKmRWlC0=
|
||||||
|
github.com/feederbox826/gosx-notifier v0.2.2/go.mod h1:R6rqw7VuwuiCuvsr7EOONmWq++CRA5Ijmkmx75/C3Fs=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||||
|
|
@ -389,8 +392,6 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kermieisinthehouse/gosx-notifier v0.1.2 h1:KV0KBeKK2B24kIHY7iK0jgS64Q05f4oB+hUZmsPodxQ=
|
|
||||||
github.com/kermieisinthehouse/gosx-notifier v0.1.2/go.mod h1:xyWT07azFtUOcHl96qMVvKhvKzsMcS7rKTHQyv8WTho=
|
|
||||||
github.com/kermieisinthehouse/systray v1.2.4 h1:pdH5vnl+KKjRrVCRU4g/2W1/0HVzuuJ6WXHlPPHYY6s=
|
github.com/kermieisinthehouse/systray v1.2.4 h1:pdH5vnl+KKjRrVCRU4g/2W1/0HVzuuJ6WXHlPPHYY6s=
|
||||||
github.com/kermieisinthehouse/systray v1.2.4/go.mod h1:axh6C/jNuSyC0QGtidZJURc9h+h41HNoMySoLVrhVR4=
|
github.com/kermieisinthehouse/systray v1.2.4/go.mod h1:axh6C/jNuSyC0QGtidZJURc9h+h41HNoMySoLVrhVR4=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
|
@ -667,8 +668,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
|
@ -682,8 +683,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
||||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
|
@ -714,8 +715,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
|
@ -770,8 +771,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
|
@ -806,8 +807,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/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=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
|
@ -894,8 +895,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
|
@ -905,8 +906,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|
@ -923,8 +924,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
|
@ -992,8 +993,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|
|
||||||
|
|
@ -148,12 +148,12 @@ func makeGithubRequest(ctx context.Context, url string, output interface{}) erro
|
||||||
response, err := client.Do(req)
|
response, err := client.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//lint:ignore ST1005 Github is a proper capitalized noun
|
//nolint:staticcheck // ST1005 Github is a proper capitalized noun
|
||||||
return fmt.Errorf("Github API request failed: %w", err)
|
return fmt.Errorf("Github API request failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.StatusCode != http.StatusOK {
|
if response.StatusCode != http.StatusOK {
|
||||||
//lint:ignore ST1005 Github is a proper capitalized noun
|
//nolint:staticcheck // ST1005 Github is a proper capitalized noun
|
||||||
return fmt.Errorf("Github API request failed: %s", response.Status)
|
return fmt.Errorf("Github API request failed: %s", response.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +161,7 @@ func makeGithubRequest(ctx context.Context, url string, output interface{}) erro
|
||||||
|
|
||||||
data, err := io.ReadAll(response.Body)
|
data, err := io.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//lint:ignore ST1005 Github is a proper capitalized noun
|
//nolint:staticcheck // ST1005 Github is a proper capitalized noun
|
||||||
return fmt.Errorf("Github API read response failed: %w", err)
|
return fmt.Errorf("Github API read response failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,10 +295,10 @@ func printLatestVersion(ctx context.Context) {
|
||||||
logger.Errorf("Couldn't retrieve latest version: %v", err)
|
logger.Errorf("Couldn't retrieve latest version: %v", err)
|
||||||
} else {
|
} else {
|
||||||
_, githash, _ := build.Version()
|
_, githash, _ := build.Version()
|
||||||
switch {
|
switch githash {
|
||||||
case githash == "":
|
case "":
|
||||||
logger.Infof("Latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
|
logger.Infof("Latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
|
||||||
case githash == latestRelease.ShortHash:
|
case latestRelease.ShortHash:
|
||||||
logger.Infof("Version %s (%s) is already the latest released", latestRelease.Version, latestRelease.ShortHash)
|
logger.Infof("Version %s (%s) is already the latest released", latestRelease.Version, latestRelease.ShortHash)
|
||||||
default:
|
default:
|
||||||
logger.Infof("New version available: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
|
logger.Infof("New version available: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
|
||||||
objHash := obj.GetHash(config.GetVideoFileNamingAlgorithm())
|
objHash := obj.GetHash(config.GetVideoFileNamingAlgorithm())
|
||||||
vttPath := builder.GetSpriteVTTURL(objHash)
|
vttPath := builder.GetSpriteVTTURL(objHash)
|
||||||
spritePath := builder.GetSpriteURL(objHash)
|
spritePath := builder.GetSpriteURL(objHash)
|
||||||
funscriptPath := builder.GetFunscriptURL()
|
funscriptPath := builder.GetFunscriptURL(config.GetAPIKey()).String()
|
||||||
captionBasePath := builder.GetCaptionURL()
|
captionBasePath := builder.GetCaptionURL()
|
||||||
interactiveHeatmap := builder.GetInteractiveHeatmapURL()
|
interactiveHeatmap := builder.GetInteractiveHeatmapURL()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ func (r *mutationResolver) Migrate(ctx context.Context, input manager.MigrateInp
|
||||||
Database: mgr.Database,
|
Database: mgr.Database,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := t.PreExecute(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
jobID := mgr.JobManager.Add(ctx, "Migrating database...", t)
|
jobID := mgr.JobManager.Add(ctx, "Migrating database...", t)
|
||||||
|
|
||||||
return strconv.Itoa(jobID), nil
|
return strconv.Itoa(jobID), nil
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,10 @@ import (
|
||||||
func refreshPackageType(typeArg PackageType) {
|
func refreshPackageType(typeArg PackageType) {
|
||||||
mgr := manager.GetInstance()
|
mgr := manager.GetInstance()
|
||||||
|
|
||||||
if typeArg == PackageTypePlugin {
|
switch typeArg {
|
||||||
|
case PackageTypePlugin:
|
||||||
mgr.RefreshPluginCache()
|
mgr.RefreshPluginCache()
|
||||||
} else if typeArg == PackageTypeScraper {
|
case PackageTypeScraper:
|
||||||
mgr.RefreshScraperCache()
|
mgr.RefreshScraperCache()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -654,7 +654,7 @@ func (r *mutationResolver) PerformerMerge(ctx context.Context, input PerformerMe
|
||||||
}
|
}
|
||||||
legacyURLs := legacyPerformerURLsFromInput(*input.Values, translator)
|
legacyURLs := legacyPerformerURLsFromInput(*input.Values, translator)
|
||||||
if legacyURLs.AnySet() {
|
if legacyURLs.AnySet() {
|
||||||
return nil, errors.New("Merging legacy performer URLs is not supported")
|
return nil, errors.New("merging legacy performer URLs is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.Values.Image != nil {
|
if input.Values.Image != nil {
|
||||||
|
|
|
||||||
|
|
@ -33,15 +33,26 @@ func (r *queryResolver) FindJob(ctx context.Context, input FindJobInput) (*Job,
|
||||||
}
|
}
|
||||||
|
|
||||||
func jobToJobModel(j job.Job) *Job {
|
func jobToJobModel(j job.Job) *Job {
|
||||||
|
subTasks := make([]string, len(j.Details))
|
||||||
|
for i, t := range j.Details {
|
||||||
|
subTasks[i] = sanitiseWebsocketString(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobError *string
|
||||||
|
if j.Error != nil {
|
||||||
|
s := sanitiseWebsocketString(*j.Error)
|
||||||
|
jobError = &s
|
||||||
|
}
|
||||||
|
|
||||||
ret := &Job{
|
ret := &Job{
|
||||||
ID: strconv.Itoa(j.ID),
|
ID: strconv.Itoa(j.ID),
|
||||||
Status: JobStatus(j.Status),
|
Status: JobStatus(j.Status),
|
||||||
Description: j.Description,
|
Description: sanitiseWebsocketString(j.Description),
|
||||||
SubTasks: j.Details,
|
SubTasks: subTasks,
|
||||||
StartTime: j.StartTime,
|
StartTime: j.StartTime,
|
||||||
EndTime: j.EndTime,
|
EndTime: j.EndTime,
|
||||||
AddTime: j.AddTime,
|
AddTime: j.AddTime,
|
||||||
Error: j.Error,
|
Error: jobError,
|
||||||
}
|
}
|
||||||
|
|
||||||
if j.Progress != -1 {
|
if j.Progress != -1 {
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,19 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/log"
|
"github.com/stashapp/stash/internal/log"
|
||||||
"github.com/stashapp/stash/internal/manager"
|
"github.com/stashapp/stash/internal/manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// sanitiseWebsocketString is used to ensure that any strings sent over the websocket are valid UTF-8.
|
||||||
|
// Any invalid UTF-8 sequences will be replaced with the Unicode replacement character (U+FFFD).
|
||||||
|
// Invalid UTF-8 sequences can cause the websocket connection to be closed.
|
||||||
|
func sanitiseWebsocketString(s string) string {
|
||||||
|
return strings.ToValidUTF8(s, "\uFFFD")
|
||||||
|
}
|
||||||
|
|
||||||
func getLogLevel(logType string) LogLevel {
|
func getLogLevel(logType string) LogLevel {
|
||||||
switch logType {
|
switch logType {
|
||||||
case "progress":
|
case "progress":
|
||||||
|
|
@ -33,7 +41,7 @@ func logEntriesFromLogItems(logItems []log.LogItem) []*LogEntry {
|
||||||
ret[i] = &LogEntry{
|
ret[i] = &LogEntry{
|
||||||
Time: entry.Time,
|
Time: entry.Time,
|
||||||
Level: getLogLevel(entry.Type),
|
Level: getLogLevel(entry.Type),
|
||||||
Message: entry.Message,
|
Message: sanitiseWebsocketString(entry.Message),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,20 @@ func (b SceneURLBuilder) GetScreenshotURL() string {
|
||||||
return b.BaseURL + "/scene/" + b.SceneID + "/screenshot?t=" + b.UpdatedAt
|
return b.BaseURL + "/scene/" + b.SceneID + "/screenshot?t=" + b.UpdatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b SceneURLBuilder) GetFunscriptURL() string {
|
func (b SceneURLBuilder) GetFunscriptURL(apiKey string) *url.URL {
|
||||||
return b.BaseURL + "/scene/" + b.SceneID + "/funscript"
|
u, err := url.Parse(fmt.Sprintf("%s/scene/%s/funscript", b.BaseURL, b.SceneID))
|
||||||
|
if err != nil {
|
||||||
|
// shouldn't happen
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiKey != "" {
|
||||||
|
v := u.Query()
|
||||||
|
v.Set("apikey", apiKey)
|
||||||
|
u.RawQuery = v.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b SceneURLBuilder) GetCaptionURL() string {
|
func (b SceneURLBuilder) GetCaptionURL() string {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
gosxnotifier "github.com/kermieisinthehouse/gosx-notifier"
|
gosxnotifier "github.com/feederbox826/gosx-notifier"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,10 @@ type MetadataOptions struct {
|
||||||
SetCoverImage *bool `json:"setCoverImage"`
|
SetCoverImage *bool `json:"setCoverImage"`
|
||||||
SetOrganized *bool `json:"setOrganized"`
|
SetOrganized *bool `json:"setOrganized"`
|
||||||
// defaults to true if not provided
|
// defaults to true if not provided
|
||||||
|
|
||||||
// Deprecated: use PerformerGenders instead
|
// Deprecated: use PerformerGenders instead
|
||||||
IncludeMalePerformers *bool `json:"includeMalePerformers"`
|
IncludeMalePerformers *bool `json:"includeMalePerformers"`
|
||||||
|
|
||||||
// Filter to only include performers with these genders. If not provided, all genders are included.
|
// Filter to only include performers with these genders. If not provided, all genders are included.
|
||||||
PerformerGenders []models.GenderEnum `json:"performerGenders"`
|
PerformerGenders []models.GenderEnum `json:"performerGenders"`
|
||||||
// defaults to true if not provided
|
// defaults to true if not provided
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ type SceneMissingHashCounter interface {
|
||||||
// will ensure that all oshash values are set on all scenes.
|
// will ensure that all oshash values are set on all scenes.
|
||||||
func ValidateVideoFileNamingAlgorithm(ctx context.Context, qb SceneMissingHashCounter, newValue models.HashAlgorithm) error {
|
func ValidateVideoFileNamingAlgorithm(ctx context.Context, qb SceneMissingHashCounter, newValue models.HashAlgorithm) error {
|
||||||
// if algorithm is being set to MD5, then all checksums must be present
|
// if algorithm is being set to MD5, then all checksums must be present
|
||||||
if newValue == models.HashAlgorithmMd5 {
|
switch newValue {
|
||||||
|
case models.HashAlgorithmMd5:
|
||||||
missingMD5, err := qb.CountMissingChecksum(ctx)
|
missingMD5, err := qb.CountMissingChecksum(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -31,7 +32,7 @@ func ValidateVideoFileNamingAlgorithm(ctx context.Context, qb SceneMissingHashCo
|
||||||
if missingMD5 > 0 {
|
if missingMD5 > 0 {
|
||||||
return errors.New("some checksums are missing on scenes. Run Scan with calculateMD5 set to true")
|
return errors.New("some checksums are missing on scenes. Run Scan with calculateMD5 set to true")
|
||||||
}
|
}
|
||||||
} else if newValue == models.HashAlgorithmOshash {
|
case models.HashAlgorithmOshash:
|
||||||
missingOSHash, err := qb.CountMissingOSHash(ctx)
|
missingOSHash, err := qb.CountMissingOSHash(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -408,7 +408,7 @@ func ConvertFunscriptToCSV(funscriptPath string) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// I don't know whether the csv format requires int or float, so for now we'll use int
|
// I don't know whether the csv format requires int or float, so for now we'll use int
|
||||||
buffer.WriteString(fmt.Sprintf("%d,%d\r\n", int(math.Round(action.At)), pos))
|
fmt.Fprintf(&buffer, "%d,%d\r\n", int(math.Round(action.At)), pos)
|
||||||
}
|
}
|
||||||
return buffer.Bytes(), nil
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,10 @@ func performImport(ctx context.Context, i importer, duplicateBehaviour ImportDup
|
||||||
var id int
|
var id int
|
||||||
|
|
||||||
if existing != nil {
|
if existing != nil {
|
||||||
if duplicateBehaviour == ImportDuplicateEnumFail {
|
switch duplicateBehaviour {
|
||||||
|
case ImportDuplicateEnumFail:
|
||||||
return fmt.Errorf("existing object with name '%s'", name)
|
return fmt.Errorf("existing object with name '%s'", name)
|
||||||
} else if duplicateBehaviour == ImportDuplicateEnumIgnore {
|
case ImportDuplicateEnumIgnore:
|
||||||
logger.Infof("Skipping existing object %q", name)
|
logger.Infof("Skipping existing object %q", name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -313,9 +313,36 @@ func (j *CleanGeneratedJob) cleanBlobFiles(ctx context.Context, progress *job.Pr
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove empty hash prefix subdirectories
|
||||||
|
j.removeEmptyDirs(j.Paths.Blobs)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *CleanGeneratedJob) removeEmptyDirs(root string) {
|
||||||
|
entries, err := os.ReadDir(root)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dirPath := filepath.Join(root, entry.Name())
|
||||||
|
subEntries, err := os.ReadDir(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(subEntries) == 0 {
|
||||||
|
j.logDelete("removing empty directory: %s", entry.Name())
|
||||||
|
j.deleteDir(dirPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (j *CleanGeneratedJob) getScenesWithHash(ctx context.Context, hash string) ([]*models.Scene, error) {
|
func (j *CleanGeneratedJob) getScenesWithHash(ctx context.Context, hash string) ([]*models.Scene, error) {
|
||||||
fp := models.Fingerprint{
|
fp := models.Fingerprint{
|
||||||
Fingerprint: hash,
|
Fingerprint: hash,
|
||||||
|
|
@ -637,6 +664,8 @@ func (j *CleanGeneratedJob) cleanMarkerFiles(ctx context.Context, progress *job.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
j.removeEmptyDirs(j.Paths.Generated.Markers)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -730,5 +759,7 @@ func (j *CleanGeneratedJob) cleanThumbnailFiles(ctx context.Context, progress *j
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
j.removeEmptyDirs(j.Paths.Generated.Thumbnails)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/job"
|
"github.com/stashapp/stash/pkg/job"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/sqlite"
|
"github.com/stashapp/stash/pkg/sqlite"
|
||||||
|
|
@ -29,6 +31,21 @@ type databaseSchemaInfo struct {
|
||||||
StepsRequired uint
|
StepsRequired uint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PreExecute validates the environment before executing the migration.
|
||||||
|
// It returns an error if the migration cannot be performed.
|
||||||
|
func (s *MigrateJob) PreExecute() error {
|
||||||
|
// ensure backup directory exists and is writable
|
||||||
|
backupDir := s.Config.GetBackupDirectoryPathOrDefault()
|
||||||
|
if backupDir != "" {
|
||||||
|
if err := fsutil.EnsureDir(backupDir); err != nil {
|
||||||
|
logger.Errorf("error ensuring backup directory exists: %s", err)
|
||||||
|
logger.Warnf("Backup directory (%s) must be modified to a valid directory or removed from the config file", config.BackupDirectoryPath)
|
||||||
|
return fmt.Errorf("error creating backup directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error {
|
func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error {
|
||||||
schemaInfo, err := s.required()
|
schemaInfo, err := s.required()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,9 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error
|
||||||
|
|
||||||
for f := range queue {
|
for f := range queue {
|
||||||
if job.IsCancelled(ctx) {
|
if job.IsCancelled(ctx) {
|
||||||
break
|
// keep draining the queue so the producer goroutine can finish
|
||||||
|
// and release its read transaction, otherwise the DB stays locked
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Add()
|
wg.Add()
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ func (j *OptimiseDatabaseJob) Execute(ctx context.Context, progress *job.Progres
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error analyzing database: %w", err)
|
return fmt.Errorf("error analyzing database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.ExecuteTask("Vacuuming database", func() {
|
progress.ExecuteTask("Vacuuming database", func() {
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,12 @@ func (s *Manager) RunPluginTask(
|
||||||
pluginProgress := make(chan float64)
|
pluginProgress := make(chan float64)
|
||||||
task, err := s.PluginCache.CreateTask(ctx, pluginID, taskName, args, pluginProgress)
|
task, err := s.PluginCache.CreateTask(ctx, pluginID, taskName, args, pluginProgress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error creating plugin task: %w", err)
|
return fmt.Errorf("error creating plugin task: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = task.Start()
|
err = task.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error running plugin task: %w", err)
|
return fmt.Errorf("error running plugin task: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
|
|
|
||||||
|
|
@ -283,8 +283,10 @@ func (j *ScanJob) processQueue(ctx context.Context, parallelTasks int, progress
|
||||||
|
|
||||||
for f := range j.fileQueue {
|
for f := range j.fileQueue {
|
||||||
logger.Tracef("Processing queued file %s", f.Path)
|
logger.Tracef("Processing queued file %s", f.Path)
|
||||||
if err := ctx.Err(); err != nil {
|
if ctx.Err() != nil {
|
||||||
return
|
// Keep receiving until queueFiles closes the channel; otherwise
|
||||||
|
// the walker can block on send (full buffer) and never finish.
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Add()
|
wg.Add()
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,13 @@ func (f *FFMpeg) InitHWSupport(ctx context.Context) {
|
||||||
|
|
||||||
// log if the initialization takes too long
|
// log if the initialization takes too long
|
||||||
const hwInitLogTimeoutSecondsDefault = 5
|
const hwInitLogTimeoutSecondsDefault = 5
|
||||||
hwInitLogTimeoutSeconds := hwInitLogTimeoutSecondsDefault * time.Second
|
hwInitLogTimeout := hwInitLogTimeoutSecondsDefault * time.Second
|
||||||
timer := time.NewTimer(hwInitLogTimeoutSeconds)
|
timer := time.NewTimer(hwInitLogTimeout)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
select {
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
logger.Warnf("[InitHWSupport] Hardware codec initialization is taking longer than %s...", hwInitLogTimeoutSeconds)
|
logger.Warnf("[InitHWSupport] Hardware codec initialization is taking longer than %s...", hwInitLogTimeout)
|
||||||
logger.Info("[InitHWSupport] Hardware encoding will not be available until initialization is complete.")
|
logger.Info("[InitHWSupport] Hardware encoding will not be available until initialization is complete.")
|
||||||
case <-done:
|
case <-done:
|
||||||
if !timer.Stop() {
|
if !timer.Stop() {
|
||||||
|
|
@ -96,16 +96,16 @@ func (f *FFMpeg) initHWSupport(ctx context.Context) {
|
||||||
|
|
||||||
// #6064 - add timeout to context to prevent hangs
|
// #6064 - add timeout to context to prevent hangs
|
||||||
const hwTestTimeoutSecondsDefault = 10
|
const hwTestTimeoutSecondsDefault = 10
|
||||||
hwTestTimeoutSeconds := hwTestTimeoutSecondsDefault * time.Second
|
hwTestTimeout := hwTestTimeoutSecondsDefault * time.Second
|
||||||
|
|
||||||
// allow timeout to be overridden with environment variable
|
// allow timeout to be overridden with environment variable
|
||||||
if timeout := os.Getenv("STASH_HW_TEST_TIMEOUT"); timeout != "" {
|
if timeout := os.Getenv("STASH_HW_TEST_TIMEOUT"); timeout != "" {
|
||||||
if seconds, err := strconv.Atoi(timeout); err == nil {
|
if seconds, err := strconv.Atoi(timeout); err == nil {
|
||||||
hwTestTimeoutSeconds = time.Duration(seconds) * time.Second
|
hwTestTimeout = time.Duration(seconds) * time.Second
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testCtx, cancel := context.WithTimeout(ctx, hwTestTimeoutSeconds)
|
testCtx, cancel := context.WithTimeout(ctx, hwTestTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := f.Command(testCtx, args)
|
cmd := f.Command(testCtx, args)
|
||||||
|
|
@ -117,7 +117,7 @@ func (f *FFMpeg) initHWSupport(ctx context.Context) {
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
if testCtx.Err() != nil {
|
if testCtx.Err() != nil {
|
||||||
logger.Debugf("[InitHWSupport] Codec %s test timed out after %s", codec, hwTestTimeoutSeconds)
|
logger.Debugf("[InitHWSupport] Codec %s test timed out after %s", codec, hwTestTimeout)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,8 @@ func (f *StashIgnoreFilter) collectIgnoreEntries(dir string, libraryRoot string)
|
||||||
current := dir
|
current := dir
|
||||||
for {
|
for {
|
||||||
// Check if we're still within the library root.
|
// Check if we're still within the library root.
|
||||||
|
// nolint:staticcheck // QF1006 - we could make this the for condition
|
||||||
|
// but I don't think it improves readability
|
||||||
if !isPathInOrEqual(libraryRoot, current) {
|
if !isPathInOrEqual(libraryRoot, current) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,19 +69,19 @@ type ScanHandler struct {
|
||||||
|
|
||||||
func (h *ScanHandler) validate() error {
|
func (h *ScanHandler) validate() error {
|
||||||
if h.CreatorUpdater == nil {
|
if h.CreatorUpdater == nil {
|
||||||
return errors.New("CreatorUpdater is required")
|
return errors.New("internal error: CreatorUpdater is required")
|
||||||
}
|
}
|
||||||
if h.ScanGenerator == nil {
|
if h.ScanGenerator == nil {
|
||||||
return errors.New("ScanGenerator is required")
|
return errors.New("internal error: ScanGenerator is required")
|
||||||
}
|
}
|
||||||
if h.GalleryFinder == nil {
|
if h.GalleryFinder == nil {
|
||||||
return errors.New("GalleryFinder is required")
|
return errors.New("internal error: GalleryFinder is required")
|
||||||
}
|
}
|
||||||
if h.ScanConfig == nil {
|
if h.ScanConfig == nil {
|
||||||
return errors.New("ScanConfig is required")
|
return errors.New("internal error: ScanConfig is required")
|
||||||
}
|
}
|
||||||
if h.Paths == nil {
|
if h.Paths == nil {
|
||||||
return errors.New("Paths is required")
|
return errors.New("internal error: Paths is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -375,13 +375,13 @@ func (h *ScanHandler) getOrCreateGallery(ctx context.Context, f models.File) (*m
|
||||||
if _, err := os.Stat(filepath.Join(folderPath, ".forcegallery")); err == nil {
|
if _, err := os.Stat(filepath.Join(folderPath, ".forcegallery")); err == nil {
|
||||||
forceGallery = true
|
forceGallery = true
|
||||||
} else if !errors.Is(err, os.ErrNotExist) {
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, fmt.Errorf("Could not test Path %s: %w", folderPath, err)
|
return nil, fmt.Errorf("could not test Path %s: %w", folderPath, err)
|
||||||
}
|
}
|
||||||
exemptGallery := false
|
exemptGallery := false
|
||||||
if _, err := os.Stat(filepath.Join(folderPath, ".nogallery")); err == nil {
|
if _, err := os.Stat(filepath.Join(folderPath, ".nogallery")); err == nil {
|
||||||
exemptGallery = true
|
exemptGallery = true
|
||||||
} else if !errors.Is(err, os.ErrNotExist) {
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, fmt.Errorf("Could not test Path %s: %w", folderPath, err)
|
return nil, fmt.Errorf("could not test Path %s: %w", folderPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if forceGallery || (h.ScanConfig.GetCreateGalleriesFromFolders() && !exemptGallery) {
|
if forceGallery || (h.ScanConfig.GetCreateGalleriesFromFolders() && !exemptGallery) {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,23 @@ type Job struct {
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// statusCopy returns a copy of the Job with only the fields needed for
|
||||||
|
// status reporting. Internal fields (exec, cancelFunc, outerCtx) are
|
||||||
|
// excluded so that subscription channels don't retain heavy resources.
|
||||||
|
func (j *Job) statusCopy() Job {
|
||||||
|
return Job{
|
||||||
|
ID: j.ID,
|
||||||
|
Status: j.Status,
|
||||||
|
Details: j.Details,
|
||||||
|
Description: j.Description,
|
||||||
|
Progress: j.Progress,
|
||||||
|
StartTime: j.StartTime,
|
||||||
|
EndTime: j.EndTime,
|
||||||
|
AddTime: j.AddTime,
|
||||||
|
Error: j.Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TimeElapsed returns the total time elapsed for the job.
|
// TimeElapsed returns the total time elapsed for the job.
|
||||||
// If the EndTime is set, then it uses this to calculate the elapsed time, otherwise it uses time.Now.
|
// If the EndTime is set, then it uses this to calculate the elapsed time, otherwise it uses time.Now.
|
||||||
func (j *Job) TimeElapsed() time.Duration {
|
func (j *Job) TimeElapsed() time.Duration {
|
||||||
|
|
@ -80,9 +97,10 @@ func (j *Job) TimeElapsed() time.Duration {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *Job) cancel() {
|
func (j *Job) cancel() {
|
||||||
if j.Status == StatusReady {
|
switch j.Status {
|
||||||
|
case StatusReady:
|
||||||
j.Status = StatusCancelled
|
j.Status = StatusCancelled
|
||||||
} else if j.Status == StatusRunning {
|
case StatusRunning:
|
||||||
j.Status = StatusStopping
|
j.Status = StatusStopping
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ func (m *Manager) notifyNewJob(j *Job) {
|
||||||
for _, s := range m.subscriptions {
|
for _, s := range m.subscriptions {
|
||||||
// don't block if channel is full
|
// don't block if channel is full
|
||||||
select {
|
select {
|
||||||
case s.newJob <- *j:
|
case s.newJob <- j.statusCopy():
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -232,7 +232,9 @@ func (m *Manager) removeJob(job *Job) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear any subtasks
|
// release the executor and subtask details so they can be GC'd
|
||||||
|
// while the job remains in the graveyard for status reporting
|
||||||
|
job.exec = nil
|
||||||
job.Details = nil
|
job.Details = nil
|
||||||
|
|
||||||
m.queue = append(m.queue[:index], m.queue[index+1:]...)
|
m.queue = append(m.queue[:index], m.queue[index+1:]...)
|
||||||
|
|
@ -246,7 +248,7 @@ func (m *Manager) removeJob(job *Job) {
|
||||||
for _, s := range m.subscriptions {
|
for _, s := range m.subscriptions {
|
||||||
// don't block if channel is full
|
// don't block if channel is full
|
||||||
select {
|
select {
|
||||||
case s.removedJob <- *job:
|
case s.removedJob <- job.statusCopy():
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -310,8 +312,7 @@ func (m *Manager) GetJob(id int) *Job {
|
||||||
// get from the queue or graveyard
|
// get from the queue or graveyard
|
||||||
_, j := m.getJob(append(m.queue, m.graveyard...), id)
|
_, j := m.getJob(append(m.queue, m.graveyard...), id)
|
||||||
if j != nil {
|
if j != nil {
|
||||||
// make a copy of the job and return the pointer
|
jCopy := j.statusCopy()
|
||||||
jCopy := *j
|
|
||||||
return &jCopy
|
return &jCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -326,8 +327,7 @@ func (m *Manager) GetQueue() []Job {
|
||||||
var ret []Job
|
var ret []Job
|
||||||
|
|
||||||
for _, j := range m.queue {
|
for _, j := range m.queue {
|
||||||
jCopy := *j
|
ret = append(ret, j.statusCopy())
|
||||||
ret = append(ret, jCopy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
@ -372,7 +372,7 @@ func (m *Manager) notifyJobUpdate(j *Job) {
|
||||||
for _, s := range m.subscriptions {
|
for _, s := range m.subscriptions {
|
||||||
// don't block if channel is full
|
// don't block if channel is full
|
||||||
select {
|
select {
|
||||||
case s.updatedJob <- *j:
|
case s.updatedJob <- j.statusCopy():
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func (tq *TaskQueue) executer(ctx context.Context) {
|
||||||
defer tq.wg.Wait()
|
defer tq.wg.Wait()
|
||||||
for task := range tq.tasks {
|
for task := range tq.tasks {
|
||||||
if IsCancelled(ctx) {
|
if IsCancelled(ctx) {
|
||||||
return
|
continue // allow channel to continue draining until Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
tt := task
|
tt := task
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ type GroupNamesFinder interface {
|
||||||
|
|
||||||
type SceneRelationships struct {
|
type SceneRelationships struct {
|
||||||
PerformerFinder PerformerFinder
|
PerformerFinder PerformerFinder
|
||||||
TagFinder models.TagQueryer
|
TagFinder models.TagNameFinder
|
||||||
StudioFinder StudioFinder
|
StudioFinder StudioFinder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,7 +189,7 @@ func ScrapedGroup(ctx context.Context, qb GroupNamesFinder, storedID *string, na
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScrapedTagHierarchy executes ScrapedTag for the provided tag and its parent.
|
// ScrapedTagHierarchy executes ScrapedTag for the provided tag and its parent.
|
||||||
func ScrapedTagHierarchy(ctx context.Context, qb models.TagQueryer, s *models.ScrapedTag, stashBoxEndpoint string) error {
|
func ScrapedTagHierarchy(ctx context.Context, qb models.TagNameFinder, s *models.ScrapedTag, stashBoxEndpoint string) error {
|
||||||
if err := ScrapedTag(ctx, qb, s, stashBoxEndpoint); err != nil {
|
if err := ScrapedTag(ctx, qb, s, stashBoxEndpoint); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +204,7 @@ func ScrapedTagHierarchy(ctx context.Context, qb models.TagQueryer, s *models.Sc
|
||||||
|
|
||||||
// ScrapedTag matches the provided tag with the tags
|
// ScrapedTag matches the provided tag with the tags
|
||||||
// in the database and sets the ID field if one is found.
|
// in the database and sets the ID field if one is found.
|
||||||
func ScrapedTag(ctx context.Context, qb models.TagQueryer, s *models.ScrapedTag, stashBoxEndpoint string) error {
|
func ScrapedTag(ctx context.Context, qb models.TagNameFinder, s *models.ScrapedTag, stashBoxEndpoint string) error {
|
||||||
if s.StoredID != nil {
|
if s.StoredID != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,29 @@ func (_m *TagReaderWriter) FindAllDescendants(ctx context.Context, tagID int, ex
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindByAlias provides a mock function with given fields: ctx, alias, nocase
|
||||||
|
func (_m *TagReaderWriter) FindByAlias(ctx context.Context, alias string, nocase bool) (*models.Tag, error) {
|
||||||
|
ret := _m.Called(ctx, alias, nocase)
|
||||||
|
|
||||||
|
var r0 *models.Tag
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, bool) *models.Tag); ok {
|
||||||
|
r0 = rf(ctx, alias, nocase)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.Tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {
|
||||||
|
r1 = rf(ctx, alias, nocase)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// FindByChildTagID provides a mock function with given fields: ctx, childID
|
// FindByChildTagID provides a mock function with given fields: ctx, childID
|
||||||
func (_m *TagReaderWriter) FindByChildTagID(ctx context.Context, childID int) ([]*models.Tag, error) {
|
func (_m *TagReaderWriter) FindByChildTagID(ctx context.Context, childID int) ([]*models.Tag, error) {
|
||||||
ret := _m.Called(ctx, childID)
|
ret := _m.Called(ctx, childID)
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,16 @@ type TagGetter interface {
|
||||||
Find(ctx context.Context, id int) (*Tag, error)
|
Find(ctx context.Context, id int) (*Tag, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TagNameFinder interface {
|
||||||
|
FindByName(ctx context.Context, name string, nocase bool) (*Tag, error)
|
||||||
|
FindByNames(ctx context.Context, names []string, nocase bool) ([]*Tag, error)
|
||||||
|
FindByAlias(ctx context.Context, alias string, nocase bool) (*Tag, error)
|
||||||
|
}
|
||||||
|
|
||||||
// TagFinder provides methods to find tags.
|
// TagFinder provides methods to find tags.
|
||||||
type TagFinder interface {
|
type TagFinder interface {
|
||||||
TagGetter
|
TagGetter
|
||||||
|
TagNameFinder
|
||||||
FindAllAncestors(ctx context.Context, tagID int, excludeIDs []int) ([]*TagPath, error)
|
FindAllAncestors(ctx context.Context, tagID int, excludeIDs []int) ([]*TagPath, error)
|
||||||
FindAllDescendants(ctx context.Context, tagID int, excludeIDs []int) ([]*TagPath, error)
|
FindAllDescendants(ctx context.Context, tagID int, excludeIDs []int) ([]*TagPath, error)
|
||||||
FindByParentTagID(ctx context.Context, parentID int) ([]*Tag, error)
|
FindByParentTagID(ctx context.Context, parentID int) ([]*Tag, error)
|
||||||
|
|
@ -23,8 +30,6 @@ type TagFinder interface {
|
||||||
FindByGroupID(ctx context.Context, groupID int) ([]*Tag, error)
|
FindByGroupID(ctx context.Context, groupID int) ([]*Tag, error)
|
||||||
FindBySceneMarkerID(ctx context.Context, sceneMarkerID int) ([]*Tag, error)
|
FindBySceneMarkerID(ctx context.Context, sceneMarkerID int) ([]*Tag, error)
|
||||||
FindByStudioID(ctx context.Context, studioID int) ([]*Tag, error)
|
FindByStudioID(ctx context.Context, studioID int) ([]*Tag, error)
|
||||||
FindByName(ctx context.Context, name string, nocase bool) (*Tag, error)
|
|
||||||
FindByNames(ctx context.Context, names []string, nocase bool) ([]*Tag, error)
|
|
||||||
FindByStashID(ctx context.Context, stashID StashID) ([]*Tag, error)
|
FindByStashID(ctx context.Context, stashID StashID) ([]*Tag, error)
|
||||||
FindByStashIDStatus(ctx context.Context, hasStashID bool, stashboxEndpoint string) ([]*Tag, error)
|
FindByStashIDStatus(ctx context.Context, hasStashID bool, stashboxEndpoint string) ([]*Tag, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -456,7 +456,7 @@ type FilenameParserRepository struct {
|
||||||
Performer PerformerNamesFinder
|
Performer PerformerNamesFinder
|
||||||
Studio models.StudioQueryer
|
Studio models.StudioQueryer
|
||||||
Group GroupNameFinder
|
Group GroupNameFinder
|
||||||
Tag models.TagQueryer
|
Tag models.TagNameFinder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFilenameParserRepository(repo models.Repository) FilenameParserRepository {
|
func NewFilenameParserRepository(repo models.Repository) FilenameParserRepository {
|
||||||
|
|
@ -599,7 +599,7 @@ func (p *FilenameParser) queryGroup(ctx context.Context, qb GroupNameFinder, gro
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FilenameParser) queryTag(ctx context.Context, qb models.TagQueryer, tagName string) *models.Tag {
|
func (p *FilenameParser) queryTag(ctx context.Context, qb models.TagNameFinder, tagName string) *models.Tag {
|
||||||
// massage the tag name
|
// massage the tag name
|
||||||
tagName = delimiterRE.ReplaceAllString(tagName, " ")
|
tagName = delimiterRE.ReplaceAllString(tagName, " ")
|
||||||
|
|
||||||
|
|
@ -638,7 +638,7 @@ func (p *FilenameParser) setPerformers(ctx context.Context, qb PerformerNamesFin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FilenameParser) setTags(ctx context.Context, qb models.TagQueryer, h sceneHolder, result *models.SceneParserResult) {
|
func (p *FilenameParser) setTags(ctx context.Context, qb models.TagNameFinder, h sceneHolder, result *models.SceneParserResult) {
|
||||||
// query for each performer
|
// query for each performer
|
||||||
tagsSet := make(map[int]bool)
|
tagsSet := make(map[int]bool)
|
||||||
for _, tagName := range h.tags {
|
for _, tagName := range h.tags {
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ func (g Generator) generateConcatFile(chunkFiles []string) (fn string, err error
|
||||||
for _, f := range chunkFiles {
|
for _, f := range chunkFiles {
|
||||||
// files in concat file should be relative to concat
|
// files in concat file should be relative to concat
|
||||||
relFile := filepath.Base(f)
|
relFile := filepath.Base(f)
|
||||||
if _, err := w.WriteString(fmt.Sprintf("file '%s'\n", relFile)); err != nil {
|
if _, err := fmt.Fprintf(w, "file '%s'\n", relFile); err != nil {
|
||||||
return concatFile.Name(), fmt.Errorf("writing concat file: %w", err)
|
return concatFile.Name(), fmt.Errorf("writing concat file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,19 +57,19 @@ type ScanHandler struct {
|
||||||
|
|
||||||
func (h *ScanHandler) validate() error {
|
func (h *ScanHandler) validate() error {
|
||||||
if h.CreatorUpdater == nil {
|
if h.CreatorUpdater == nil {
|
||||||
return errors.New("CreatorUpdater is required")
|
return errors.New("internal error: CreatorUpdater is required")
|
||||||
}
|
}
|
||||||
if h.ScanGenerator == nil {
|
if h.ScanGenerator == nil {
|
||||||
return errors.New("ScanGenerator is required")
|
return errors.New("internal error: ScanGenerator is required")
|
||||||
}
|
}
|
||||||
if h.CaptionUpdater == nil {
|
if h.CaptionUpdater == nil {
|
||||||
return errors.New("CaptionUpdater is required")
|
return errors.New("internal error: CaptionUpdater is required")
|
||||||
}
|
}
|
||||||
if !h.FileNamingAlgorithm.IsValid() {
|
if !h.FileNamingAlgorithm.IsValid() {
|
||||||
return errors.New("FileNamingAlgorithm is required")
|
return errors.New("internal error: FileNamingAlgorithm is required")
|
||||||
}
|
}
|
||||||
if h.Paths == nil {
|
if h.Paths == nil {
|
||||||
return errors.New("Paths is required")
|
return errors.New("internal error: Paths is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ type StudioFinder interface {
|
||||||
|
|
||||||
type TagFinder interface {
|
type TagFinder interface {
|
||||||
models.TagGetter
|
models.TagGetter
|
||||||
|
models.TagNameFinder
|
||||||
models.TagAutoTagQueryer
|
models.TagAutoTagQueryer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/sliceutil"
|
"github.com/stashapp/stash/pkg/sliceutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func postProcessTags(ctx context.Context, tqb models.TagQueryer, scrapedTags []*models.ScrapedTag) (ret []*models.ScrapedTag, err error) {
|
func postProcessTags(ctx context.Context, tqb models.TagNameFinder, scrapedTags []*models.ScrapedTag) (ret []*models.ScrapedTag, err error) {
|
||||||
ret = make([]*models.ScrapedTag, 0, len(scrapedTags))
|
ret = make([]*models.ScrapedTag, 0, len(scrapedTags))
|
||||||
|
|
||||||
for _, t := range scrapedTags {
|
for _, t := range scrapedTags {
|
||||||
|
|
|
||||||
|
|
@ -70,11 +70,52 @@ func stringCriterionHandler(c *models.StringCriterionInput, column string) crite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinedStringCriterionHandler(c *models.StringCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func stringNoTrimCriterionHandler(c *models.StringCriterionInput, column string) criterionHandlerFunc {
|
||||||
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
|
if c != nil {
|
||||||
|
if modifier := c.Modifier; c.Modifier.IsValid() {
|
||||||
|
switch modifier {
|
||||||
|
case models.CriterionModifierIncludes:
|
||||||
|
f.whereClauses = append(f.whereClauses, getStringSearchClause([]string{column}, c.Value, false))
|
||||||
|
case models.CriterionModifierExcludes:
|
||||||
|
f.whereClauses = append(f.whereClauses, getStringSearchClause([]string{column}, c.Value, true))
|
||||||
|
case models.CriterionModifierEquals:
|
||||||
|
f.addWhere(column+" LIKE ?", c.Value)
|
||||||
|
case models.CriterionModifierNotEquals:
|
||||||
|
f.addWhere(column+" NOT LIKE ?", c.Value)
|
||||||
|
case models.CriterionModifierMatchesRegex:
|
||||||
|
if _, err := regexp.Compile(c.Value); err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.addWhere(fmt.Sprintf("(%s IS NOT NULL AND %[1]s regexp ?)", column), c.Value)
|
||||||
|
case models.CriterionModifierNotMatchesRegex:
|
||||||
|
if _, err := regexp.Compile(c.Value); err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.addWhere(fmt.Sprintf("(%s IS NULL OR %[1]s NOT regexp ?)", column), c.Value)
|
||||||
|
case models.CriterionModifierIsNull:
|
||||||
|
f.addWhere("(" + column + " IS NULL)")
|
||||||
|
case models.CriterionModifierNotNull:
|
||||||
|
f.addWhere("(" + column + " IS NOT NULL)")
|
||||||
|
default:
|
||||||
|
panic("unsupported string filter modifier")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinedStringCriterionHandler(c *models.StringCriterionInput, column string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if c != nil {
|
if c != nil {
|
||||||
if addJoinFn != nil {
|
if addJoinFn != nil {
|
||||||
addJoinFn(f)
|
joinType := joinTypeInner
|
||||||
|
if c.Modifier == models.CriterionModifierIsNull || c.Modifier == models.CriterionModifierNotMatchesRegex {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
addJoinFn(f, joinType)
|
||||||
}
|
}
|
||||||
stringCriterionHandler(c, column)(ctx, f)
|
stringCriterionHandler(c, column)(ctx, f)
|
||||||
}
|
}
|
||||||
|
|
@ -104,16 +145,20 @@ func enumCriterionHandler(modifier models.CriterionModifier, values []string, co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathCriterionHandler(c *models.StringCriterionInput, pathColumn string, basenameColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func pathCriterionHandler(c *models.StringCriterionInput, pathColumn string, basenameColumn string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if c != nil {
|
if c != nil {
|
||||||
if addJoinFn != nil {
|
|
||||||
addJoinFn(f)
|
|
||||||
}
|
|
||||||
addWildcards := true
|
|
||||||
not := false
|
|
||||||
|
|
||||||
if modifier := c.Modifier; c.Modifier.IsValid() {
|
if modifier := c.Modifier; c.Modifier.IsValid() {
|
||||||
|
if addJoinFn != nil {
|
||||||
|
joinType := joinTypeInner
|
||||||
|
if modifier == models.CriterionModifierIsNull || modifier == models.CriterionModifierNotMatchesRegex {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
addJoinFn(f, joinType)
|
||||||
|
}
|
||||||
|
addWildcards := true
|
||||||
|
not := false
|
||||||
|
|
||||||
switch modifier {
|
switch modifier {
|
||||||
case models.CriterionModifierIncludes:
|
case models.CriterionModifierIncludes:
|
||||||
f.whereClauses = append(f.whereClauses, getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not))
|
f.whereClauses = append(f.whereClauses, getPathSearchClauseMany(pathColumn, basenameColumn, c.Value, addWildcards, not))
|
||||||
|
|
@ -194,11 +239,15 @@ func getPathSearchClauseMany(pathColumn, basenameColumn, p string, addWildcards,
|
||||||
return getPathSearchClause(pathColumn, basenameColumn, trimmedQuery, addWildcards, not)
|
return getPathSearchClause(pathColumn, basenameColumn, trimmedQuery, addWildcards, not)
|
||||||
}
|
}
|
||||||
|
|
||||||
func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if c != nil {
|
if c != nil {
|
||||||
if addJoinFn != nil {
|
if addJoinFn != nil {
|
||||||
addJoinFn(f)
|
joinType := joinTypeInner
|
||||||
|
if c.Modifier == models.CriterionModifierIsNull {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
addJoinFn(f, joinType)
|
||||||
}
|
}
|
||||||
clause, args := getIntCriterionWhereClause(column, *c)
|
clause, args := getIntCriterionWhereClause(column, *c)
|
||||||
f.addWhere(clause, args...)
|
f.addWhere(clause, args...)
|
||||||
|
|
@ -206,11 +255,15 @@ func intCriterionHandler(c *models.IntCriterionInput, column string, addJoinFn f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func floatCriterionHandler(c *models.FloatCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func floatCriterionHandler(c *models.FloatCriterionInput, column string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if c != nil {
|
if c != nil {
|
||||||
if addJoinFn != nil {
|
if addJoinFn != nil {
|
||||||
addJoinFn(f)
|
joinType := joinTypeInner
|
||||||
|
if c.Modifier == models.CriterionModifierIsNull {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
addJoinFn(f, joinType)
|
||||||
}
|
}
|
||||||
clause, args := getFloatCriterionWhereClause(column, *c)
|
clause, args := getFloatCriterionWhereClause(column, *c)
|
||||||
f.addWhere(clause, args...)
|
f.addWhere(clause, args...)
|
||||||
|
|
@ -218,11 +271,15 @@ func floatCriterionHandler(c *models.FloatCriterionInput, column string, addJoin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func floatIntCriterionHandler(durationFilter *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func floatIntCriterionHandler(durationFilter *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if durationFilter != nil {
|
if durationFilter != nil {
|
||||||
if addJoinFn != nil {
|
if addJoinFn != nil {
|
||||||
addJoinFn(f)
|
joinType := joinTypeInner
|
||||||
|
if durationFilter.Modifier == models.CriterionModifierIsNull {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
addJoinFn(f, joinType)
|
||||||
}
|
}
|
||||||
clause, args := getIntCriterionWhereClause("cast("+column+" as int)", *durationFilter)
|
clause, args := getIntCriterionWhereClause("cast("+column+" as int)", *durationFilter)
|
||||||
f.addWhere(clause, args...)
|
f.addWhere(clause, args...)
|
||||||
|
|
@ -230,11 +287,11 @@ func floatIntCriterionHandler(durationFilter *models.IntCriterionInput, column s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func boolCriterionHandler(c *bool, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func boolCriterionHandler(c *bool, column string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if c != nil {
|
if c != nil {
|
||||||
if addJoinFn != nil {
|
if addJoinFn != nil {
|
||||||
addJoinFn(f)
|
addJoinFn(f, joinTypeInner)
|
||||||
}
|
}
|
||||||
var v string
|
var v string
|
||||||
if *c {
|
if *c {
|
||||||
|
|
@ -289,11 +346,11 @@ func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) crit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if resolution != nil && resolution.Value.IsValid() {
|
if resolution != nil && resolution.Value.IsValid() {
|
||||||
if addJoinFn != nil {
|
if addJoinFn != nil {
|
||||||
addJoinFn(f)
|
addJoinFn(f, joinTypeInner)
|
||||||
}
|
}
|
||||||
|
|
||||||
mn := resolution.Value.GetMinResolution()
|
mn := resolution.Value.GetMinResolution()
|
||||||
|
|
@ -315,11 +372,11 @@ func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, hei
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func orientationCriterionHandler(orientation *models.OrientationCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func orientationCriterionHandler(orientation *models.OrientationCriterionInput, heightColumn string, widthColumn string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if orientation != nil {
|
if orientation != nil {
|
||||||
if addJoinFn != nil {
|
if addJoinFn != nil {
|
||||||
addJoinFn(f)
|
addJoinFn(f, joinTypeInner)
|
||||||
}
|
}
|
||||||
|
|
||||||
var clauses []sqlClause
|
var clauses []sqlClause
|
||||||
|
|
@ -362,7 +419,7 @@ type joinedMultiCriterionHandlerBuilder struct {
|
||||||
// foreign key of the foreign object on the join table
|
// foreign key of the foreign object on the join table
|
||||||
foreignFK string
|
foreignFK string
|
||||||
|
|
||||||
addJoinTable func(f *filterBuilder)
|
addJoinTable func(f *filterBuilder, joinType joinType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInput) criterionHandlerFunc {
|
func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
|
@ -378,11 +435,13 @@ func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInp
|
||||||
|
|
||||||
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
var notClause string
|
var notClause string
|
||||||
|
joinType := joinTypeLeft
|
||||||
if criterion.Modifier == models.CriterionModifierNotNull {
|
if criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
notClause = "NOT"
|
notClause = "NOT"
|
||||||
|
joinType = joinTypeInner
|
||||||
}
|
}
|
||||||
|
|
||||||
m.addJoinTable(f)
|
m.addJoinTable(f, joinType)
|
||||||
|
|
||||||
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
|
f.addWhere(utils.StrFormat("{table}.{column} IS {not} NULL", utils.StrFormatMap{
|
||||||
"table": joinAlias,
|
"table": joinAlias,
|
||||||
|
|
@ -415,11 +474,11 @@ func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInp
|
||||||
switch criterion.Modifier {
|
switch criterion.Modifier {
|
||||||
case models.CriterionModifierIncludes:
|
case models.CriterionModifierIncludes:
|
||||||
// includes any of the provided ids
|
// includes any of the provided ids
|
||||||
m.addJoinTable(f)
|
m.addJoinTable(f, joinTypeInner)
|
||||||
whereClause = fmt.Sprintf("%s.%s IN %s", joinAlias, m.foreignFK, getInBinding(len(criterion.Value)))
|
whereClause = fmt.Sprintf("%s.%s IN %s", joinAlias, m.foreignFK, getInBinding(len(criterion.Value)))
|
||||||
case models.CriterionModifierEquals:
|
case models.CriterionModifierEquals:
|
||||||
// includes only the provided ids
|
// includes only the provided ids
|
||||||
m.addJoinTable(f)
|
m.addJoinTable(f, joinTypeInner)
|
||||||
whereClause = utils.StrFormat("{joinAlias}.{foreignFK} IN {inBinding} AND (SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.id) = ?", utils.StrFormatMap{
|
whereClause = utils.StrFormat("{joinAlias}.{foreignFK} IN {inBinding} AND (SELECT COUNT(*) FROM {joinTable} s WHERE s.{primaryFK} = {primaryTable}.id) = ?", utils.StrFormatMap{
|
||||||
"joinAlias": joinAlias,
|
"joinAlias": joinAlias,
|
||||||
"foreignFK": m.foreignFK,
|
"foreignFK": m.foreignFK,
|
||||||
|
|
@ -434,7 +493,7 @@ func (m *joinedMultiCriterionHandlerBuilder) handler(c *models.MultiCriterionInp
|
||||||
f.setError(fmt.Errorf("not equals modifier is not supported for multi criterion input"))
|
f.setError(fmt.Errorf("not equals modifier is not supported for multi criterion input"))
|
||||||
case models.CriterionModifierIncludesAll:
|
case models.CriterionModifierIncludesAll:
|
||||||
// includes all of the provided ids
|
// includes all of the provided ids
|
||||||
m.addJoinTable(f)
|
m.addJoinTable(f, joinTypeInner)
|
||||||
whereClause = fmt.Sprintf("%s.%s IN %s", joinAlias, m.foreignFK, getInBinding(len(criterion.Value)))
|
whereClause = fmt.Sprintf("%s.%s IN %s", joinAlias, m.foreignFK, getInBinding(len(criterion.Value)))
|
||||||
havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value))
|
havingClause = fmt.Sprintf("count(distinct %s.%s) IS %d", joinAlias, m.foreignFK, len(criterion.Value))
|
||||||
}
|
}
|
||||||
|
|
@ -468,7 +527,7 @@ type multiCriterionHandlerBuilder struct {
|
||||||
foreignFK string
|
foreignFK string
|
||||||
|
|
||||||
// function that will be called to perform any necessary joins
|
// function that will be called to perform any necessary joins
|
||||||
addJoinsFunc func(f *filterBuilder)
|
addJoinsFunc func(f *filterBuilder, joinType joinType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
|
func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
|
@ -500,7 +559,7 @@ func (m *multiCriterionHandlerBuilder) handler(criterion *models.MultiCriterionI
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.addJoinsFunc != nil {
|
if m.addJoinsFunc != nil {
|
||||||
m.addJoinsFunc(f)
|
m.addJoinsFunc(f, joinTypeInner)
|
||||||
}
|
}
|
||||||
|
|
||||||
whereClause, havingClause := getMultiCriterionClause(m.primaryTable, m.foreignTable, m.joinTable, m.primaryFK, m.foreignFK, criterion)
|
whereClause, havingClause := getMultiCriterionClause(m.primaryTable, m.foreignTable, m.joinTable, m.primaryFK, m.foreignFK, criterion)
|
||||||
|
|
@ -536,7 +595,7 @@ type stringListCriterionHandlerBuilder struct {
|
||||||
// string field on the join table
|
// string field on the join table
|
||||||
stringColumn string
|
stringColumn string
|
||||||
|
|
||||||
addJoinTable func(f *filterBuilder)
|
addJoinTable func(f *filterBuilder, joinType joinType)
|
||||||
excludeHandler func(f *filterBuilder, criterion *models.StringCriterionInput)
|
excludeHandler func(f *filterBuilder, criterion *models.StringCriterionInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -570,7 +629,11 @@ func (m *stringListCriterionHandlerBuilder) handler(criterion *models.StringCrit
|
||||||
// Modifier: models.CriterionModifierNotNull,
|
// Modifier: models.CriterionModifierNotNull,
|
||||||
// }, m.joinTable+"."+m.stringColumn)(ctx, f)
|
// }, m.joinTable+"."+m.stringColumn)(ctx, f)
|
||||||
} else {
|
} else {
|
||||||
m.addJoinTable(f)
|
joinType := joinTypeInner
|
||||||
|
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotMatchesRegex {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
m.addJoinTable(f, joinType)
|
||||||
stringCriterionHandler(criterion, m.joinTable+"."+m.stringColumn)(ctx, f)
|
stringCriterionHandler(criterion, m.joinTable+"."+m.stringColumn)(ctx, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1028,14 +1091,18 @@ func (h *stashIDCriterionHandler) handle(ctx context.Context, f *filterBuilder)
|
||||||
joinClause += fmt.Sprintf(" AND %s.endpoint = '%s'", t, *h.c.Endpoint)
|
joinClause += fmt.Sprintf(" AND %s.endpoint = '%s'", t, *h.c.Endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.addLeftJoin(stashIDRepo.tableName, h.stashIDTableAs, joinClause)
|
joinType := joinTypeInner
|
||||||
|
if h.c.Modifier == models.CriterionModifierIsNull || h.c.Modifier == models.CriterionModifierNotMatchesRegex {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
f.addJoin(joinType, stashIDRepo.tableName, h.stashIDTableAs, joinClause)
|
||||||
|
|
||||||
v := ""
|
v := ""
|
||||||
if h.c.StashID != nil {
|
if h.c.StashID != nil {
|
||||||
v = *h.c.StashID
|
v = *h.c.StashID
|
||||||
}
|
}
|
||||||
|
|
||||||
stringCriterionHandler(&models.StringCriterionInput{
|
stringNoTrimCriterionHandler(&models.StringCriterionInput{
|
||||||
Value: v,
|
Value: v,
|
||||||
Modifier: h.c.Modifier,
|
Modifier: h.c.Modifier,
|
||||||
}, t+".stash_id")(ctx, f)
|
}, t+".stash_id")(ctx, f)
|
||||||
|
|
@ -1064,7 +1131,12 @@ func (h *stashIDsCriterionHandler) handle(ctx context.Context, f *filterBuilder)
|
||||||
joinClause += fmt.Sprintf(" AND %s.endpoint = '%s'", t, *h.c.Endpoint)
|
joinClause += fmt.Sprintf(" AND %s.endpoint = '%s'", t, *h.c.Endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.addLeftJoin(stashIDRepo.tableName, h.stashIDTableAs, joinClause)
|
joinType := joinTypeInner
|
||||||
|
if h.c.Modifier == models.CriterionModifierIsNull {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
f.addJoin(joinType, stashIDRepo.tableName, h.stashIDTableAs, joinClause)
|
||||||
|
|
||||||
switch h.c.Modifier {
|
switch h.c.Modifier {
|
||||||
case models.CriterionModifierIsNull:
|
case models.CriterionModifierIsNull:
|
||||||
|
|
|
||||||
|
|
@ -300,15 +300,19 @@ func (qb *videoFileFilterHandler) criterionHandler() criterionHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *videoFileFilterHandler) addVideoFilesTable(f *filterBuilder) {
|
func (qb *videoFileFilterHandler) addVideoFilesTable(f *filterBuilder, joinType joinType) {
|
||||||
f.addLeftJoin(videoFileTable, "", "video_files.file_id = files.id")
|
f.addJoin(joinType, videoFileTable, "", "video_files.file_id = files.id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *videoFileFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func (qb *videoFileFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if codec != nil {
|
if codec != nil {
|
||||||
if addJoinFn != nil {
|
if addJoinFn != nil {
|
||||||
addJoinFn(f)
|
joinType := joinTypeInner
|
||||||
|
if codec.Modifier == models.CriterionModifierIsNull || codec.Modifier == models.CriterionModifierNotMatchesRegex {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
addJoinFn(f, joinType)
|
||||||
}
|
}
|
||||||
|
|
||||||
stringCriterionHandler(codec, codecColumn)(ctx, f)
|
stringCriterionHandler(codec, codecColumn)(ctx, f)
|
||||||
|
|
@ -322,8 +326,8 @@ func (qb *videoFileFilterHandler) captionCriterionHandler(captions *models.Strin
|
||||||
primaryFK: sceneIDColumn,
|
primaryFK: sceneIDColumn,
|
||||||
joinTable: videoCaptionsTable,
|
joinTable: videoCaptionsTable,
|
||||||
stringColumn: captionCodeColumn,
|
stringColumn: captionCodeColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
f.addLeftJoin(videoCaptionsTable, "", "video_captions.file_id = files.id")
|
f.addJoin(joinType, videoCaptionsTable, "", "video_captions.file_id = files.id")
|
||||||
},
|
},
|
||||||
excludeHandler: func(f *filterBuilder, criterion *models.StringCriterionInput) {
|
excludeHandler: func(f *filterBuilder, criterion *models.StringCriterionInput) {
|
||||||
excludeClause := `files.id NOT IN (
|
excludeClause := `files.id NOT IN (
|
||||||
|
|
@ -361,6 +365,6 @@ func (qb *imageFileFilterHandler) criterionHandler() criterionHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *imageFileFilterHandler) addImageFilesTable(f *filterBuilder) {
|
func (qb *imageFileFilterHandler) addImageFilesTable(f *filterBuilder, joinType joinType) {
|
||||||
f.addLeftJoin(imageFileTable, "", "image_files.file_id = files.id")
|
f.addJoin(joinType, imageFileTable, "", "image_files.file_id = files.id")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,18 @@ func andClauses(clauses ...sqlClause) sqlClause {
|
||||||
return joinClauses("AND", clauses...)
|
return joinClauses("AND", clauses...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type joinType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
joinTypeLeft joinType = "LEFT"
|
||||||
|
joinTypeInner joinType = "INNER"
|
||||||
|
)
|
||||||
|
|
||||||
type join struct {
|
type join struct {
|
||||||
table string
|
table string
|
||||||
as string
|
as string
|
||||||
onClause string
|
onClause string
|
||||||
joinType string
|
joinType joinType
|
||||||
args []interface{}
|
args []interface{}
|
||||||
|
|
||||||
// if true, indicates this is required for sorting only
|
// if true, indicates this is required for sorting only
|
||||||
|
|
@ -115,15 +122,19 @@ func (j join) alias() string {
|
||||||
return j.as
|
return j.as
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j join) getJoinType() joinType {
|
||||||
|
if j.joinType == "" {
|
||||||
|
return joinTypeLeft
|
||||||
|
}
|
||||||
|
return j.joinType
|
||||||
|
}
|
||||||
|
|
||||||
func (j join) toSQL() string {
|
func (j join) toSQL() string {
|
||||||
asStr := ""
|
asStr := ""
|
||||||
joinStr := j.joinType
|
joinStr := j.getJoinType()
|
||||||
if j.as != "" && j.as != j.table {
|
if j.as != "" && j.as != j.table {
|
||||||
asStr = " AS " + j.as
|
asStr = " AS " + j.as
|
||||||
}
|
}
|
||||||
if j.joinType == "" {
|
|
||||||
joinStr = "LEFT"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s JOIN %s%s ON %s", joinStr, j.table, asStr, j.onClause)
|
return fmt.Sprintf("%s JOIN %s%s ON %s", joinStr, j.table, asStr, j.onClause)
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +152,12 @@ func (j *joins) addUnique(newJoin join) bool {
|
||||||
if !newJoin.sort && jj.sort {
|
if !newJoin.sort && jj.sort {
|
||||||
(*j)[i].sort = false
|
(*j)[i].sort = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the new join is inner, override existing left join
|
||||||
|
if newJoin.getJoinType() == joinTypeInner && jj.getJoinType() == joinTypeLeft {
|
||||||
|
(*j)[i].joinType = joinTypeInner
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -243,6 +260,23 @@ func (f *filterBuilder) not(n *filterBuilder) {
|
||||||
f.subFilterOp = notOp
|
f.subFilterOp = notOp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addJoin adds a join to the filter. The join is expressed in SQL as:
|
||||||
|
// <joinType> JOIN <table> [AS <as>] ON <onClause>
|
||||||
|
// The AS is omitted if as is empty.
|
||||||
|
// This method does not add a join if it its alias/table name is already
|
||||||
|
// present in another existing join.
|
||||||
|
func (f *filterBuilder) addJoin(joinType joinType, table, as, onClause string, args ...interface{}) {
|
||||||
|
newJoin := join{
|
||||||
|
table: table,
|
||||||
|
as: as,
|
||||||
|
onClause: onClause,
|
||||||
|
joinType: joinType,
|
||||||
|
args: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
f.joins.add(newJoin)
|
||||||
|
}
|
||||||
|
|
||||||
// addLeftJoin adds a left join to the filter. The join is expressed in SQL as:
|
// addLeftJoin adds a left join to the filter. The join is expressed in SQL as:
|
||||||
// LEFT JOIN <table> [AS <as>] ON <onClause>
|
// LEFT JOIN <table> [AS <as>] ON <onClause>
|
||||||
// The AS is omitted if as is empty.
|
// The AS is omitted if as is empty.
|
||||||
|
|
@ -253,7 +287,7 @@ func (f *filterBuilder) addLeftJoin(table, as, onClause string, args ...interfac
|
||||||
table: table,
|
table: table,
|
||||||
as: as,
|
as: as,
|
||||||
onClause: onClause,
|
onClause: onClause,
|
||||||
joinType: "LEFT",
|
joinType: joinTypeLeft,
|
||||||
args: args,
|
args: args,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +304,7 @@ func (f *filterBuilder) addInnerJoin(table, as, onClause string, args ...interfa
|
||||||
table: table,
|
table: table,
|
||||||
as: as,
|
as: as,
|
||||||
onClause: onClause,
|
onClause: onClause,
|
||||||
joinType: "INNER",
|
joinType: joinTypeInner,
|
||||||
args: args,
|
args: args,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,15 +193,15 @@ func (qb *galleryFilterHandler) urlsCriterionHandler(url *models.StringCriterion
|
||||||
primaryFK: galleryIDColumn,
|
primaryFK: galleryIDColumn,
|
||||||
joinTable: galleriesURLsTable,
|
joinTable: galleriesURLsTable,
|
||||||
stringColumn: galleriesURLColumn,
|
stringColumn: galleriesURLColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
galleriesURLsTableMgr.join(f, "", "galleries.id")
|
galleriesURLsTableMgr.join(f, joinType, "", "galleries.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.handler(url)
|
return h.handler(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *galleryFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
func (qb *galleryFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder, joinType joinType)) multiCriterionHandlerBuilder {
|
||||||
return multiCriterionHandlerBuilder{
|
return multiCriterionHandlerBuilder{
|
||||||
primaryTable: galleryTable,
|
primaryTable: galleryTable,
|
||||||
foreignTable: foreignTable,
|
foreignTable: foreignTable,
|
||||||
|
|
@ -353,7 +353,7 @@ func (qb *galleryFilterHandler) missingCriterionHandler(isMissing *string) crite
|
||||||
if isMissing != nil && *isMissing != "" {
|
if isMissing != nil && *isMissing != "" {
|
||||||
switch *isMissing {
|
switch *isMissing {
|
||||||
case "url":
|
case "url":
|
||||||
galleriesURLsTableMgr.join(f, "", "galleries.id")
|
galleriesURLsTableMgr.leftJoin(f, "", "galleries.id")
|
||||||
f.addWhere("gallery_urls.url IS NULL")
|
f.addWhere("gallery_urls.url IS NULL")
|
||||||
case "scenes":
|
case "scenes":
|
||||||
f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
|
f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
|
||||||
|
|
@ -361,12 +361,12 @@ func (qb *galleryFilterHandler) missingCriterionHandler(isMissing *string) crite
|
||||||
case "studio":
|
case "studio":
|
||||||
f.addWhere("galleries.studio_id IS NULL")
|
f.addWhere("galleries.studio_id IS NULL")
|
||||||
case "performers":
|
case "performers":
|
||||||
galleryRepository.performers.join(f, "performers_join", "galleries.id")
|
galleryRepository.performers.leftJoin(f, "performers_join", "galleries.id")
|
||||||
f.addWhere("performers_join.gallery_id IS NULL")
|
f.addWhere("performers_join.gallery_id IS NULL")
|
||||||
case "date":
|
case "date":
|
||||||
f.addWhere("galleries.date IS NULL OR galleries.date IS \"\"")
|
f.addWhere("galleries.date IS NULL OR galleries.date IS \"\"")
|
||||||
case "tags":
|
case "tags":
|
||||||
galleryRepository.tags.join(f, "tags_join", "galleries.id")
|
galleryRepository.tags.leftJoin(f, "tags_join", "galleries.id")
|
||||||
f.addWhere("tags_join.gallery_id IS NULL")
|
f.addWhere("tags_join.gallery_id IS NULL")
|
||||||
case "cover":
|
case "cover":
|
||||||
f.addLeftJoin("galleries_images", "cover_join", "cover_join.gallery_id = galleries.id AND cover_join.cover = 1")
|
f.addLeftJoin("galleries_images", "cover_join", "cover_join.gallery_id = galleries.id AND cover_join.cover = 1")
|
||||||
|
|
@ -410,9 +410,9 @@ func (qb *galleryFilterHandler) tagCountCriterionHandler(tagCount *models.IntCri
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *galleryFilterHandler) scenesCriterionHandler(scenes *models.MultiCriterionInput) criterionHandlerFunc {
|
func (qb *galleryFilterHandler) scenesCriterionHandler(scenes *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
addJoinsFunc := func(f *filterBuilder, joinType joinType) {
|
||||||
galleryRepository.scenes.join(f, "", "galleries.id")
|
galleryRepository.scenes.join(f, joinType, "", "galleries.id")
|
||||||
f.addLeftJoin("scenes", "", "scenes_galleries.scene_id = scenes.id")
|
f.addJoin(joinType, "scenes", "", "scenes_galleries.scene_id = scenes.id")
|
||||||
}
|
}
|
||||||
h := qb.getMultiCriterionHandlerBuilder(sceneTable, galleriesScenesTable, "scene_id", addJoinsFunc)
|
h := qb.getMultiCriterionHandlerBuilder(sceneTable, galleriesScenesTable, "scene_id", addJoinsFunc)
|
||||||
return h.handler(scenes)
|
return h.handler(scenes)
|
||||||
|
|
@ -426,8 +426,8 @@ func (qb *galleryFilterHandler) performersCriterionHandler(performers *models.Mu
|
||||||
primaryFK: galleryIDColumn,
|
primaryFK: galleryIDColumn,
|
||||||
foreignFK: performerIDColumn,
|
foreignFK: performerIDColumn,
|
||||||
|
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
galleryRepository.performers.join(f, "performers_join", "galleries.id")
|
galleryRepository.performers.join(f, joinType, "performers_join", "galleries.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -515,7 +515,7 @@ func (qb *galleryFilterHandler) performerAgeCriterionHandler(performerAge *model
|
||||||
func (qb *galleryFilterHandler) averageResolutionCriterionHandler(resolution *models.ResolutionCriterionInput) criterionHandlerFunc {
|
func (qb *galleryFilterHandler) averageResolutionCriterionHandler(resolution *models.ResolutionCriterionInput) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if resolution != nil && resolution.Value.IsValid() {
|
if resolution != nil && resolution.Value.IsValid() {
|
||||||
galleryRepository.images.join(f, "images_join", "galleries.id")
|
galleryRepository.images.leftJoin(f, "images_join", "galleries.id")
|
||||||
f.addLeftJoin("images", "", "images_join.image_id = images.id")
|
f.addLeftJoin("images", "", "images_join.image_id = images.id")
|
||||||
f.addLeftJoin("images_files", "", "images.id = images_files.image_id")
|
f.addLeftJoin("images_files", "", "images.id = images_files.image_id")
|
||||||
f.addLeftJoin("image_files", "", "images_files.file_id = image_files.file_id")
|
f.addLeftJoin("image_files", "", "images_files.file_id = image_files.file_id")
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ func (qb *groupFilterHandler) missingCriterionHandler(isMissing *string) criteri
|
||||||
f.addLeftJoin("groups_scenes", "", "groups_scenes.group_id = groups.id")
|
f.addLeftJoin("groups_scenes", "", "groups_scenes.group_id = groups.id")
|
||||||
f.addWhere("groups_scenes.scene_id IS NULL")
|
f.addWhere("groups_scenes.scene_id IS NULL")
|
||||||
case "url":
|
case "url":
|
||||||
groupsURLsTableMgr.join(f, "", "groups.id")
|
groupsURLsTableMgr.leftJoin(f, "", "groups.id")
|
||||||
f.addWhere("group_urls.url IS NULL")
|
f.addWhere("group_urls.url IS NULL")
|
||||||
case "studio":
|
case "studio":
|
||||||
f.addWhere("groups.studio_id IS NULL")
|
f.addWhere("groups.studio_id IS NULL")
|
||||||
|
|
@ -129,7 +129,7 @@ func (qb *groupFilterHandler) missingCriterionHandler(isMissing *string) criteri
|
||||||
f.addLeftJoin("performers_scenes", "ps_perf", "gs_perf.scene_id = ps_perf.scene_id")
|
f.addLeftJoin("performers_scenes", "ps_perf", "gs_perf.scene_id = ps_perf.scene_id")
|
||||||
f.addWhere("ps_perf.performer_id IS NULL")
|
f.addWhere("ps_perf.performer_id IS NULL")
|
||||||
case "tags":
|
case "tags":
|
||||||
groupRepository.tags.join(f, "tags_join", "groups.id")
|
groupRepository.tags.leftJoin(f, "tags_join", "groups.id")
|
||||||
f.addWhere("tags_join.group_id IS NULL")
|
f.addWhere("tags_join.group_id IS NULL")
|
||||||
default:
|
default:
|
||||||
if err := validateIsMissing(*isMissing, []string{
|
if err := validateIsMissing(*isMissing, []string{
|
||||||
|
|
@ -150,8 +150,8 @@ func (qb *groupFilterHandler) urlsCriterionHandler(url *models.StringCriterionIn
|
||||||
primaryFK: groupIDColumn,
|
primaryFK: groupIDColumn,
|
||||||
joinTable: groupURLsTable,
|
joinTable: groupURLsTable,
|
||||||
stringColumn: groupURLColumn,
|
stringColumn: groupURLColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
groupsURLsTableMgr.join(f, "", "groups.id")
|
groupsURLsTableMgr.join(f, joinType, "", "groups.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,23 +123,23 @@ type imageRepositoryType struct {
|
||||||
files filesRepository
|
files filesRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *imageRepositoryType) addImagesFilesTable(f *filterBuilder) {
|
func (r *imageRepositoryType) addImagesFilesTable(f *filterBuilder, joinType joinType) {
|
||||||
f.addLeftJoin(imagesFilesTable, "", "images_files.image_id = images.id")
|
f.addJoin(joinType, imagesFilesTable, "", "images_files.image_id = images.id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *imageRepositoryType) addFilesTable(f *filterBuilder) {
|
func (r *imageRepositoryType) addFilesTable(f *filterBuilder, joinType joinType) {
|
||||||
r.addImagesFilesTable(f)
|
r.addImagesFilesTable(f, joinType)
|
||||||
f.addLeftJoin(fileTable, "", "images_files.file_id = files.id")
|
f.addJoin(joinType, fileTable, "", "images_files.file_id = files.id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *imageRepositoryType) addFoldersTable(f *filterBuilder) {
|
func (r *imageRepositoryType) addFoldersTable(f *filterBuilder, joinType joinType) {
|
||||||
r.addFilesTable(f)
|
r.addFilesTable(f, joinType)
|
||||||
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
|
f.addJoin(joinType, folderTable, "", "files.parent_folder_id = folders.id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *imageRepositoryType) addImageFilesTable(f *filterBuilder) {
|
func (r *imageRepositoryType) addImageFilesTable(f *filterBuilder, joinType joinType) {
|
||||||
r.addImagesFilesTable(f)
|
r.addImagesFilesTable(f, joinType)
|
||||||
f.addLeftJoin(imageFileTable, "", "image_files.file_id = images_files.file_id")
|
f.addJoin(joinType, imageFileTable, "", "image_files.file_id = images_files.file_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,12 @@ func (qb *imageFilterHandler) criterionHandler() criterionHandler {
|
||||||
intCriterionHandler(imageFilter.ID, "images.id", nil),
|
intCriterionHandler(imageFilter.ID, "images.id", nil),
|
||||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||||
if imageFilter.Checksum != nil {
|
if imageFilter.Checksum != nil {
|
||||||
imageRepository.addImagesFilesTable(f)
|
joinType := joinTypeInner
|
||||||
f.addInnerJoin(fingerprintTable, "fingerprints_md5", "images_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
|
if imageFilter.Checksum.Modifier == models.CriterionModifierIsNull || imageFilter.Checksum.Modifier == models.CriterionModifierNotMatchesRegex {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
imageRepository.addImagesFilesTable(f, joinType)
|
||||||
|
f.addJoin(joinType, fingerprintTable, "fingerprints_md5", "images_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
|
||||||
}
|
}
|
||||||
|
|
||||||
stringCriterionHandler(imageFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
stringCriterionHandler(imageFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
||||||
|
|
@ -65,8 +69,8 @@ func (qb *imageFilterHandler) criterionHandler() criterionHandler {
|
||||||
|
|
||||||
&phashDistanceCriterionHandler{
|
&phashDistanceCriterionHandler{
|
||||||
joinFn: func(f *filterBuilder) {
|
joinFn: func(f *filterBuilder) {
|
||||||
imageRepository.addImagesFilesTable(f)
|
imageRepository.addImagesFilesTable(f, joinTypeInner)
|
||||||
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "images_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
f.addInnerJoin(fingerprintTable, "fingerprints_phash", "images_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
||||||
},
|
},
|
||||||
criterion: imageFilter.PhashDistance,
|
criterion: imageFilter.PhashDistance,
|
||||||
},
|
},
|
||||||
|
|
@ -148,8 +152,8 @@ func (qb *imageFilterHandler) criterionHandler() criterionHandler {
|
||||||
isRelated: true,
|
isRelated: true,
|
||||||
},
|
},
|
||||||
joinFn: func(f *filterBuilder) {
|
joinFn: func(f *filterBuilder) {
|
||||||
imageRepository.addFilesTable(f)
|
imageRepository.addFilesTable(f, joinTypeInner)
|
||||||
imageRepository.addFoldersTable(f)
|
imageRepository.addFoldersTable(f, joinTypeInner)
|
||||||
},
|
},
|
||||||
// don't use a subquery; join directly
|
// don't use a subquery; join directly
|
||||||
directJoin: true,
|
directJoin: true,
|
||||||
|
|
@ -172,18 +176,18 @@ func (qb *imageFilterHandler) missingCriterionHandler(isMissing *string) criteri
|
||||||
if isMissing != nil && *isMissing != "" {
|
if isMissing != nil && *isMissing != "" {
|
||||||
switch *isMissing {
|
switch *isMissing {
|
||||||
case "url":
|
case "url":
|
||||||
imagesURLsTableMgr.join(f, "", "images.id")
|
imagesURLsTableMgr.leftJoin(f, "", "images.id")
|
||||||
f.addWhere("image_urls.url IS NULL")
|
f.addWhere("image_urls.url IS NULL")
|
||||||
case "studio":
|
case "studio":
|
||||||
f.addWhere("images.studio_id IS NULL")
|
f.addWhere("images.studio_id IS NULL")
|
||||||
case "performers":
|
case "performers":
|
||||||
imageRepository.performers.join(f, "performers_join", "images.id")
|
imageRepository.performers.leftJoin(f, "performers_join", "images.id")
|
||||||
f.addWhere("performers_join.image_id IS NULL")
|
f.addWhere("performers_join.image_id IS NULL")
|
||||||
case "galleries":
|
case "galleries":
|
||||||
imageRepository.galleries.join(f, "galleries_join", "images.id")
|
imageRepository.galleries.leftJoin(f, "galleries_join", "images.id")
|
||||||
f.addWhere("galleries_join.image_id IS NULL")
|
f.addWhere("galleries_join.image_id IS NULL")
|
||||||
case "tags":
|
case "tags":
|
||||||
imageRepository.tags.join(f, "tags_join", "images.id")
|
imageRepository.tags.leftJoin(f, "tags_join", "images.id")
|
||||||
f.addWhere("tags_join.image_id IS NULL")
|
f.addWhere("tags_join.image_id IS NULL")
|
||||||
default:
|
default:
|
||||||
if err := validateIsMissing(*isMissing, []string{
|
if err := validateIsMissing(*isMissing, []string{
|
||||||
|
|
@ -204,15 +208,15 @@ func (qb *imageFilterHandler) urlsCriterionHandler(url *models.StringCriterionIn
|
||||||
primaryFK: imageIDColumn,
|
primaryFK: imageIDColumn,
|
||||||
joinTable: imagesURLsTable,
|
joinTable: imagesURLsTable,
|
||||||
stringColumn: imageURLColumn,
|
stringColumn: imageURLColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
imagesURLsTableMgr.join(f, "", "images.id")
|
imagesURLsTableMgr.join(f, joinType, "", "images.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.handler(url)
|
return h.handler(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *imageFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
func (qb *imageFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder, joinType joinType)) multiCriterionHandlerBuilder {
|
||||||
return multiCriterionHandlerBuilder{
|
return multiCriterionHandlerBuilder{
|
||||||
primaryTable: imageTable,
|
primaryTable: imageTable,
|
||||||
foreignTable: foreignTable,
|
foreignTable: foreignTable,
|
||||||
|
|
@ -249,7 +253,7 @@ func (qb *imageFilterHandler) tagCountCriterionHandler(tagCount *models.IntCrite
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *imageFilterHandler) galleriesCriterionHandler(galleries *models.MultiCriterionInput) criterionHandlerFunc {
|
func (qb *imageFilterHandler) galleriesCriterionHandler(galleries *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
addJoinsFunc := func(f *filterBuilder, joinType joinType) {
|
||||||
if galleries.Modifier == models.CriterionModifierIncludes || galleries.Modifier == models.CriterionModifierIncludesAll {
|
if galleries.Modifier == models.CriterionModifierIncludes || galleries.Modifier == models.CriterionModifierIncludesAll {
|
||||||
f.addInnerJoin(galleriesImagesTable, "", "galleries_images.image_id = images.id")
|
f.addInnerJoin(galleriesImagesTable, "", "galleries_images.image_id = images.id")
|
||||||
f.addInnerJoin(galleryTable, "", "galleries_images.gallery_id = galleries.id")
|
f.addInnerJoin(galleryTable, "", "galleries_images.gallery_id = galleries.id")
|
||||||
|
|
@ -268,8 +272,8 @@ func (qb *imageFilterHandler) performersCriterionHandler(performers *models.Mult
|
||||||
primaryFK: imageIDColumn,
|
primaryFK: imageIDColumn,
|
||||||
foreignFK: performerIDColumn,
|
foreignFK: performerIDColumn,
|
||||||
|
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
imageRepository.performers.join(f, "performers_join", "images.id")
|
imageRepository.performers.join(f, joinType, "performers_join", "images.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ import (
|
||||||
"gopkg.in/guregu/null.v4"
|
"gopkg.in/guregu/null.v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func post84(ctx context.Context, db *sqlx.DB) error {
|
func pre84(ctx context.Context, db *sqlx.DB) error {
|
||||||
logger.Info("Running post-migration for schema version 84")
|
logger.Info("Running pre-migration for schema version 84")
|
||||||
|
|
||||||
m := schema84Migrator{
|
m := schema84Migrator{
|
||||||
migrator: migrator{
|
migrator: migrator{
|
||||||
|
|
@ -36,6 +36,23 @@ func post84(ctx context.Context, db *sqlx.DB) error {
|
||||||
return fmt.Errorf("fixing incorrect parent folders: %w", err)
|
return fmt.Errorf("fixing incorrect parent folders: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := m.deduplicateFolders(ctx); err != nil {
|
||||||
|
return fmt.Errorf("deduplicating folders: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func post84(ctx context.Context, db *sqlx.DB) error {
|
||||||
|
logger.Info("Running post-migration for schema version 84")
|
||||||
|
|
||||||
|
m := schema84Migrator{
|
||||||
|
migrator: migrator{
|
||||||
|
db: db,
|
||||||
|
},
|
||||||
|
folderCache: make(map[string]folderInfo),
|
||||||
|
}
|
||||||
|
|
||||||
if err := m.migrateFolders(ctx); err != nil {
|
if err := m.migrateFolders(ctx); err != nil {
|
||||||
return fmt.Errorf("migrating folders: %w", err)
|
return fmt.Errorf("migrating folders: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +205,7 @@ func (m *schema84Migrator) getOrCreateFolderHierarchy(tx *sqlx.Tx, path string,
|
||||||
logger.Debugf("%s doesn't exist. Creating new folder entry...", path)
|
logger.Debugf("%s doesn't exist. Creating new folder entry...", path)
|
||||||
|
|
||||||
// we need to set basename to path, which will be addressed in the next step
|
// we need to set basename to path, which will be addressed in the next step
|
||||||
const insertSQL = "INSERT INTO `folders` (`path`,`basename`,`parent_folder_id`,`mod_time`,`created_at`,`updated_at`) VALUES (?,?,?,?,?,?)"
|
const insertSQL = "INSERT INTO `folders` (`path`,`parent_folder_id`,`mod_time`,`created_at`,`updated_at`) VALUES (?,?,?,?,?)"
|
||||||
|
|
||||||
var parentFolderID null.Int
|
var parentFolderID null.Int
|
||||||
if parentID != nil {
|
if parentID != nil {
|
||||||
|
|
@ -196,7 +213,7 @@ func (m *schema84Migrator) getOrCreateFolderHierarchy(tx *sqlx.Tx, path string,
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
result, err := tx.Exec(insertSQL, path, path, parentFolderID, time.Time{}, now, now)
|
result, err := tx.Exec(insertSQL, path, parentFolderID, time.Time{}, now, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating folder %s: %w", path, err)
|
return nil, fmt.Errorf("creating folder %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
@ -264,11 +281,6 @@ func (m *schema84Migrator) fixIncorrectParents(ctx context.Context, rootPaths []
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !logged {
|
|
||||||
logger.Info("Fixing folders with incorrect parent folder assignments...")
|
|
||||||
logged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
correctParentID, err := m.getOrCreateFolderHierarchy(tx, expectedParent, rootPaths)
|
correctParentID, err := m.getOrCreateFolderHierarchy(tx, expectedParent, rootPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting/creating correct parent for folder %d %q: %w", id, p, err)
|
return fmt.Errorf("error getting/creating correct parent for folder %d %q: %w", id, p, err)
|
||||||
|
|
@ -278,6 +290,11 @@ func (m *schema84Migrator) fixIncorrectParents(ctx context.Context, rootPaths []
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !logged {
|
||||||
|
logger.Info("Fixing folders with incorrect parent folder assignments...")
|
||||||
|
logged = true
|
||||||
|
}
|
||||||
|
|
||||||
logger.Debugf("Fixing folder %d %q: changing parent_folder_id from %d to %d", id, p, parentFolderID, *correctParentID)
|
logger.Debugf("Fixing folder %d %q: changing parent_folder_id from %d to %d", id, p, parentFolderID, *correctParentID)
|
||||||
|
|
||||||
_, err = tx.Exec("UPDATE `folders` SET `parent_folder_id` = ? WHERE `id` = ?", *correctParentID, id)
|
_, err = tx.Exec("UPDATE `folders` SET `parent_folder_id` = ? WHERE `id` = ?", *correctParentID, id)
|
||||||
|
|
@ -309,6 +326,136 @@ func (m *schema84Migrator) fixIncorrectParents(ctx context.Context, rootPaths []
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deduplicateFolders finds folders that would have the same (parent_folder_id, basename) after
|
||||||
|
// migrateFolders sets basename = filepath.Base(path), and merges the duplicates.
|
||||||
|
// This can happen when the database contains entries for the same physical folder with different
|
||||||
|
// path representations (e.g., mixed separators like "\data/movies" vs "\data\movies" on Windows).
|
||||||
|
func (m *schema84Migrator) deduplicateFolders(ctx context.Context) error {
|
||||||
|
for {
|
||||||
|
n, err := m.deduplicateFoldersPass(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// repeat until no more duplicates are found, since merging child folders
|
||||||
|
// from a duplicate parent into the canonical parent may create new conflicts
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *schema84Migrator) deduplicateFoldersPass(ctx context.Context) (int, error) {
|
||||||
|
type folderRow struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Path string `db:"path"`
|
||||||
|
ParentFolderID int `db:"parent_folder_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var folders []folderRow
|
||||||
|
if err := m.db.SelectContext(ctx, &folders,
|
||||||
|
"SELECT id, path, parent_folder_id FROM folders WHERE parent_folder_id IS NOT NULL ORDER BY id"); err != nil {
|
||||||
|
return 0, fmt.Errorf("loading folders: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// group by (parent_folder_id, computed basename)
|
||||||
|
type groupKey struct {
|
||||||
|
parentID int
|
||||||
|
basename string
|
||||||
|
}
|
||||||
|
groups := make(map[groupKey][]folderRow)
|
||||||
|
for _, f := range folders {
|
||||||
|
key := groupKey{
|
||||||
|
parentID: f.ParentFolderID,
|
||||||
|
basename: filepath.Base(f.Path),
|
||||||
|
}
|
||||||
|
groups[key] = append(groups[key], f)
|
||||||
|
}
|
||||||
|
|
||||||
|
deduped := 0
|
||||||
|
for _, group := range groups {
|
||||||
|
if len(group) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if deduped == 0 {
|
||||||
|
logger.Info("Deduplicating folders with conflicting basenames...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefer the folder whose path is already normalized for the current OS,
|
||||||
|
// falling back to the newest entry (highest ID) since it's most likely
|
||||||
|
// from the current filesystem
|
||||||
|
keep := group[len(group)-1]
|
||||||
|
for _, f := range group {
|
||||||
|
if f.Path == filepath.Clean(f.Path) {
|
||||||
|
keep = f
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dup := range group {
|
||||||
|
if dup.ID == keep.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("Merging duplicate folder %d %q into folder %d %q", dup.ID, dup.Path, keep.ID, keep.Path)
|
||||||
|
|
||||||
|
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
|
||||||
|
return m.mergeFolder(tx, keep.ID, dup.ID)
|
||||||
|
}); err != nil {
|
||||||
|
return 0, fmt.Errorf("merging folder %d into %d: %w", dup.ID, keep.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deduped++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if deduped > 0 {
|
||||||
|
logger.Infof("Deduplicated %d folder entries", deduped)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deduped, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *schema84Migrator) mergeFolder(tx *sqlx.Tx, keepID, dupID int) error {
|
||||||
|
// Re-parent child folders from the duplicate to the canonical folder.
|
||||||
|
// At this point basenames are still full paths (unique), so this won't cause
|
||||||
|
// UNIQUE constraint violations on (parent_folder_id, basename).
|
||||||
|
if _, err := tx.Exec("UPDATE folders SET parent_folder_id = ? WHERE parent_folder_id = ?", keepID, dupID); err != nil {
|
||||||
|
return fmt.Errorf("re-parenting child folders: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-parent any files under the duplicate folder to the canonical folder.
|
||||||
|
if _, err := tx.Exec("UPDATE files SET parent_folder_id = ? WHERE parent_folder_id = ?", keepID, dupID); err != nil {
|
||||||
|
return fmt.Errorf("re-parenting files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the duplicate folder entry only if it is not referenced by any galleries
|
||||||
|
var count int
|
||||||
|
if err := tx.Get(&count, "SELECT COUNT(*) FROM galleries WHERE folder_id = ?", dupID); err != nil {
|
||||||
|
return fmt.Errorf("checking for gallery references: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
logger.Warnf("Duplicate folder %d is still referenced by %d galleries. Orphaning instead of deleting.", dupID, count)
|
||||||
|
|
||||||
|
// Orphan the stale duplicate folder by clearing its parent so the UNIQUE
|
||||||
|
// constraint on (parent_folder_id, basename) won't be violated when
|
||||||
|
// migrateFolders sets basenames. Any stale file entries under it are left
|
||||||
|
// untouched — the clean task will handle them on the next scan.
|
||||||
|
if _, err := tx.Exec("UPDATE folders SET parent_folder_id = NULL WHERE id = ?", dupID); err != nil {
|
||||||
|
return fmt.Errorf("orphaning duplicate folder: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// delete the duplicate folder entry
|
||||||
|
if _, err := tx.Exec("DELETE FROM folders WHERE id = ?", dupID); err != nil {
|
||||||
|
return fmt.Errorf("deleting duplicate folder: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *schema84Migrator) migrateFolders(ctx context.Context) error {
|
func (m *schema84Migrator) migrateFolders(ctx context.Context) error {
|
||||||
const (
|
const (
|
||||||
limit = 1000
|
limit = 1000
|
||||||
|
|
@ -381,5 +528,6 @@ func (m *schema84Migrator) migrateFolders(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
sqlite.RegisterPreMigration(84, pre84)
|
||||||
sqlite.RegisterPostMigration(84, post84)
|
sqlite.RegisterPostMigration(84, post84)
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +188,7 @@ func (qb *performerFilterHandler) criterionHandler() criterionHandler {
|
||||||
intCriterionHandler(filter.Weight, tableName+".weight", nil),
|
intCriterionHandler(filter.Weight, tableName+".weight", nil),
|
||||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||||
if filter.StashID != nil {
|
if filter.StashID != nil {
|
||||||
performerRepository.stashIDs.join(f, "performer_stash_ids", "performers.id")
|
performerRepository.stashIDs.leftJoin(f, "performer_stash_ids", "performers.id")
|
||||||
stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f)
|
stringCriterionHandler(filter.StashID, "performer_stash_ids.stash_id")(ctx, f)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -333,7 +333,7 @@ func (qb *performerFilterHandler) performerIsMissingCriterionHandler(isMissing *
|
||||||
if isMissing != nil && *isMissing != "" {
|
if isMissing != nil && *isMissing != "" {
|
||||||
switch *isMissing {
|
switch *isMissing {
|
||||||
case "url":
|
case "url":
|
||||||
performersURLsTableMgr.join(f, "", "performers.id")
|
performersURLsTableMgr.leftJoin(f, "", "performers.id")
|
||||||
f.addWhere("performer_urls.url IS NULL")
|
f.addWhere("performer_urls.url IS NULL")
|
||||||
case "scenes": // Deprecated: use `scene_count == 0` filter instead
|
case "scenes": // Deprecated: use `scene_count == 0` filter instead
|
||||||
f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id")
|
f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id")
|
||||||
|
|
@ -341,10 +341,10 @@ func (qb *performerFilterHandler) performerIsMissingCriterionHandler(isMissing *
|
||||||
case "image":
|
case "image":
|
||||||
f.addWhere("performers.image_blob IS NULL")
|
f.addWhere("performers.image_blob IS NULL")
|
||||||
case "stash_id":
|
case "stash_id":
|
||||||
performersStashIDsTableMgr.join(f, "performer_stash_ids", "performers.id")
|
performersStashIDsTableMgr.leftJoin(f, "performer_stash_ids", "performers.id")
|
||||||
f.addWhere("performer_stash_ids.performer_id IS NULL")
|
f.addWhere("performer_stash_ids.performer_id IS NULL")
|
||||||
case "aliases":
|
case "aliases":
|
||||||
performersAliasesTableMgr.join(f, "", "performers.id")
|
performersAliasesTableMgr.leftJoin(f, "", "performers.id")
|
||||||
f.addWhere("performer_aliases.alias IS NULL")
|
f.addWhere("performer_aliases.alias IS NULL")
|
||||||
case "tags":
|
case "tags":
|
||||||
f.addLeftJoin(performersTagsTable, "tags_join", "tags_join.performer_id = performers.id")
|
f.addLeftJoin(performersTagsTable, "tags_join", "tags_join.performer_id = performers.id")
|
||||||
|
|
@ -383,8 +383,8 @@ func (qb *performerFilterHandler) urlsCriterionHandler(url *models.StringCriteri
|
||||||
primaryFK: performerIDColumn,
|
primaryFK: performerIDColumn,
|
||||||
joinTable: performerURLsTable,
|
joinTable: performerURLsTable,
|
||||||
stringColumn: performerURLColumn,
|
stringColumn: performerURLColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
performersURLsTableMgr.join(f, "", "performers.id")
|
performersURLsTableMgr.join(f, joinType, "", "performers.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -397,8 +397,8 @@ func (qb *performerFilterHandler) aliasCriterionHandler(alias *models.StringCrit
|
||||||
primaryFK: performerIDColumn,
|
primaryFK: performerIDColumn,
|
||||||
joinTable: performersAliasesTable,
|
joinTable: performersAliasesTable,
|
||||||
stringColumn: performerAliasColumn,
|
stringColumn: performerAliasColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
performersAliasesTableMgr.join(f, "", "performers.id")
|
performersAliasesTableMgr.join(f, joinType, "", "performers.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ func (r *updateRecord) setTimestamp(destField string, v models.OptionalTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:golint,unused
|
//nolint:unused
|
||||||
func (r *updateRecord) setNullTimestamp(destField string, v models.OptionalTime) {
|
func (r *updateRecord) setNullTimestamp(destField string, v models.OptionalTime) {
|
||||||
if v.Set {
|
if v.Set {
|
||||||
r.set(destField, NullTimestampFromTimePtr(v.Ptr()))
|
r.set(destField, NullTimestampFromTimePtr(v.Ptr()))
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,15 @@ func (r *repository) newQuery() queryBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repository) join(j joiner, as string, parentIDCol string) {
|
func (r *repository) join(j joiner, t joinType, as string, parentIDCol string) {
|
||||||
|
fn := r.innerJoin
|
||||||
|
if t == joinTypeLeft {
|
||||||
|
fn = r.leftJoin
|
||||||
|
}
|
||||||
|
fn(j, as, parentIDCol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *repository) leftJoin(j joiner, as string, parentIDCol string) {
|
||||||
t := r.tableName
|
t := r.tableName
|
||||||
if as != "" {
|
if as != "" {
|
||||||
t = as
|
t = as
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,12 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
||||||
stringCriterionHandler(sceneFilter.Director, "scenes.director"),
|
stringCriterionHandler(sceneFilter.Director, "scenes.director"),
|
||||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||||
if sceneFilter.Oshash != nil {
|
if sceneFilter.Oshash != nil {
|
||||||
qb.addSceneFilesTable(f)
|
joinType := joinTypeInner
|
||||||
f.addLeftJoin(fingerprintTable, "fingerprints_oshash", "scenes_files.file_id = fingerprints_oshash.file_id AND fingerprints_oshash.type = 'oshash'")
|
if sceneFilter.Oshash.Modifier == models.CriterionModifierIsNull {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
qb.addSceneFilesTable(f, joinType)
|
||||||
|
f.addJoin(joinType, fingerprintTable, "fingerprints_oshash", "scenes_files.file_id = fingerprints_oshash.file_id AND fingerprints_oshash.type = 'oshash'")
|
||||||
}
|
}
|
||||||
|
|
||||||
stringCriterionHandler(sceneFilter.Oshash, "fingerprints_oshash.fingerprint")(ctx, f)
|
stringCriterionHandler(sceneFilter.Oshash, "fingerprints_oshash.fingerprint")(ctx, f)
|
||||||
|
|
@ -72,8 +76,12 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
||||||
|
|
||||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||||
if sceneFilter.Checksum != nil {
|
if sceneFilter.Checksum != nil {
|
||||||
qb.addSceneFilesTable(f)
|
joinType := joinTypeInner
|
||||||
f.addLeftJoin(fingerprintTable, "fingerprints_md5", "scenes_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
|
if sceneFilter.Checksum.Modifier == models.CriterionModifierIsNull {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
qb.addSceneFilesTable(f, joinType)
|
||||||
|
f.addJoin(joinType, fingerprintTable, "fingerprints_md5", "scenes_files.file_id = fingerprints_md5.file_id AND fingerprints_md5.type = 'md5'")
|
||||||
}
|
}
|
||||||
|
|
||||||
stringCriterionHandler(sceneFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
stringCriterionHandler(sceneFilter.Checksum, "fingerprints_md5.fingerprint")(ctx, f)
|
||||||
|
|
@ -84,8 +92,12 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
||||||
// backwards compatibility
|
// backwards compatibility
|
||||||
h := phashDistanceCriterionHandler{
|
h := phashDistanceCriterionHandler{
|
||||||
joinFn: func(f *filterBuilder) {
|
joinFn: func(f *filterBuilder) {
|
||||||
qb.addSceneFilesTable(f)
|
joinType := joinTypeInner
|
||||||
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
if sceneFilter.Phash.Modifier == models.CriterionModifierIsNull {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
qb.addSceneFilesTable(f, joinType)
|
||||||
|
f.addJoin(joinType, fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
||||||
},
|
},
|
||||||
criterion: &models.PhashDistanceCriterionInput{
|
criterion: &models.PhashDistanceCriterionInput{
|
||||||
Value: sceneFilter.Phash.Value,
|
Value: sceneFilter.Phash.Value,
|
||||||
|
|
@ -98,8 +110,9 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
||||||
|
|
||||||
&phashDistanceCriterionHandler{
|
&phashDistanceCriterionHandler{
|
||||||
joinFn: func(f *filterBuilder) {
|
joinFn: func(f *filterBuilder) {
|
||||||
qb.addSceneFilesTable(f)
|
const joinType = joinTypeInner
|
||||||
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
qb.addSceneFilesTable(f, joinType)
|
||||||
|
f.addJoin(joinType, fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
||||||
},
|
},
|
||||||
criterion: sceneFilter.PhashDistance,
|
criterion: sceneFilter.PhashDistance,
|
||||||
},
|
},
|
||||||
|
|
@ -122,7 +135,7 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
||||||
|
|
||||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||||
if sceneFilter.StashID != nil {
|
if sceneFilter.StashID != nil {
|
||||||
sceneRepository.stashIDs.join(f, "scene_stash_ids", "scenes.id")
|
sceneRepository.stashIDs.leftJoin(f, "scene_stash_ids", "scenes.id")
|
||||||
stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f)
|
stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id")(ctx, f)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -236,8 +249,8 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
||||||
isRelated: true,
|
isRelated: true,
|
||||||
},
|
},
|
||||||
joinFn: func(f *filterBuilder) {
|
joinFn: func(f *filterBuilder) {
|
||||||
qb.addFilesTable(f)
|
qb.addFilesTable(f, joinTypeInner)
|
||||||
qb.addFoldersTable(f)
|
qb.addFoldersTable(f, joinTypeInner)
|
||||||
},
|
},
|
||||||
// don't use a subquery; join directly
|
// don't use a subquery; join directly
|
||||||
directJoin: true,
|
directJoin: true,
|
||||||
|
|
@ -254,23 +267,23 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneFilterHandler) addSceneFilesTable(f *filterBuilder) {
|
func (qb *sceneFilterHandler) addSceneFilesTable(f *filterBuilder, joinType joinType) {
|
||||||
f.addLeftJoin(scenesFilesTable, "", "scenes_files.scene_id = scenes.id")
|
f.addJoin(joinType, scenesFilesTable, "", "scenes_files.scene_id = scenes.id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneFilterHandler) addFilesTable(f *filterBuilder) {
|
func (qb *sceneFilterHandler) addFilesTable(f *filterBuilder, joinType joinType) {
|
||||||
qb.addSceneFilesTable(f)
|
qb.addSceneFilesTable(f, joinType)
|
||||||
f.addLeftJoin(fileTable, "", "scenes_files.file_id = files.id")
|
f.addJoin(joinType, fileTable, "", "scenes_files.file_id = files.id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneFilterHandler) addFoldersTable(f *filterBuilder) {
|
func (qb *sceneFilterHandler) addFoldersTable(f *filterBuilder, joinType joinType) {
|
||||||
qb.addFilesTable(f)
|
qb.addFilesTable(f, joinType)
|
||||||
f.addLeftJoin(folderTable, "", "files.parent_folder_id = folders.id")
|
f.addJoin(joinType, folderTable, "", "files.parent_folder_id = folders.id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneFilterHandler) addVideoFilesTable(f *filterBuilder) {
|
func (qb *sceneFilterHandler) addVideoFilesTable(f *filterBuilder, joinType joinType) {
|
||||||
qb.addSceneFilesTable(f)
|
qb.addSceneFilesTable(f, joinType)
|
||||||
f.addLeftJoin(videoFileTable, "", "video_files.file_id = scenes_files.file_id")
|
f.addJoin(joinType, videoFileTable, "", "video_files.file_id = scenes_files.file_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneFilterHandler) playCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
func (qb *sceneFilterHandler) playCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
|
|
@ -318,7 +331,7 @@ func (qb *sceneFilterHandler) duplicatedCriterionHandler(duplicatedFilter *model
|
||||||
|
|
||||||
// Handle explicit fields
|
// Handle explicit fields
|
||||||
if duplicatedFilter.Phash != nil {
|
if duplicatedFilter.Phash != nil {
|
||||||
qb.addSceneFilesTable(f)
|
qb.addSceneFilesTable(f, joinTypeInner)
|
||||||
qb.applyPhashDuplication(f, *duplicatedFilter.Phash)
|
qb.applyPhashDuplication(f, *duplicatedFilter.Phash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -368,11 +381,15 @@ func (qb *sceneFilterHandler) applyURLDuplication(f *filterBuilder, duplicated b
|
||||||
f.addInnerJoin("(SELECT scene_id FROM scene_urls INNER JOIN (SELECT url FROM scene_urls GROUP BY url HAVING COUNT(DISTINCT scene_id) "+v+" 1) dupes ON scene_urls.url = dupes.url)", "scurl", "scenes.id = scurl.scene_id")
|
f.addInnerJoin("(SELECT scene_id FROM scene_urls INNER JOIN (SELECT url FROM scene_urls GROUP BY url HAVING COUNT(DISTINCT scene_id) "+v+" 1) dupes ON scene_urls.url = dupes.url)", "scurl", "scenes.id = scurl.scene_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
func (qb *sceneFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder, joinType joinType)) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if codec != nil {
|
if codec != nil {
|
||||||
if addJoinFn != nil {
|
if addJoinFn != nil {
|
||||||
addJoinFn(f)
|
joinType := joinTypeInner
|
||||||
|
if codec.Modifier == models.CriterionModifierIsNull {
|
||||||
|
joinType = joinTypeLeft
|
||||||
|
}
|
||||||
|
addJoinFn(f, joinType)
|
||||||
}
|
}
|
||||||
|
|
||||||
stringCriterionHandler(codec, codecColumn)(ctx, f)
|
stringCriterionHandler(codec, codecColumn)(ctx, f)
|
||||||
|
|
@ -398,29 +415,29 @@ func (qb *sceneFilterHandler) isMissingCriterionHandler(isMissing *string) crite
|
||||||
if isMissing != nil && *isMissing != "" {
|
if isMissing != nil && *isMissing != "" {
|
||||||
switch *isMissing {
|
switch *isMissing {
|
||||||
case "url":
|
case "url":
|
||||||
scenesURLsTableMgr.join(f, "", "scenes.id")
|
scenesURLsTableMgr.leftJoin(f, "", "scenes.id")
|
||||||
f.addWhere("scene_urls.url IS NULL")
|
f.addWhere("scene_urls.url IS NULL")
|
||||||
case "galleries":
|
case "galleries":
|
||||||
sceneRepository.galleries.join(f, "galleries_join", "scenes.id")
|
sceneRepository.galleries.leftJoin(f, "galleries_join", "scenes.id")
|
||||||
f.addWhere("galleries_join.scene_id IS NULL")
|
f.addWhere("galleries_join.scene_id IS NULL")
|
||||||
case "studio":
|
case "studio":
|
||||||
f.addWhere("scenes.studio_id IS NULL")
|
f.addWhere("scenes.studio_id IS NULL")
|
||||||
case "movie", "group":
|
case "movie", "group":
|
||||||
sceneRepository.groups.join(f, "groups_join", "scenes.id")
|
sceneRepository.groups.leftJoin(f, "groups_join", "scenes.id")
|
||||||
f.addWhere("groups_join.scene_id IS NULL")
|
f.addWhere("groups_join.scene_id IS NULL")
|
||||||
case "performers":
|
case "performers":
|
||||||
sceneRepository.performers.join(f, "performers_join", "scenes.id")
|
sceneRepository.performers.leftJoin(f, "performers_join", "scenes.id")
|
||||||
f.addWhere("performers_join.scene_id IS NULL")
|
f.addWhere("performers_join.scene_id IS NULL")
|
||||||
case "date":
|
case "date":
|
||||||
f.addWhere(`scenes.date IS NULL OR scenes.date IS ""`)
|
f.addWhere(`scenes.date IS NULL OR scenes.date IS ""`)
|
||||||
case "tags":
|
case "tags":
|
||||||
sceneRepository.tags.join(f, "tags_join", "scenes.id")
|
sceneRepository.tags.leftJoin(f, "tags_join", "scenes.id")
|
||||||
f.addWhere("tags_join.scene_id IS NULL")
|
f.addWhere("tags_join.scene_id IS NULL")
|
||||||
case "stash_id":
|
case "stash_id":
|
||||||
sceneRepository.stashIDs.join(f, "scene_stash_ids", "scenes.id")
|
sceneRepository.stashIDs.leftJoin(f, "scene_stash_ids", "scenes.id")
|
||||||
f.addWhere("scene_stash_ids.scene_id IS NULL")
|
f.addWhere("scene_stash_ids.scene_id IS NULL")
|
||||||
case "phash":
|
case "phash":
|
||||||
qb.addSceneFilesTable(f)
|
qb.addSceneFilesTable(f, joinTypeLeft)
|
||||||
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
|
||||||
f.addWhere("fingerprints_phash.fingerprint IS NULL")
|
f.addWhere("fingerprints_phash.fingerprint IS NULL")
|
||||||
case "cover":
|
case "cover":
|
||||||
|
|
@ -444,15 +461,15 @@ func (qb *sceneFilterHandler) urlsCriterionHandler(url *models.StringCriterionIn
|
||||||
primaryFK: sceneIDColumn,
|
primaryFK: sceneIDColumn,
|
||||||
joinTable: scenesURLsTable,
|
joinTable: scenesURLsTable,
|
||||||
stringColumn: sceneURLColumn,
|
stringColumn: sceneURLColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
scenesURLsTableMgr.join(f, "", "scenes.id")
|
scenesURLsTableMgr.join(f, joinType, "", "scenes.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.handler(url)
|
return h.handler(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
func (qb *sceneFilterHandler) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder, joinType joinType)) multiCriterionHandlerBuilder {
|
||||||
return multiCriterionHandlerBuilder{
|
return multiCriterionHandlerBuilder{
|
||||||
primaryTable: sceneTable,
|
primaryTable: sceneTable,
|
||||||
foreignTable: foreignTable,
|
foreignTable: foreignTable,
|
||||||
|
|
@ -469,9 +486,9 @@ func (qb *sceneFilterHandler) captionCriterionHandler(captions *models.StringCri
|
||||||
primaryFK: sceneIDColumn,
|
primaryFK: sceneIDColumn,
|
||||||
joinTable: videoCaptionsTable,
|
joinTable: videoCaptionsTable,
|
||||||
stringColumn: captionCodeColumn,
|
stringColumn: captionCodeColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
qb.addSceneFilesTable(f)
|
qb.addSceneFilesTable(f, joinTypeLeft)
|
||||||
f.addLeftJoin(videoCaptionsTable, "", "video_captions.file_id = scenes_files.file_id")
|
f.addJoin(joinType, videoCaptionsTable, "", "video_captions.file_id = scenes_files.file_id")
|
||||||
},
|
},
|
||||||
excludeHandler: func(f *filterBuilder, criterion *models.StringCriterionInput) {
|
excludeHandler: func(f *filterBuilder, criterion *models.StringCriterionInput) {
|
||||||
excludeClause := `scenes.id NOT IN (
|
excludeClause := `scenes.id NOT IN (
|
||||||
|
|
@ -531,8 +548,8 @@ func (qb *sceneFilterHandler) performersCriterionHandler(performers *models.Mult
|
||||||
primaryFK: sceneIDColumn,
|
primaryFK: sceneIDColumn,
|
||||||
foreignFK: performerIDColumn,
|
foreignFK: performerIDColumn,
|
||||||
|
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
sceneRepository.performers.join(f, "performers_join", "scenes.id")
|
sceneRepository.performers.join(f, joinType, "performers_join", "scenes.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -587,9 +604,9 @@ func (qb *sceneFilterHandler) performerAgeCriterionHandler(performerAge *models.
|
||||||
|
|
||||||
// legacy handler
|
// legacy handler
|
||||||
func (qb *sceneFilterHandler) moviesCriterionHandler(movies *models.MultiCriterionInput) criterionHandlerFunc {
|
func (qb *sceneFilterHandler) moviesCriterionHandler(movies *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
addJoinsFunc := func(f *filterBuilder, joinType joinType) {
|
||||||
sceneRepository.groups.join(f, "", "scenes.id")
|
sceneRepository.groups.leftJoin(f, "", "scenes.id")
|
||||||
f.addLeftJoin("groups", "", "groups_scenes.group_id = groups.id")
|
f.addJoin(joinType, "groups", "", "groups_scenes.group_id = groups.id")
|
||||||
}
|
}
|
||||||
h := qb.getMultiCriterionHandlerBuilder(groupTable, groupsScenesTable, "group_id", addJoinsFunc)
|
h := qb.getMultiCriterionHandlerBuilder(groupTable, groupsScenesTable, "group_id", addJoinsFunc)
|
||||||
return h.handler(movies)
|
return h.handler(movies)
|
||||||
|
|
@ -613,9 +630,9 @@ func (qb *sceneFilterHandler) groupsCriterionHandler(groups *models.Hierarchical
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneFilterHandler) galleriesCriterionHandler(galleries *models.MultiCriterionInput) criterionHandlerFunc {
|
func (qb *sceneFilterHandler) galleriesCriterionHandler(galleries *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
addJoinsFunc := func(f *filterBuilder, joinType joinType) {
|
||||||
sceneRepository.galleries.join(f, "", "scenes.id")
|
sceneRepository.galleries.leftJoin(f, "", "scenes.id")
|
||||||
f.addLeftJoin("galleries", "", "scenes_galleries.gallery_id = galleries.id")
|
f.addJoin(joinType, "galleries", "", "scenes_galleries.gallery_id = galleries.id")
|
||||||
}
|
}
|
||||||
h := qb.getMultiCriterionHandlerBuilder(galleryTable, scenesGalleriesTable, "gallery_id", addJoinsFunc)
|
h := qb.getMultiCriterionHandlerBuilder(galleryTable, scenesGalleriesTable, "gallery_id", addJoinsFunc)
|
||||||
return h.handler(galleries)
|
return h.handler(galleries)
|
||||||
|
|
|
||||||
|
|
@ -173,8 +173,8 @@ func (qb *sceneMarkerFilterHandler) performersCriterionHandler(performers *model
|
||||||
primaryFK: sceneIDColumn,
|
primaryFK: sceneIDColumn,
|
||||||
foreignFK: performerIDColumn,
|
foreignFK: performerIDColumn,
|
||||||
|
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
f.addLeftJoin(performersScenesTable, "performers_join", "performers_join.scene_id = scene_markers.scene_id")
|
f.addJoin(joinType, performersScenesTable, "performers_join", "performers_join.scene_id = scene_markers.scene_id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,8 +191,8 @@ func (qb *sceneMarkerFilterHandler) performersCriterionHandler(performers *model
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneMarkerFilterHandler) scenesCriterionHandler(scenes *models.MultiCriterionInput) criterionHandlerFunc {
|
func (qb *sceneMarkerFilterHandler) scenesCriterionHandler(scenes *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
addJoinsFunc := func(f *filterBuilder, joinType joinType) {
|
||||||
f.addLeftJoin(sceneTable, "markers_scenes", "markers_scenes.id = scene_markers.scene_id")
|
f.addJoin(joinType, sceneTable, "markers_scenes", "markers_scenes.id = scene_markers.scene_id")
|
||||||
}
|
}
|
||||||
h := multiCriterionHandlerBuilder{
|
h := multiCriterionHandlerBuilder{
|
||||||
primaryTable: sceneMarkerTable,
|
primaryTable: sceneMarkerTable,
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ func (qb *studioFilterHandler) criterionHandler() criterionHandler {
|
||||||
|
|
||||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||||
if studioFilter.StashID != nil {
|
if studioFilter.StashID != nil {
|
||||||
studioRepository.stashIDs.join(f, "studio_stash_ids", "studios.id")
|
studioRepository.stashIDs.leftJoin(f, "studio_stash_ids", "studios.id")
|
||||||
stringCriterionHandler(studioFilter.StashID, "studio_stash_ids.stash_id")(ctx, f)
|
stringCriterionHandler(studioFilter.StashID, "studio_stash_ids.stash_id")(ctx, f)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -143,15 +143,15 @@ func (qb *studioFilterHandler) isMissingCriterionHandler(isMissing *string) crit
|
||||||
if isMissing != nil && *isMissing != "" {
|
if isMissing != nil && *isMissing != "" {
|
||||||
switch *isMissing {
|
switch *isMissing {
|
||||||
case "url":
|
case "url":
|
||||||
studiosURLsTableMgr.join(f, "", "studios.id")
|
studiosURLsTableMgr.leftJoin(f, "", "studios.id")
|
||||||
f.addWhere("studio_urls.url IS NULL")
|
f.addWhere("studio_urls.url IS NULL")
|
||||||
case "image":
|
case "image":
|
||||||
f.addWhere("studios.image_blob IS NULL")
|
f.addWhere("studios.image_blob IS NULL")
|
||||||
case "stash_id":
|
case "stash_id":
|
||||||
studioRepository.stashIDs.join(f, "studio_stash_ids", "studios.id")
|
studioRepository.stashIDs.leftJoin(f, "studio_stash_ids", "studios.id")
|
||||||
f.addWhere("studio_stash_ids.studio_id IS NULL")
|
f.addWhere("studio_stash_ids.studio_id IS NULL")
|
||||||
case "aliases":
|
case "aliases":
|
||||||
studiosAliasesTableMgr.join(f, "", "studios.id")
|
studiosAliasesTableMgr.leftJoin(f, "", "studios.id")
|
||||||
f.addWhere("studio_aliases.alias IS NULL")
|
f.addWhere("studio_aliases.alias IS NULL")
|
||||||
case "tags":
|
case "tags":
|
||||||
f.addLeftJoin(studiosTagsTable, "tags_join", "tags_join.studio_id = studios.id")
|
f.addLeftJoin(studiosTagsTable, "tags_join", "tags_join.studio_id = studios.id")
|
||||||
|
|
@ -224,8 +224,8 @@ func (qb *studioFilterHandler) tagCountCriterionHandler(tagCount *models.IntCrit
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *studioFilterHandler) parentCriterionHandler(parents *models.MultiCriterionInput) criterionHandlerFunc {
|
func (qb *studioFilterHandler) parentCriterionHandler(parents *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
addJoinsFunc := func(f *filterBuilder, joinType joinType) {
|
||||||
f.addLeftJoin("studios", "parent_studio", "parent_studio.id = studios.parent_id")
|
f.addJoin(joinType, "studios", "parent_studio", "parent_studio.id = studios.parent_id")
|
||||||
}
|
}
|
||||||
h := multiCriterionHandlerBuilder{
|
h := multiCriterionHandlerBuilder{
|
||||||
primaryTable: studioTable,
|
primaryTable: studioTable,
|
||||||
|
|
@ -244,8 +244,8 @@ func (qb *studioFilterHandler) aliasCriterionHandler(alias *models.StringCriteri
|
||||||
primaryFK: studioIDColumn,
|
primaryFK: studioIDColumn,
|
||||||
joinTable: studioAliasesTable,
|
joinTable: studioAliasesTable,
|
||||||
stringColumn: studioAliasColumn,
|
stringColumn: studioAliasColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
studiosAliasesTableMgr.join(f, "", "studios.id")
|
studiosAliasesTableMgr.join(f, joinType, "", "studios.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,8 +258,8 @@ func (qb *studioFilterHandler) urlsCriterionHandler(url *models.StringCriterionI
|
||||||
primaryFK: studioIDColumn,
|
primaryFK: studioIDColumn,
|
||||||
joinTable: studioURLsTable,
|
joinTable: studioURLsTable,
|
||||||
stringColumn: studioURLColumn,
|
stringColumn: studioURLColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
studiosURLsTableMgr.join(f, "", "studios.id")
|
studiosURLsTableMgr.join(f, joinType, "", "studios.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,22 @@ func (t *table) destroy(ctx context.Context, ids []int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *table) join(j joiner, as string, parentIDCol string) {
|
func (t *table) join(j joiner, jt joinType, as string, parentIDCol string) {
|
||||||
|
tableName := t.table.GetTable()
|
||||||
|
tt := tableName
|
||||||
|
if as != "" {
|
||||||
|
tt = as
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := j.addInnerJoin
|
||||||
|
if jt == joinTypeLeft {
|
||||||
|
fn = j.addLeftJoin
|
||||||
|
}
|
||||||
|
|
||||||
|
fn(tableName, as, fmt.Sprintf("%s.%s = %s", tt, t.idColumn.GetCol(), parentIDCol))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *table) leftJoin(j joiner, as string, parentIDCol string) {
|
||||||
tableName := t.table.GetTable()
|
tableName := t.table.GetTable()
|
||||||
tt := tableName
|
tt := tableName
|
||||||
if as != "" {
|
if as != "" {
|
||||||
|
|
|
||||||
|
|
@ -416,6 +416,18 @@ func (qb *TagStore) find(ctx context.Context, id int) (*models.Tag, error) {
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *TagStore) findBySubquery(ctx context.Context, sq *goqu.SelectDataset) ([]*models.Tag, error) {
|
||||||
|
table := qb.table()
|
||||||
|
|
||||||
|
q := qb.selectDataset().Prepared(true).Where(
|
||||||
|
table.Col(idColumn).Eq(
|
||||||
|
sq,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return qb.getMany(ctx, q)
|
||||||
|
}
|
||||||
|
|
||||||
// returns nil, sql.ErrNoRows if not found
|
// returns nil, sql.ErrNoRows if not found
|
||||||
func (qb *TagStore) get(ctx context.Context, q *goqu.SelectDataset) (*models.Tag, error) {
|
func (qb *TagStore) get(ctx context.Context, q *goqu.SelectDataset) (*models.Tag, error) {
|
||||||
ret, err := qb.getMany(ctx, q)
|
ret, err := qb.getMany(ctx, q)
|
||||||
|
|
@ -579,6 +591,27 @@ func (qb *TagStore) FindByNames(ctx context.Context, names []string, nocase bool
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *TagStore) FindByAlias(ctx context.Context, alias string, nocase bool) (*models.Tag, error) {
|
||||||
|
where := fmt.Sprintf("%s = ?", tagAliasColumn)
|
||||||
|
if nocase {
|
||||||
|
where += " COLLATE NOCASE"
|
||||||
|
}
|
||||||
|
sq := dialect.From(tagsAliasesJoinTable).Select(
|
||||||
|
tagsAliasesJoinTable.Col(tagIDColumn),
|
||||||
|
).Prepared(true).Where(goqu.L(where, alias)).Limit(1)
|
||||||
|
ret, err := qb.findBySubquery(ctx, sq)
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *TagStore) FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Tag, error) {
|
func (qb *TagStore) FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Tag, error) {
|
||||||
sq := dialect.From(tagsStashIDsJoinTable).Select(tagsStashIDsJoinTable.Col(tagIDColumn)).Where(
|
sq := dialect.From(tagsStashIDsJoinTable).Select(tagsStashIDsJoinTable.Col(tagIDColumn)).Where(
|
||||||
tagsStashIDsJoinTable.Col("stash_id").Eq(stashID.StashID),
|
tagsStashIDsJoinTable.Col("stash_id").Eq(stashID.StashID),
|
||||||
|
|
|
||||||
|
|
@ -184,8 +184,8 @@ func (qb *tagFilterHandler) aliasCriterionHandler(alias *models.StringCriterionI
|
||||||
primaryFK: tagIDColumn,
|
primaryFK: tagIDColumn,
|
||||||
joinTable: tagAliasesTable,
|
joinTable: tagAliasesTable,
|
||||||
stringColumn: tagAliasColumn,
|
stringColumn: tagAliasColumn,
|
||||||
addJoinTable: func(f *filterBuilder) {
|
addJoinTable: func(f *filterBuilder, joinType joinType) {
|
||||||
tagRepository.aliases.join(f, "", "tags.id")
|
tagRepository.aliases.join(f, joinType, "", "tags.id")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,10 +199,10 @@ func (qb *tagFilterHandler) isMissingCriterionHandler(isMissing *string) criteri
|
||||||
case "image":
|
case "image":
|
||||||
f.addWhere("tags.image_blob IS NULL")
|
f.addWhere("tags.image_blob IS NULL")
|
||||||
case "aliases":
|
case "aliases":
|
||||||
tagRepository.aliases.join(f, "", "tags.id")
|
tagRepository.aliases.leftJoin(f, "", "tags.id")
|
||||||
f.addWhere("tag_aliases.alias IS NULL")
|
f.addWhere("tag_aliases.alias IS NULL")
|
||||||
case "stash_id":
|
case "stash_id":
|
||||||
tagRepository.stashIDs.join(f, "tag_stash_ids", "tags.id")
|
tagRepository.stashIDs.leftJoin(f, "tag_stash_ids", "tags.id")
|
||||||
f.addWhere("tag_stash_ids.tag_id IS NULL")
|
f.addWhere("tag_stash_ids.tag_id IS NULL")
|
||||||
default:
|
default:
|
||||||
if err := validateIsMissing(*isMissing, []string{
|
if err := validateIsMissing(*isMissing, []string{
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,24 @@ func TestTagFindByName(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTagFindByAlias(t *testing.T) {
|
||||||
|
withTxn(func(ctx context.Context) error {
|
||||||
|
tqb := db.Tag
|
||||||
|
|
||||||
|
alias := getTagStringValue(tagIdxWithScene, "Alias")
|
||||||
|
|
||||||
|
tag, err := tqb.FindByAlias(ctx, alias, false)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error finding tags: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tagIDs[tagIdxWithScene], tag.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestTagQueryIgnoreAutoTag(t *testing.T) {
|
func TestTagQueryIgnoreAutoTag(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
withTxn(func(ctx context.Context) error {
|
||||||
ignoreAutoTag := true
|
ignoreAutoTag := true
|
||||||
|
|
|
||||||
|
|
@ -204,11 +204,7 @@ func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.Scen
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, t := range s.Tags {
|
for _, t := range s.Tags {
|
||||||
st := &models.ScrapedTag{
|
ss.Tags = append(ss.Tags, tagFragmentToScrapedTag(*t))
|
||||||
Name: t.Name,
|
|
||||||
RemoteSiteID: &t.ID,
|
|
||||||
}
|
|
||||||
ss.Tags = append(ss.Tags, st)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ss, nil
|
return ss, nil
|
||||||
|
|
|
||||||
|
|
@ -6,50 +6,24 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ByName(ctx context.Context, qb models.TagQueryer, name string) (*models.Tag, error) {
|
func ByName(ctx context.Context, qb models.TagNameFinder, name string) (*models.Tag, error) {
|
||||||
f := &models.TagFilterType{
|
const nocase = true
|
||||||
Name: &models.StringCriterionInput{
|
ret, err := qb.FindByName(ctx, name, nocase)
|
||||||
Value: name,
|
|
||||||
Modifier: models.CriterionModifierEquals,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pp := 1
|
|
||||||
ret, count, err := qb.Query(ctx, f, &models.FindFilterType{
|
|
||||||
PerPage: &pp,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if count > 0 {
|
return ret, nil
|
||||||
return ret[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByAlias(ctx context.Context, qb models.TagQueryer, alias string) (*models.Tag, error) {
|
func ByAlias(ctx context.Context, qb models.TagNameFinder, alias string) (*models.Tag, error) {
|
||||||
f := &models.TagFilterType{
|
const nocase = true
|
||||||
Aliases: &models.StringCriterionInput{
|
ret, err := qb.FindByAlias(ctx, alias, nocase)
|
||||||
Value: alias,
|
|
||||||
Modifier: models.CriterionModifierEquals,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pp := 1
|
|
||||||
ret, count, err := qb.Query(ctx, f, &models.FindFilterType{
|
|
||||||
PerPage: &pp,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if count > 0 {
|
return ret, nil
|
||||||
return ret[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ func (e *InvalidTagHierarchyError) Error() string {
|
||||||
|
|
||||||
// EnsureTagNameUnique returns an error if the tag name provided
|
// EnsureTagNameUnique returns an error if the tag name provided
|
||||||
// is used as a name or alias of another existing tag.
|
// is used as a name or alias of another existing tag.
|
||||||
func EnsureTagNameUnique(ctx context.Context, id int, name string, qb models.TagQueryer) error {
|
func EnsureTagNameUnique(ctx context.Context, id int, name string, qb models.TagNameFinder) error {
|
||||||
// ensure name is unique
|
// ensure name is unique
|
||||||
sameNameTag, err := ByName(ctx, qb, name)
|
sameNameTag, err := ByName(ctx, qb, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -71,7 +71,7 @@ func EnsureTagNameUnique(ctx context.Context, id int, name string, qb models.Tag
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnsureAliasesUnique(ctx context.Context, id int, aliases []string, qb models.TagQueryer) error {
|
func EnsureAliasesUnique(ctx context.Context, id int, aliases []string, qb models.TagNameFinder) error {
|
||||||
for _, a := range aliases {
|
for _, a := range aliases {
|
||||||
if err := EnsureTagNameUnique(ctx, id, a, qb); err != nil {
|
if err := EnsureTagNameUnique(ctx, id, a, qb); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,60 @@
|
||||||
package tag
|
package tag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/mocks"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func nameFilter(n string) *models.TagFilterType {
|
type tagNameFinderMock struct {
|
||||||
return &models.TagFilterType{
|
existingTags []*models.Tag
|
||||||
Name: &models.StringCriterionInput{
|
|
||||||
Value: n,
|
|
||||||
Modifier: models.CriterionModifierEquals,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func aliasFilter(n string) *models.TagFilterType {
|
func (m tagNameFinderMock) FindByName(ctx context.Context, name string, nocase bool) (*models.Tag, error) {
|
||||||
return &models.TagFilterType{
|
for _, n := range m.existingTags {
|
||||||
Aliases: &models.StringCriterionInput{
|
if n.Name == name {
|
||||||
Value: n,
|
return n, nil
|
||||||
Modifier: models.CriterionModifierEquals,
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m tagNameFinderMock) FindByNames(ctx context.Context, names []string, nocase bool) ([]*models.Tag, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m tagNameFinderMock) FindByAlias(ctx context.Context, alias string, nocase bool) (*models.Tag, error) {
|
||||||
|
for _, n := range m.existingTags {
|
||||||
|
for _, a := range n.Aliases.List() {
|
||||||
|
if a == alias {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnsureAliasesUnique(t *testing.T) {
|
func TestEnsureAliasesUnique(t *testing.T) {
|
||||||
db := mocks.NewDatabase()
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
name1 = "name 1"
|
name1 = "name 1"
|
||||||
name2 = "name 2"
|
name2 = "name 2"
|
||||||
|
name3 = "name 3"
|
||||||
alias1 = "alias 1"
|
alias1 = "alias 1"
|
||||||
newAlias = "new alias"
|
newAlias = "new alias"
|
||||||
)
|
)
|
||||||
|
|
||||||
existing2 := models.Tag{
|
tagMock := tagNameFinderMock{
|
||||||
ID: 2,
|
existingTags: []*models.Tag{
|
||||||
Name: name2,
|
{Name: name1, Aliases: models.NewRelatedStrings([]string{})},
|
||||||
|
{Name: name2, Aliases: models.NewRelatedStrings([]string{})},
|
||||||
|
{Name: name3, Aliases: models.NewRelatedStrings([]string{newAlias})},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pp := 1
|
|
||||||
findFilter := &models.FindFilterType{
|
|
||||||
PerPage: &pp,
|
|
||||||
}
|
|
||||||
|
|
||||||
// name1 matches existing1 name - ok
|
|
||||||
// EnsureAliasesUnique calls EnsureTagNameUnique.
|
|
||||||
// EnsureTagNameUnique calls ByName then ByAlias.
|
|
||||||
|
|
||||||
// Case 1: valid alias
|
|
||||||
// ByName "alias 1" -> nil
|
|
||||||
// ByAlias "alias 1" -> nil
|
|
||||||
db.Tag.On("Query", testCtx, nameFilter(alias1), findFilter).Return(nil, 0, nil)
|
|
||||||
db.Tag.On("Query", testCtx, aliasFilter(alias1), findFilter).Return(nil, 0, nil)
|
|
||||||
|
|
||||||
// Case 2: alias duplicates existing2 name
|
|
||||||
// ByName "name 2" -> existing2
|
|
||||||
db.Tag.On("Query", testCtx, nameFilter(name2), findFilter).Return([]*models.Tag{&existing2}, 1, nil)
|
|
||||||
|
|
||||||
// Case 3: alias duplicates existing2 alias
|
|
||||||
// ByName "new alias" -> nil
|
|
||||||
// ByAlias "new alias" -> existing2
|
|
||||||
db.Tag.On("Query", testCtx, nameFilter(newAlias), findFilter).Return(nil, 0, nil)
|
|
||||||
db.Tag.On("Query", testCtx, aliasFilter(newAlias), findFilter).Return([]*models.Tag{&existing2}, 1, nil)
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
tName string
|
tName string
|
||||||
id int
|
id int
|
||||||
|
|
@ -74,12 +63,12 @@ func TestEnsureAliasesUnique(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{"valid alias", 1, []string{alias1}, nil},
|
{"valid alias", 1, []string{alias1}, nil},
|
||||||
{"alias duplicates other name", 1, []string{name2}, &NameExistsError{name2}},
|
{"alias duplicates other name", 1, []string{name2}, &NameExistsError{name2}},
|
||||||
{"alias duplicates other alias", 1, []string{newAlias}, &NameUsedByAliasError{newAlias, existing2.Name}},
|
{"alias duplicates other alias", 1, []string{newAlias}, &NameUsedByAliasError{newAlias, tagMock.existingTags[2].Name}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.tName, func(t *testing.T) {
|
t.Run(tt.tName, func(t *testing.T) {
|
||||||
got := EnsureAliasesUnique(testCtx, tt.id, tt.aliases, db.Tag)
|
got := EnsureAliasesUnique(testCtx, tt.id, tt.aliases, tagMock)
|
||||||
assert.Equal(t, tt.want, got)
|
assert.Equal(t, tt.want, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "stash",
|
"name": "stash",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "./",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",
|
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|
@ -23,39 +25,39 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/react-slick": "^1.0.0",
|
"@ant-design/react-slick": "^1.0.0",
|
||||||
"@apollo/client": "^3.8.10",
|
"@apollo/client": "3.14",
|
||||||
"@formatjs/intl-getcanonicallocales": "^2.0.5",
|
"@blaineam/videojs-vr": "^3.1.1",
|
||||||
"@formatjs/intl-locale": "^3.0.11",
|
"@formatjs/intl-getcanonicallocales": "^3.2.2",
|
||||||
"@formatjs/intl-numberformat": "^8.3.3",
|
"@formatjs/intl-locale": "^5.3.1",
|
||||||
"@formatjs/intl-pluralrules": "^5.1.8",
|
"@formatjs/intl-numberformat": "^8.15.6",
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
"@formatjs/intl-pluralrules": "^6.3.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
"@fortawesome/free-brands-svg-icons": "^7.2.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
"@fortawesome/free-regular-svg-icons": "^7.2.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.6",
|
"@fortawesome/react-fontawesome": "^0.2.6",
|
||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@silvermine/videojs-airplay": "^1.2.0",
|
"@silvermine/videojs-airplay": "^1.3.0",
|
||||||
"@silvermine/videojs-chromecast": "^1.4.1",
|
"@silvermine/videojs-chromecast": "^1.5.0",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"apollo-upload-client": "^18.0.1",
|
"apollo-upload-client": "18",
|
||||||
"base64-blob": "^1.4.1",
|
"base64-blob": "^1.4.1",
|
||||||
"bootstrap": "^4.6.2",
|
"bootstrap": "^4.6.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.5.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"event-target-polyfill": "^0.0.4",
|
"event-target-polyfill": "^0.0.4",
|
||||||
"flag-icons": "^6.6.6",
|
"flag-icons": "^7.5.0",
|
||||||
"flexbin": "^0.2.0",
|
"flexbin": "^0.2.0",
|
||||||
"formik": "^2.4.5",
|
"formik": "^2.4.9",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"graphql-ws": "^5.14.3",
|
"graphql-ws": "^5.14.3",
|
||||||
"i18n-iso-countries": "^7.5.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.18.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"mousetrap-pause": "^1.0.0",
|
"mousetrap-pause": "^1.0.0",
|
||||||
"normalize-url": "^4.5.1",
|
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-bootstrap": "^1.6.6",
|
"react-bootstrap": "^1.6.6",
|
||||||
"react-datepicker": "^4.10.0",
|
"react-datepicker": "^4.10.0",
|
||||||
|
|
@ -71,21 +73,19 @@
|
||||||
"remark-gfm": "^1.0.0",
|
"remark-gfm": "^1.0.0",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"slick-carousel": "^1.8.1",
|
"slick-carousel": "^1.8.1",
|
||||||
"string.prototype.replaceall": "^1.0.7",
|
"thehandy": "^1.1.0",
|
||||||
"thehandy": "^1.0.3",
|
"ua-parser-js": "^2.0.9",
|
||||||
"ua-parser-js": "^1.0.34",
|
"universal-cookie": "^8.1.0",
|
||||||
"universal-cookie": "^4.0.4",
|
"video.js": "^7.21.7",
|
||||||
"video.js": "^7.21.3",
|
|
||||||
"videojs-abloop": "^1.2.0",
|
"videojs-abloop": "^1.2.0",
|
||||||
"videojs-contrib-dash": "^5.1.1",
|
"videojs-contrib-dash": "^5.1.1",
|
||||||
"videojs-mobile-ui": "^0.8.0",
|
"videojs-mobile-ui": "^0.8.0",
|
||||||
"videojs-seek-buttons": "^3.0.1",
|
"videojs-seek-buttons": "^3.0.1",
|
||||||
"videojs-vr": "1.8.0",
|
"videojs-vtt.js": "^0.15.5",
|
||||||
"videojs-vtt.js": "^0.15.4",
|
"yup": "^1.7.1"
|
||||||
"yup": "^1.3.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.12",
|
"@babel/core": "^7.29.0",
|
||||||
"@graphql-codegen/cli": "^5.0.0",
|
"@graphql-codegen/cli": "^5.0.0",
|
||||||
"@graphql-codegen/time": "^5.0.0",
|
"@graphql-codegen/time": "^5.0.0",
|
||||||
"@graphql-codegen/typescript": "^4.0.1",
|
"@graphql-codegen/typescript": "^4.0.1",
|
||||||
|
|
@ -93,45 +93,44 @@
|
||||||
"@graphql-codegen/typescript-react-apollo": "^4.1.0",
|
"@graphql-codegen/typescript-react-apollo": "^4.1.0",
|
||||||
"@types/apollo-upload-client": "^18.0.0",
|
"@types/apollo-upload-client": "^18.0.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/dom-screen-wake-lock": "^1.0.3",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/mousetrap": "^1.6.15",
|
||||||
"@types/mousetrap": "^1.6.11",
|
"@types/node": "^20.19.37",
|
||||||
"@types/node": "^18.13.0",
|
|
||||||
"@types/react": "^17.0.53",
|
"@types/react": "^17.0.53",
|
||||||
"@types/react-datepicker": "^4.10.0",
|
"@types/react-datepicker": "^4.10.0",
|
||||||
"@types/react-dom": "^17.0.19",
|
"@types/react-dom": "^17.0.19",
|
||||||
"@types/react-helmet": "^6.1.6",
|
"@types/react-helmet": "^6.1.6",
|
||||||
"@types/react-router-bootstrap": "^0.24.5",
|
"@types/react-router-bootstrap": "^0.24.5",
|
||||||
"@types/react-router-hash-link": "^2.4.5",
|
"@types/react-router-hash-link": "^2.4.5",
|
||||||
"@types/three": "^0.154.0",
|
"@types/three": "^0.183.1",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"@types/video.js": "^7.3.51",
|
"@types/video.js": "^7.3.58",
|
||||||
"@types/videojs-mobile-ui": "^0.8.0",
|
"@types/videojs-mobile-ui": "^0.8.3",
|
||||||
"@types/videojs-seek-buttons": "^2.1.0",
|
"@types/videojs-seek-buttons": "^2.1.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
"@typescript-eslint/parser": "^5.52.0",
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
"@vitejs/plugin-legacy": "^5.4.3",
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"eslint": "^8.57.1",
|
||||||
"eslint": "^8.34.0",
|
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.10.2",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-deprecation": "^3.0.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"extract-react-intl-messages": "^4.1.1",
|
"extract-react-intl-messages": "^4.1.1",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.5.8",
|
||||||
"postcss-scss": "^4.0.6",
|
"postcss-scss": "^4.0.9",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.8",
|
||||||
"sass": "^1.58.1",
|
"sass": "^1.98.0",
|
||||||
"stylelint": "^15.10.1",
|
"stylelint": "^17.6.0",
|
||||||
"stylelint-order": "^6.0.2",
|
"stylelint-order": "^8.1.1",
|
||||||
"terser": "^5.9.0",
|
"terser": "^5.46.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "~10.9.2",
|
||||||
"typescript": "~4.8.4",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^5.4.21",
|
"vite": "^7.3.2",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-tsconfig-paths": "^4.0.5"
|
"vite-tsconfig-paths": "^6.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,3 +2,7 @@ onlyBuiltDependencies:
|
||||||
- '@parcel/watcher'
|
- '@parcel/watcher'
|
||||||
- core-js
|
- core-js
|
||||||
- esbuild
|
- esbuild
|
||||||
|
overrides:
|
||||||
|
"yaml@1.10.2": "~1.10.3"
|
||||||
|
"brace-expansion@1.1.12": "~1.1.13"
|
||||||
|
"@xmldom/xmldom@0.8.12": "~0.8.13"
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
declare module "string.prototype.replaceall" {
|
|
||||||
function replaceAll(
|
|
||||||
searchValue: string | RegExp,
|
|
||||||
replaceValue: string
|
|
||||||
): string;
|
|
||||||
function replaceAll(
|
|
||||||
searchValue: string | RegExp,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
replacer: (substring: string, ...args: any[]) => string
|
|
||||||
): string;
|
|
||||||
|
|
||||||
namespace replaceAll {
|
|
||||||
function getPolyfill(): typeof replaceAll;
|
|
||||||
function implementation(): typeof replaceAll;
|
|
||||||
function shim(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default replaceAll;
|
|
||||||
}
|
|
||||||
31
ui/v2.5/src/@types/videojs-contrib-dash.d.ts
vendored
31
ui/v2.5/src/@types/videojs-contrib-dash.d.ts
vendored
|
|
@ -1,31 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
|
||||||
|
|
||||||
declare module "videojs-contrib-dash" {
|
|
||||||
class Html5DashJS {
|
|
||||||
/**
|
|
||||||
* Get a list of hooks for a specific lifecycle.
|
|
||||||
*
|
|
||||||
* @param type the lifecycle to get hooks from
|
|
||||||
* @param hook optionally add a hook to the lifecycle
|
|
||||||
* @return an array of hooks or empty if none
|
|
||||||
*/
|
|
||||||
static hooks(type: string, hook: Function | Function[]): Function[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a function hook to a specific dash lifecycle.
|
|
||||||
*
|
|
||||||
* @param type the lifecycle to hook the function to
|
|
||||||
* @param hook the function or array of functions to attach
|
|
||||||
*/
|
|
||||||
static hook(type: string, hook: Function | Function[]): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a hook from a specific dash lifecycle.
|
|
||||||
*
|
|
||||||
* @param type the lifecycle that the function hooked to
|
|
||||||
* @param hook the hooked function to remove
|
|
||||||
* @return true if the function was removed, false if not found
|
|
||||||
*/
|
|
||||||
static removeHook(type: string, hook: Function): boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
113
ui/v2.5/src/@types/videojs-vr.d.ts
vendored
113
ui/v2.5/src/@types/videojs-vr.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
declare module "videojs-vr" {
|
declare module "@blaineam/videojs-vr" {
|
||||||
import videojs from "video.js";
|
import videojs from "video.js";
|
||||||
// we don't want to depend on THREE.js directly, these are just typedefs for videojs-vr
|
// we don't want to depend on THREE.js directly, these are just typedefs for videojs-vr
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
|
@ -36,61 +36,50 @@ declare module "videojs-vr" {
|
||||||
// Used for Equi-Angular Cubemap videos
|
// Used for Equi-Angular Cubemap videos
|
||||||
| "EAC"
|
| "EAC"
|
||||||
// Used for side-by-side Equi-Angular Cubemap videos
|
// Used for side-by-side Equi-Angular Cubemap videos
|
||||||
| "EAC_LR";
|
| "EAC_LR"
|
||||||
|
// flat screen side-by-side
|
||||||
|
| "SBS_MONO";
|
||||||
|
|
||||||
|
interface mediaItem {
|
||||||
|
title: string;
|
||||||
|
thumbnail: string;
|
||||||
|
url: string;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
type mediaItems = mediaItem[];
|
||||||
|
|
||||||
|
type orientationOffset = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// options are taken verbaitum from the README
|
||||||
interface Options {
|
interface Options {
|
||||||
/**
|
// Projection mode
|
||||||
* Force the cardboard button to display on all devices even if we don't think they support it.
|
projection?: ProjectionType; // see ProjectionType
|
||||||
*
|
sphereDetails?: number; // Sphere mesh detail (higher = smoother)
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
forceCardboard?: boolean;
|
|
||||||
|
|
||||||
/**
|
// VR HUD options
|
||||||
* Whether motion/gyro controls should be enabled.
|
enableVRHud?: boolean; // Enable in-VR controls
|
||||||
*
|
enableVRGallery?: boolean; // Enable in-VR video gallery
|
||||||
* @default true on iOS and Android
|
showHUDOnStart?: boolean; // Show HUD when entering VR
|
||||||
*/
|
hudAutoHideDelay?: number; // Auto-hide HUD after ms (0 to disable)
|
||||||
motionControls?: boolean;
|
hudDistance?: number; // Distance of HUD from viewer
|
||||||
|
hudHeight?: number; // Height of HUD
|
||||||
|
hudScale?: number; // Scale of HUD elements
|
||||||
|
|
||||||
/**
|
// Behavior options
|
||||||
* Defines the projection type.
|
forceCardboard?: boolean; // Force cardboard button on all devices
|
||||||
*
|
motionControls?: boolean; // Enable gyroscope/device orientation
|
||||||
* @default "AUTO"
|
disableTogglePlay?: boolean; // Disable click-to-play
|
||||||
*/
|
|
||||||
projection?: ProjectionType;
|
|
||||||
|
|
||||||
/**
|
// Spatial audio (requires Omnitone library)
|
||||||
* This alters the number of segments in the spherical mesh onto which equirectangular videos are projected.
|
omnitone?: Object; // Pass Omnitone library object
|
||||||
* The default is 32 but in some circumstances you may notice artifacts and need to increase this number.
|
omnitoneOptions?: Record<string, unknown>; // Omnitone configuration
|
||||||
*
|
|
||||||
* @default 32
|
|
||||||
*/
|
|
||||||
sphereDetail?: number;
|
|
||||||
|
|
||||||
/**
|
// Media gallery items
|
||||||
* Enable debug logging for this plugin
|
mediaItems?: mediaItems; // Array of media items for gallery
|
||||||
*
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
debug?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this property to pass the Omnitone library object to the plugin. Please be aware of, the Omnitone library is not included in the build files.
|
|
||||||
*/
|
|
||||||
omnitone?: object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default options for the Omnitone library. Please check available options on https://github.com/GoogleChrome/omnitone
|
|
||||||
*/
|
|
||||||
omnitoneOptions?: object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature to disable the togglePlay manually. This functionality is useful in live events so that users cannot stop the live, but still have a controlBar available.
|
|
||||||
*
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
disableTogglePlay?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlayerMediaInfo {
|
interface PlayerMediaInfo {
|
||||||
|
|
@ -106,11 +95,33 @@ declare module "videojs-vr" {
|
||||||
init(): void;
|
init(): void;
|
||||||
reset(): void;
|
reset(): void;
|
||||||
|
|
||||||
cameraVector: THREE.Vector3;
|
// VR HUD
|
||||||
|
showHUD(): void; // Show the VR HUD
|
||||||
|
hideHUD(): void; // Hide the VR HUD
|
||||||
|
toggleHUD(): void; // Toggle HUD visibility
|
||||||
|
|
||||||
|
// VR Gallery
|
||||||
|
showGallery(): void; // Show the gallery panel
|
||||||
|
hideGallery(): void; // Hide the gallery panel
|
||||||
|
toggleGallery(): void; // Toggle gallery visibility
|
||||||
|
setGalleryItems(mediaItems): void; // Update gallery media items
|
||||||
|
|
||||||
|
// Favorite state
|
||||||
|
setFavoriteState(boolean): void; // Set favorite button state
|
||||||
|
getFavoriteState(): boolean; // Get current favorite state
|
||||||
|
|
||||||
|
// Orientation
|
||||||
|
setOrientationOffset(orientationOffset): void; // Tilt view
|
||||||
|
resetOrientationOffset(): void; // Reset to default orientation
|
||||||
|
recenter(): void; // Recenter VR view
|
||||||
|
|
||||||
|
// Status
|
||||||
|
isPresenting(): boolean; // Check if currently in VR mode
|
||||||
|
|
||||||
camera: THREE.Camera;
|
camera: THREE.Camera;
|
||||||
scene: THREE.Scene;
|
scene: THREE.Scene;
|
||||||
renderer: THREE.Renderer;
|
renderer: THREE.Renderer;
|
||||||
|
cameraVector: THREE.Vector3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import {
|
||||||
CustomFieldsInput,
|
CustomFieldsInput,
|
||||||
formatCustomFieldInput,
|
formatCustomFieldInput,
|
||||||
} from "src/components/Shared/CustomFields";
|
} from "src/components/Shared/CustomFields";
|
||||||
import { cloneDeep } from "@apollo/client/utilities";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import {
|
||||||
CustomFieldsInput,
|
CustomFieldsInput,
|
||||||
formatCustomFieldInput,
|
formatCustomFieldInput,
|
||||||
} from "src/components/Shared/CustomFields";
|
} from "src/components/Shared/CustomFields";
|
||||||
import { cloneDeep } from "@apollo/client/utilities";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
|
||||||
interface IGroupEditPanel {
|
interface IGroupEditPanel {
|
||||||
group: Partial<GQL.GroupDataFragment>;
|
group: Partial<GQL.GroupDataFragment>;
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ import {
|
||||||
CustomFieldsInput,
|
CustomFieldsInput,
|
||||||
formatCustomFieldInput,
|
formatCustomFieldInput,
|
||||||
} from "src/components/Shared/CustomFields";
|
} from "src/components/Shared/CustomFields";
|
||||||
import { cloneDeep } from "@apollo/client/utilities";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
CriterionModifier,
|
CriterionModifier,
|
||||||
CustomFieldCriterionInput,
|
CustomFieldCriterionInput,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { cloneDeep } from "@apollo/client/utilities";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
import { ModifierSelect } from "../ModifierSelect";
|
import { ModifierSelect } from "../ModifierSelect";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
|
|
||||||
|
|
@ -514,8 +514,8 @@ ul.selectable-list {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
// to prevent unnecessary vertical scrollbar
|
// to prevent unnecessary vertical scrollbar
|
||||||
padding-bottom: 0.15rem;
|
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
|
padding-bottom: 0.15rem;
|
||||||
|
|
||||||
.modifier-object {
|
.modifier-object {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
@ -621,8 +621,8 @@ ul.selectable-list {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
// to prevent unnecessary vertical scrollbar
|
// to prevent unnecessary vertical scrollbar
|
||||||
padding-bottom: 0.15rem;
|
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
|
padding-bottom: 0.15rem;
|
||||||
|
|
||||||
.modifier-object {
|
.modifier-object {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ import {
|
||||||
CustomFieldsInput,
|
CustomFieldsInput,
|
||||||
formatCustomFieldInput,
|
formatCustomFieldInput,
|
||||||
} from "src/components/Shared/CustomFields";
|
} from "src/components/Shared/CustomFields";
|
||||||
import { cloneDeep } from "@apollo/client/utilities";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
|
||||||
const isScraper = (
|
const isScraper = (
|
||||||
scraper: GQL.Scraper | GQL.StashBox
|
scraper: GQL.Scraper | GQL.StashBox
|
||||||
|
|
|
||||||
|
|
@ -614,9 +614,17 @@ const PerformerMergeDetails: React.FC<IPerformerMergeDetailsProps> = ({
|
||||||
title={intl.formatMessage({ id: "stash_id" })}
|
title={intl.formatMessage({ id: "stash_id" })}
|
||||||
result={stashIDs}
|
result={stashIDs}
|
||||||
originalField={
|
originalField={
|
||||||
<StashIDsField values={stashIDs?.originalValue ?? []} />
|
<StashIDsField
|
||||||
|
values={stashIDs?.originalValue ?? []}
|
||||||
|
linkType="performers"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
newField={
|
||||||
|
<StashIDsField
|
||||||
|
values={stashIDs?.newValue ?? []}
|
||||||
|
linkType="performers"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
newField={<StashIDsField values={stashIDs?.newValue ?? []} />}
|
|
||||||
onChange={(value) => setStashIDs(value)}
|
onChange={(value) => setStashIDs(value)}
|
||||||
alwaysShow={
|
alwaysShow={
|
||||||
!!stashIDs.originalValue?.length || !!stashIDs.newValue?.length
|
!!stashIDs.originalValue?.length || !!stashIDs.newValue?.length
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import videojs, { VideoJsPlayer } from "video.js";
|
import videojs, { VideoJsPlayer } from "video.js";
|
||||||
import "videojs-vr";
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import "@blaineam/videojs-vr";
|
||||||
// separate type import, otherwise typescript elides the above import
|
// separate type import, otherwise typescript elides the above import
|
||||||
// and the plugin does not get initialized
|
// and the plugin does not get initialized
|
||||||
import type { ProjectionType, Plugin as VideoJsVRPlugin } from "videojs-vr";
|
import type {
|
||||||
|
ProjectionType,
|
||||||
|
Plugin as VideoJsVRPlugin,
|
||||||
|
} from "@blaineam/videojs-vr";
|
||||||
|
|
||||||
export interface VRMenuOptions {
|
export interface VRMenuOptions {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ import {
|
||||||
CustomFieldsInput,
|
CustomFieldsInput,
|
||||||
formatCustomFieldInput,
|
formatCustomFieldInput,
|
||||||
} from "src/components/Shared/CustomFields";
|
} from "src/components/Shared/CustomFields";
|
||||||
import { cloneDeep } from "@apollo/client/utilities";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
|
||||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export const MarkerWallItem: React.FC<
|
||||||
divStyle.top = props.top;
|
divStyle.top = props.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
var handleClick = function handleClick(event: React.MouseEvent) {
|
const handleClick = function (event: React.MouseEvent) {
|
||||||
if (props.selecting && props.onSelectedChanged) {
|
if (props.selecting && props.onSelectedChanged) {
|
||||||
props.onSelectedChanged(!props.selected, event.shiftKey);
|
props.onSelectedChanged(!props.selected, event.shiftKey);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -131,7 +131,8 @@ export const MarkerWallItem: React.FC<
|
||||||
alt={props.photo.alt}
|
alt={props.photo.alt}
|
||||||
onMouseEnter={() => setActive(true)}
|
onMouseEnter={() => setActive(true)}
|
||||||
onMouseLeave={() => setActive(false)}
|
onMouseLeave={() => setActive(false)}
|
||||||
onClick={handleClick}
|
// having a click handler here results in multiple calls to handleClick
|
||||||
|
// due to having the same click handler on the parent div
|
||||||
onError={() => {
|
onError={() => {
|
||||||
props.photo.onError?.(props.photo);
|
props.photo.onError?.(props.photo);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -587,9 +587,17 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||||
title={intl.formatMessage({ id: "stash_id" })}
|
title={intl.formatMessage({ id: "stash_id" })}
|
||||||
result={stashIDs}
|
result={stashIDs}
|
||||||
originalField={
|
originalField={
|
||||||
<StashIDsField values={stashIDs?.originalValue ?? []} />
|
<StashIDsField
|
||||||
|
values={stashIDs?.originalValue ?? []}
|
||||||
|
linkType="scenes"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
newField={
|
||||||
|
<StashIDsField
|
||||||
|
values={stashIDs?.newValue ?? []}
|
||||||
|
linkType="scenes"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
newField={<StashIDsField values={stashIDs?.newValue ?? []} />}
|
|
||||||
onChange={(value) => setStashIDs(value)}
|
onChange={(value) => setStashIDs(value)}
|
||||||
alwaysShow={
|
alwaysShow={
|
||||||
!!stashIDs.originalValue?.length || !!stashIDs.newValue?.length
|
!!stashIDs.originalValue?.length || !!stashIDs.newValue?.length
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export const SceneWallItem: React.FC<
|
||||||
divStyle.top = props.top;
|
divStyle.top = props.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
var handleClick = function handleClick(event: React.MouseEvent) {
|
const handleClick = function (event: React.MouseEvent) {
|
||||||
if (props.selecting && props.onSelectedChanged) {
|
if (props.selecting && props.onSelectedChanged) {
|
||||||
props.onSelectedChanged(!props.selected, event.shiftKey);
|
props.onSelectedChanged(!props.selected, event.shiftKey);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -96,7 +96,8 @@ export const SceneWallItem: React.FC<
|
||||||
alt: props.photo.alt,
|
alt: props.photo.alt,
|
||||||
onMouseEnter: () => setActive(true),
|
onMouseEnter: () => setActive(true),
|
||||||
onMouseLeave: () => setActive(false),
|
onMouseLeave: () => setActive(false),
|
||||||
onClick: handleClick,
|
// having a click handler here results in multiple calls to handleClick
|
||||||
|
// due to having the same click handler on the parent div
|
||||||
onError: () => {
|
onError: () => {
|
||||||
props.photo.onError?.(props.photo);
|
props.photo.onError?.(props.photo);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { CollapseButton } from "./CollapseButton";
|
||||||
import { DetailItem } from "./DetailItem";
|
import { DetailItem } from "./DetailItem";
|
||||||
import { Button, Col, Form, FormGroup, InputGroup, Row } from "react-bootstrap";
|
import { Button, Col, Form, FormGroup, InputGroup, Row } from "react-bootstrap";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { cloneDeep } from "@apollo/client/utilities";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
|
|
||||||
|
|
@ -34,16 +34,20 @@ export const StashIDPill: React.FC<{
|
||||||
|
|
||||||
interface IStashIDsField {
|
interface IStashIDsField {
|
||||||
values: StashId[];
|
values: StashId[];
|
||||||
|
linkType: LinkType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StashIDsField: React.FC<IStashIDsField> = ({ values }) => {
|
export const StashIDsField: React.FC<IStashIDsField> = ({
|
||||||
|
values,
|
||||||
|
linkType,
|
||||||
|
}) => {
|
||||||
if (!values.length) return null;
|
if (!values.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="pl-0 mw-100">
|
<ul className="pl-0 mw-100">
|
||||||
{values.map((v) => (
|
{values.map((v) => (
|
||||||
<li key={v.stash_id} className="row no-gutters">
|
<li key={v.stash_id} className="row no-gutters">
|
||||||
<StashIDPill linkType="scenes" stashID={v} />
|
<StashIDPill linkType={linkType} stashID={v} />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import {
|
||||||
CustomFieldsInput,
|
CustomFieldsInput,
|
||||||
formatCustomFieldInput,
|
formatCustomFieldInput,
|
||||||
} from "src/components/Shared/CustomFields";
|
} from "src/components/Shared/CustomFields";
|
||||||
import { cloneDeep } from "@apollo/client/utilities";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
|
||||||
interface IStudioEditPanel {
|
interface IStudioEditPanel {
|
||||||
studio: Partial<GQL.StudioDataFragment>;
|
studio: Partial<GQL.StudioDataFragment>;
|
||||||
|
|
|
||||||
|
|
@ -303,8 +303,6 @@ export const TaggerContext: React.FC = ({ children }) => {
|
||||||
|
|
||||||
if (results.error) {
|
if (results.error) {
|
||||||
newResult = { error: results.error.message };
|
newResult = { error: results.error.message };
|
||||||
} else if (results.errors) {
|
|
||||||
newResult = { error: results.errors.toString() };
|
|
||||||
} else {
|
} else {
|
||||||
newResult = {
|
newResult = {
|
||||||
results: results.data.scrapeSingleScene.map((r) => ({
|
results: results.data.scrapeSingleScene.map((r) => ({
|
||||||
|
|
@ -339,8 +337,6 @@ export const TaggerContext: React.FC = ({ children }) => {
|
||||||
|
|
||||||
if (results.error) {
|
if (results.error) {
|
||||||
newResult = { error: results.error.message };
|
newResult = { error: results.error.message };
|
||||||
} else if (results.errors) {
|
|
||||||
newResult = { error: results.errors.toString() };
|
|
||||||
} else {
|
} else {
|
||||||
newResult = {
|
newResult = {
|
||||||
results: results.data.scrapeSingleScene.map((r) => ({
|
results: results.data.scrapeSingleScene.map((r) => ({
|
||||||
|
|
@ -401,8 +397,6 @@ export const TaggerContext: React.FC = ({ children }) => {
|
||||||
|
|
||||||
if (results.error) {
|
if (results.error) {
|
||||||
setMultiError(results.error.message);
|
setMultiError(results.error.message);
|
||||||
} else if (results.errors) {
|
|
||||||
setMultiError(results.errors.toString());
|
|
||||||
} else {
|
} else {
|
||||||
const newSearchResults = { ...searchResults };
|
const newSearchResults = { ...searchResults };
|
||||||
sceneIDs.forEach((sceneID, index) => {
|
sceneIDs.forEach((sceneID, index) => {
|
||||||
|
|
|
||||||
|
|
@ -434,7 +434,11 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||||
t: GQL.ScrapedTag,
|
t: GQL.ScrapedTag,
|
||||||
createInput?: GQL.TagCreateInput
|
createInput?: GQL.TagCreateInput
|
||||||
) {
|
) {
|
||||||
const toCreate: GQL.TagCreateInput = createInput ?? { name: t.name };
|
const toCreate: GQL.TagCreateInput = createInput ?? {
|
||||||
|
name: t.name,
|
||||||
|
description: t.description ?? undefined,
|
||||||
|
aliases: t.alias_list?.filter((a) => a) ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
// If the tag has a remote_site_id and we have an endpoint, include the stash_id
|
// If the tag has a remote_site_id and we have an endpoint, include the stash_id
|
||||||
const endpoint = currentSource?.sourceInput.stash_box_endpoint;
|
const endpoint = currentSource?.sourceInput.stash_box_endpoint;
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,10 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
.optional-field-content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.original-scene-details {
|
.original-scene-details {
|
||||||
|
|
@ -278,6 +282,10 @@
|
||||||
.form-check {
|
.form-check {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.lead {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.StudioTagger,
|
.StudioTagger,
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,18 @@ import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { ModalComponent } from "src/components/Shared/Modal";
|
import { ModalComponent } from "src/components/Shared/Modal";
|
||||||
import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faCheck,
|
||||||
|
faExclamationTriangle,
|
||||||
|
faTimes,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import { excludeFields } from "src/utils/data";
|
import { excludeFields } from "src/utils/data";
|
||||||
import { StashIDPill } from "src/components/Shared/StashID";
|
import { StashIDPill } from "src/components/Shared/StashID";
|
||||||
|
|
||||||
interface ITagModalProps {
|
interface ITagModalProps {
|
||||||
tag: GQL.ScrapedSceneTagDataFragment;
|
tag: GQL.ScrapedTag;
|
||||||
modalVisible: boolean;
|
modalVisible: boolean;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
onSave: (input: GQL.TagCreateInput, parentInput?: GQL.TagCreateInput) => void;
|
onSave: (input: GQL.TagCreateInput, parentInput?: GQL.TagCreateInput) => void;
|
||||||
|
|
@ -178,6 +182,15 @@ const TagModal: React.FC<ITagModalProps> = ({
|
||||||
// force create if there is no current parent tag and parent tag is not excluded
|
// force create if there is no current parent tag and parent tag is not excluded
|
||||||
const mustCreateParent = true;
|
const mustCreateParent = true;
|
||||||
|
|
||||||
|
// warn the user if the parent tag does not have a remote_site_id,
|
||||||
|
// which means it won't be automatically linked to the source tag
|
||||||
|
const missingStashIDWarning = !tag.parent.remote_site_id && (
|
||||||
|
<p className="lead">
|
||||||
|
<Icon icon={faExclamationTriangle} className="text-warning" />
|
||||||
|
<FormattedMessage id="tag_tagger.parent_tag_no_remote_site_id_warning" />
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4 mt-4">
|
<div className="mb-4 mt-4">
|
||||||
|
|
@ -192,6 +205,7 @@ const TagModal: React.FC<ITagModalProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{maybeRenderParentTagDetails()}
|
{maybeRenderParentTagDetails()}
|
||||||
|
{missingStashIDWarning}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ const TagTaggerList: React.FC<ITagTaggerListProps> = ({
|
||||||
const [modalTag, setModalTag] = useState<
|
const [modalTag, setModalTag] = useState<
|
||||||
| {
|
| {
|
||||||
existingTag: GQL.TagListDataFragment;
|
existingTag: GQL.TagListDataFragment;
|
||||||
scrapedTag: GQL.ScrapedSceneTagDataFragment;
|
scrapedTag: GQL.ScrapedTag;
|
||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
>();
|
>();
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
CustomFieldsInput,
|
CustomFieldsInput,
|
||||||
formatCustomFieldInput,
|
formatCustomFieldInput,
|
||||||
} from "src/components/Shared/CustomFields";
|
} from "src/components/Shared/CustomFields";
|
||||||
import { cloneDeep } from "@apollo/client/utilities";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
|
|
||||||
interface ITagEditPanel {
|
interface ITagEditPanel {
|
||||||
tag: Partial<GQL.TagDataFragment>;
|
tag: Partial<GQL.TagDataFragment>;
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,7 @@ export const FilteredTagList = PatchComponent(
|
||||||
showModal(
|
showModal(
|
||||||
<ExportDialog
|
<ExportDialog
|
||||||
exportInput={{
|
exportInput={{
|
||||||
studios: {
|
tags: {
|
||||||
ids: Array.from(selectedIds.values()),
|
ids: Array.from(selectedIds.values()),
|
||||||
all: all,
|
all: all,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -321,9 +321,14 @@ const TagMergeDetails: React.FC<ITagMergeDetailsProps> = ({
|
||||||
title={intl.formatMessage({ id: "stash_id" })}
|
title={intl.formatMessage({ id: "stash_id" })}
|
||||||
result={stashIDs}
|
result={stashIDs}
|
||||||
originalField={
|
originalField={
|
||||||
<StashIDsField values={stashIDs?.originalValue ?? []} />
|
<StashIDsField
|
||||||
|
values={stashIDs?.originalValue ?? []}
|
||||||
|
linkType="tags"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
newField={
|
||||||
|
<StashIDsField values={stashIDs?.newValue ?? []} linkType="tags" />
|
||||||
}
|
}
|
||||||
newField={<StashIDsField values={stashIDs?.newValue ?? []} />}
|
|
||||||
onChange={(value) => setStashIDs(value)}
|
onChange={(value) => setStashIDs(value)}
|
||||||
alwaysShow={
|
alwaysShow={
|
||||||
!!stashIDs.originalValue?.length || !!stashIDs.newValue?.length
|
!!stashIDs.originalValue?.length || !!stashIDs.newValue?.length
|
||||||
|
|
|
||||||
|
|
@ -2264,6 +2264,9 @@ export const mutateDeleteFiles = (ids: string[]) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
evictQueries(cache, [
|
evictQueries(cache, [
|
||||||
|
GQL.FindSceneDocument, // files list on scene detail
|
||||||
|
GQL.FindImageDocument, // files list on image detail
|
||||||
|
GQL.FindGalleryDocument, // files list on gallery detail
|
||||||
GQL.FindScenesDocument, // filter by file count
|
GQL.FindScenesDocument, // filter by file count
|
||||||
GQL.FindImagesDocument, // filter by file count
|
GQL.FindImagesDocument, // filter by file count
|
||||||
GQL.FindGalleriesDocument, // filter by file count
|
GQL.FindGalleriesDocument, // filter by file count
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import {
|
import {
|
||||||
ApolloClient,
|
ApolloClient,
|
||||||
InMemoryCache,
|
InMemoryCache,
|
||||||
split,
|
ApolloLink,
|
||||||
from,
|
|
||||||
ServerError,
|
ServerError,
|
||||||
TypePolicies,
|
TypePolicies,
|
||||||
} from "@apollo/client";
|
} from "@apollo/client";
|
||||||
|
|
@ -171,7 +170,7 @@ Please disable it on the server and refresh the page.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const splitLink = split(
|
const splitLink = ApolloLink.split(
|
||||||
({ query }) => {
|
({ query }) => {
|
||||||
const definition = getMainDefinition(query);
|
const definition = getMainDefinition(query);
|
||||||
return (
|
return (
|
||||||
|
|
@ -183,7 +182,7 @@ Please disable it on the server and refresh the page.`);
|
||||||
httpLink
|
httpLink
|
||||||
);
|
);
|
||||||
|
|
||||||
const link = from([errorLink, splitLink]);
|
const link = ApolloLink.from([errorLink, splitLink]);
|
||||||
|
|
||||||
const cache = new InMemoryCache({
|
const cache = new InMemoryCache({
|
||||||
typePolicies,
|
typePolicies,
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue