mirror of
https://github.com/stashapp/stash.git
synced 2026-06-10 21:42:03 -05:00
Compare commits
536 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76648fee66 | ||
|
|
6d07ecf751 | ||
|
|
5283eb8ce3 | ||
|
|
32c48443b5 | ||
|
|
ad00bee393 | ||
|
|
a54996d8a2 | ||
|
|
b6db4c31ca | ||
|
|
f82e24762b | ||
|
|
35b74be585 | ||
|
|
7199d2b5ac | ||
|
|
4697271294 | ||
|
|
3e4515e62a | ||
|
|
58c58beb4a | ||
|
|
f05518860f | ||
|
|
9b567fa6f4 | ||
|
|
c92de09ece | ||
|
|
9765b6d50e | ||
|
|
c6c3754f02 | ||
|
|
76a5b2a06a | ||
|
|
93a2ee1277 | ||
|
|
be6431ac13 | ||
|
|
4dd8dd948e | ||
|
|
e253ba71f8 | ||
|
|
30fc2d1209 | ||
|
|
cef5b46f93 | ||
|
|
c45ae068fc | ||
|
|
a20fbe33c0 | ||
|
|
82f4a8f671 | ||
|
|
33050f700e | ||
|
|
4ad0241c53 | ||
|
|
7e8c764dc7 | ||
|
|
fd9e4b3ec2 | ||
|
|
3abdcbee6f | ||
|
|
476688c84d | ||
|
|
7152be6086 | ||
|
|
e4ef14e830 | ||
|
|
f543046349 | ||
|
|
c9f76a01c5 | ||
|
|
5c4bf4ecdf | ||
|
|
17be7e97d3 | ||
|
|
71e39e5cb8 | ||
|
|
a17199ba21 | ||
|
|
d1c207e40b | ||
|
|
129dd0ffcc | ||
|
|
a3838734c5 | ||
|
|
b897de3e5e | ||
|
|
5407596e0d | ||
|
|
f7a164ffe5 | ||
|
|
653cd16eb2 | ||
|
|
a2153ced52 | ||
|
|
a44993bbf4 | ||
|
|
ba83da1983 | ||
|
|
0a98296642 | ||
|
|
ca970b9706 | ||
|
|
2b288fd67c | ||
|
|
7f1ad30db1 | ||
|
|
5721ea2b70 | ||
|
|
8c2a25b833 | ||
|
|
601a16b5cb | ||
|
|
879c20efc7 | ||
|
|
283f76240f | ||
|
|
ad17e7defe | ||
|
|
7a2e59fcef | ||
|
|
7c09f24f34 | ||
|
|
fb82866512 | ||
|
|
15da2c1f4c | ||
|
|
1dac598755 | ||
|
|
ad442fbee5 | ||
|
|
4e9925fd3f | ||
|
|
7b064ac99e | ||
|
|
a8a3b4cfd9 | ||
|
|
306ba63ab6 | ||
|
|
c21ded028a | ||
|
|
899ee713ab | ||
|
|
a3c34a51aa | ||
|
|
010a355e0b | ||
|
|
bcf0fda7ac | ||
|
|
96fdd94a01 | ||
|
|
68738bd227 | ||
|
|
8133aa8c91 | ||
|
|
ae1841efb0 | ||
|
|
27aef4ac2e | ||
|
|
b3d6a8eedd | ||
|
|
a023a86ca6 | ||
|
|
294e2090d0 | ||
|
|
c69d72b243 | ||
|
|
cdea9374d8 | ||
|
|
b1b223c90a | ||
|
|
c74456c07e | ||
|
|
ca55f96fd8 | ||
|
|
b7799df2a6 | ||
|
|
10341fba58 | ||
|
|
996dfb1c2f | ||
|
|
ce47efc415 | ||
|
|
3089e1ad69 | ||
|
|
62ff6f3c7f | ||
|
|
e49beb139c | ||
|
|
d8ee57cd50 | ||
|
|
427c18be7d | ||
|
|
7788a6fd07 | ||
|
|
49060e6686 | ||
|
|
a94bf29b34 | ||
|
|
ecb53cee55 | ||
|
|
fb77e18182 | ||
|
|
c47aafff66 | ||
|
|
aa1894964f | ||
|
|
c8d4dacffd | ||
|
|
c79f299d1a | ||
|
|
6a5dc4e774 | ||
|
|
540d72bc44 | ||
|
|
d96850c008 | ||
|
|
48c6373afa | ||
|
|
5512d37da3 | ||
|
|
f79677ba96 | ||
|
|
bfd8e81ffd | ||
|
|
720b233be6 | ||
|
|
3ddfafa831 | ||
|
|
f598fa71da | ||
|
|
6cebf146cb | ||
|
|
d0caf87eeb | ||
|
|
a0b082a36d | ||
|
|
ec23b26c60 | ||
|
|
15a7b8a859 | ||
|
|
b69d9cc840 | ||
|
|
12917f51d0 | ||
|
|
a3e72b61ee | ||
|
|
2739696813 | ||
|
|
f477b996b5 | ||
|
|
70250c93f1 | ||
|
|
4cca3b298d | ||
|
|
436ae0a027 | ||
|
|
dc3ce2b414 | ||
|
|
4244bd0b18 | ||
|
|
cbd273a19c | ||
|
|
2a373a25ca | ||
|
|
e116775d60 | ||
|
|
b7f938531b | ||
|
|
c5bafeb15c | ||
|
|
205b24499b | ||
|
|
48035061ec | ||
|
|
af6841be49 | ||
|
|
d986a9eb4f | ||
|
|
3156191b83 | ||
|
|
593207866f | ||
|
|
1f5377da1c | ||
|
|
a4e25f32ea | ||
|
|
6775a28ec7 | ||
|
|
a8fca47a8c | ||
|
|
2b1a57c6d0 | ||
|
|
a7e5ccd080 | ||
|
|
a1fc14f8c4 | ||
|
|
9c13b39f99 | ||
|
|
b3d35dfae4 | ||
|
|
f26766033e | ||
|
|
fda4776d30 | ||
|
|
f9a624b803 | ||
|
|
4be60310c3 | ||
|
|
2d483f2d11 | ||
|
|
e18c050fb1 | ||
|
|
da4d49d940 | ||
|
|
845d718c67 | ||
|
|
ed057c971f | ||
|
|
94a978d063 | ||
|
|
dcb86d9186 | ||
|
|
62bdff351d | ||
|
|
bf25759a57 | ||
|
|
621e890a48 | ||
|
|
e843c890fb | ||
|
|
ff23d4e20b | ||
|
|
e4b89064b1 | ||
|
|
efede32dd7 | ||
|
|
d1998cb5b0 | ||
|
|
60446af145 | ||
|
|
dbfa450ace | ||
|
|
4b8af18fab | ||
|
|
124ea609fe | ||
|
|
0a07194110 | ||
|
|
b232e58b06 | ||
|
|
b3f8839ef7 | ||
|
|
540e80c86b | ||
|
|
eec31723bd | ||
|
|
3b146588c6 | ||
|
|
2b699fcf95 | ||
|
|
d6158d70a9 | ||
|
|
cf45ac883e | ||
|
|
e4267a0d83 | ||
|
|
2ca53714a6 | ||
|
|
0ff0f9c8ec | ||
|
|
9c8bd853c5 | ||
|
|
bf0e0f2210 | ||
|
|
c314515b8f | ||
|
|
28b5fbfd4d | ||
|
|
3dd218e1ba | ||
|
|
eb67f7f4d6 | ||
|
|
98d210f7f9 | ||
|
|
4794a1d453 | ||
|
|
77ef16570b | ||
|
|
99d97804f4 | ||
|
|
89553864f5 | ||
|
|
865208844c | ||
|
|
062d566195 | ||
|
|
bfc60bb23f | ||
|
|
0fa71be697 | ||
|
|
5ba1ea8fbc | ||
|
|
4d3dc0aec8 | ||
|
|
b12269e477 | ||
|
|
e32593023e | ||
|
|
3e3e8b95e2 | ||
|
|
769540be55 | ||
|
|
1ffca39e1d | ||
|
|
dd84714a16 | ||
|
|
ad844a225c | ||
|
|
ca5febc65b | ||
|
|
c8aeb7966a | ||
|
|
1d565a7cbd | ||
|
|
408d6fc988 | ||
|
|
237a904ca4 | ||
|
|
12af7d6515 | ||
|
|
77ee620877 | ||
|
|
c5fef3977e | ||
|
|
29859fa4ad | ||
|
|
1cee1ccfe2 | ||
|
|
9cc26f7b75 | ||
|
|
c5abe28375 | ||
|
|
1b99a03847 | ||
|
|
22d14fd89e | ||
|
|
0bba8889b8 | ||
|
|
141f60f8fb | ||
|
|
560bdcd60d | ||
|
|
c43e7b4351 | ||
|
|
4c0d9d0a07 | ||
|
|
157b2e7bae | ||
|
|
ec6acab2f4 | ||
|
|
911da87264 | ||
|
|
f7b87379d4 | ||
|
|
ad60f0ebd6 | ||
|
|
c83635c7a8 | ||
|
|
034fd4407d | ||
|
|
7086109d78 | ||
|
|
a369613d42 | ||
|
|
62b8ffb2b6 | ||
|
|
213c2830d1 | ||
|
|
32770203ba | ||
|
|
8c454582c7 | ||
|
|
e5929389b4 | ||
|
|
fa172c2dfd | ||
|
|
9ceea952b6 | ||
|
|
49cd214c9d | ||
|
|
3d0a8f653a | ||
|
|
ae6d1a8109 | ||
|
|
7ac7963972 | ||
|
|
bf7cb78d6d | ||
|
|
95d0e5dd34 | ||
|
|
d995ce7ecb | ||
|
|
3521dc133e | ||
|
|
9f5b1c33f6 | ||
|
|
c5bc106c1a | ||
|
|
9735d0fad1 | ||
|
|
353d889fd5 | ||
|
|
c7b2314bb1 | ||
|
|
4614471ad9 | ||
|
|
7733a214d3 | ||
|
|
cd2f0922ab | ||
|
|
f1f6e84aa0 | ||
|
|
22986097c4 | ||
|
|
409a200ebc | ||
|
|
20ac388f77 | ||
|
|
0626a7aea1 | ||
|
|
2ca9e0f43a | ||
|
|
b4823bec8a | ||
|
|
945188a0ba | ||
|
|
b59afd2dcd | ||
|
|
9202787be0 | ||
|
|
4999e85fae | ||
|
|
2bdf0d9e62 | ||
|
|
2e00cb6c5a | ||
|
|
33857122b8 | ||
|
|
768f74a0b3 | ||
|
|
98c428ba4e | ||
|
|
fcf249e5f6 | ||
|
|
3a56dd98db | ||
|
|
48c287ed76 | ||
|
|
9c6fbfc16f | ||
|
|
6a9175c954 | ||
|
|
56896d7c7d | ||
|
|
2e35221003 | ||
|
|
ba1ebba6c0 | ||
|
|
4a3ce8b6ec | ||
|
|
4b84ec0d85 | ||
|
|
a302fc78ea | ||
|
|
f2bc3d5567 | ||
|
|
a303446bb7 | ||
|
|
0c2a2190e5 | ||
|
|
a8c909e0c9 | ||
|
|
c4a91d15a6 | ||
|
|
61bd9233b2 | ||
|
|
37acd6b79b | ||
|
|
5bb9bf902c | ||
|
|
76e5598876 | ||
|
|
8b1d4ccc97 | ||
|
|
cff068f519 | ||
|
|
276bc5a8cb | ||
|
|
b4a6cc43d1 | ||
|
|
777fb44ac6 | ||
|
|
f5a42ede2d | ||
|
|
7bb38ae6dc | ||
|
|
7d56f1a093 | ||
|
|
afd7f02644 | ||
|
|
93b851eae6 | ||
|
|
1dfb960a87 | ||
|
|
e231812203 | ||
|
|
e7f610ce18 | ||
|
|
6e9718a600 | ||
|
|
6fb1c41ae9 | ||
|
|
5aba3c1a98 | ||
|
|
440c261f5b | ||
|
|
8fc997dfe9 | ||
|
|
5b9bdadaec | ||
|
|
706b61233f | ||
|
|
aaf3114194 | ||
|
|
15aac68a14 | ||
|
|
dad4ab6a6f | ||
|
|
e9703e9a6e | ||
|
|
46eb01198a | ||
|
|
235c9c90c2 | ||
|
|
a4bbdcfbae | ||
|
|
8c410a9a14 | ||
|
|
9981574e82 | ||
|
|
79e72ff3bc | ||
|
|
a16f3da33e | ||
|
|
8770e81ec5 | ||
|
|
9284ede0fb | ||
|
|
2d73912f15 | ||
|
|
9ac6505241 | ||
|
|
a402ee5fa7 | ||
|
|
a8df95c3a4 | ||
|
|
330581283a | ||
|
|
892d74c98b | ||
|
|
de2b28d3f9 | ||
|
|
217c02f181 | ||
|
|
3ea31aeb76 | ||
|
|
cf8efa9035 | ||
|
|
1d0fa27c71 | ||
|
|
0b82dbf666 | ||
|
|
11cafe933a | ||
|
|
d82c526ada | ||
|
|
1588d1cb4e | ||
|
|
64f2071d8c | ||
|
|
3573795cf7 | ||
|
|
723211a620 | ||
|
|
dd8da7f339 | ||
|
|
e7311a60d2 | ||
|
|
29677696fd | ||
|
|
403f7c54ef | ||
|
|
75099b38a8 | ||
|
|
45e2e12594 | ||
|
|
ec547e8d30 | ||
|
|
e470dc5f52 | ||
|
|
14bde44597 | ||
|
|
aeb68a5851 | ||
|
|
5cf28cf8af | ||
|
|
08b73581a6 | ||
|
|
95a2c8d13f | ||
|
|
0b131f76df | ||
|
|
6271f18979 | ||
|
|
ca976a0994 | ||
|
|
9859ec61fb | ||
|
|
a998497004 | ||
|
|
f5e3fe77b7 | ||
|
|
743ab9a52c | ||
|
|
d23cecfc18 | ||
|
|
d8990e655d | ||
|
|
5b9a96b843 | ||
|
|
b968aa3f31 | ||
|
|
910c7025dc | ||
|
|
ea503833c5 | ||
|
|
6848dec5f4 | ||
|
|
bd7d4ac7ff | ||
|
|
5a6504b4ba | ||
|
|
f8a93789bb | ||
|
|
82cbeff9b5 | ||
|
|
f32d60f208 | ||
|
|
3e9bd8507f | ||
|
|
6ee7e6112b | ||
|
|
9bd36408ee | ||
|
|
0cdea209bb | ||
|
|
e1782d094d | ||
|
|
d258976358 | ||
|
|
afda6decf2 | ||
|
|
a1bd7cf817 | ||
|
|
23b4d4f1e0 | ||
|
|
d0d0d1e11f | ||
|
|
e304d981d0 | ||
|
|
e8af3c8e98 | ||
|
|
8c922ed9e1 | ||
|
|
c9501ef881 | ||
|
|
26c3873122 | ||
|
|
2ef2d89b06 | ||
|
|
43a9df8621 | ||
|
|
7a9cb548ab | ||
|
|
74ddfa47e9 | ||
|
|
d37de0e49b | ||
|
|
eca5838ce0 | ||
|
|
ccb1b7c3c4 | ||
|
|
d4ef182871 | ||
|
|
d24b52ae7f | ||
|
|
46e1715a59 | ||
|
|
5ebd3b3568 | ||
|
|
54461aa140 | ||
|
|
1659c8f185 | ||
|
|
599deb71b6 | ||
|
|
413311711f | ||
|
|
d1018b4c5d | ||
|
|
b78771dbcd | ||
|
|
fc1fc20df4 | ||
|
|
b915428f06 | ||
|
|
11be56cc42 | ||
|
|
910ff27730 | ||
|
|
11a1e49292 | ||
|
|
0e7c58a5a6 | ||
|
|
a8140c11ec | ||
|
|
0dcd58763f | ||
|
|
987fa80786 | ||
|
|
d95ef4059a | ||
|
|
13a24a634d | ||
|
|
b0a34a3dc0 | ||
|
|
343660c378 | ||
|
|
611a1e7854 | ||
|
|
2ea35c4dbc | ||
|
|
f9e11813f0 | ||
|
|
049a1b15c3 | ||
|
|
a0f33e3dab | ||
|
|
61f4d8bd12 | ||
|
|
959f2531fd | ||
|
|
65b416a2d9 | ||
|
|
222475df82 | ||
|
|
4dd4c3c658 | ||
|
|
72779e618d | ||
|
|
4715c5ebb2 | ||
|
|
d96558704a | ||
|
|
795af64e8e | ||
|
|
9621213424 | ||
|
|
cc6673f276 | ||
|
|
2b8c2534dd | ||
|
|
552f86586a | ||
|
|
c4d7a7ab2c | ||
|
|
298f3d4e19 | ||
|
|
87bdbb2058 | ||
|
|
b99d16b712 | ||
|
|
24984da16e | ||
|
|
2b8718100b | ||
|
|
06d8353f4f | ||
|
|
939bb422d1 | ||
|
|
339b9fcc16 | ||
|
|
a83dfff5ff | ||
|
|
21baa23fc5 | ||
|
|
b6714fafba | ||
|
|
a9ab1fcca7 | ||
|
|
5e0f27bed2 | ||
|
|
789de2d5f6 | ||
|
|
2fd7141f0f | ||
|
|
bdf705fe7c | ||
|
|
2ec948a836 | ||
|
|
7605eec6da | ||
|
|
8eb069054e | ||
|
|
b6808dc714 | ||
|
|
e5af37efbc | ||
|
|
409f8fc70c | ||
|
|
90dfaf668b | ||
|
|
bc261f789a | ||
|
|
9552273478 | ||
|
|
33f2ebf2a3 | ||
|
|
40bcb4baa5 | ||
|
|
479e716385 | ||
|
|
06c9d6f554 | ||
|
|
1f0f5eb49c | ||
|
|
c109a58231 | ||
|
|
eca2f9b97d | ||
|
|
623dc8392e | ||
|
|
b5117fd67b | ||
|
|
65fa81b6be | ||
|
|
7894d0fc1f | ||
|
|
0dbe3e6ea8 | ||
|
|
bc5df7cfaf | ||
|
|
9d5cc54cdc | ||
|
|
165528f7b6 | ||
|
|
9577600804 | ||
|
|
a369e395e7 | ||
|
|
bba60cc45b | ||
|
|
ce55392461 | ||
|
|
22350d38bc | ||
|
|
9f5bcca1eb | ||
|
|
462943a903 | ||
|
|
636b0a3167 | ||
|
|
36e9ed7a6c | ||
|
|
7ba7df052d | ||
|
|
3aa3515b9c | ||
|
|
81f39bc2f4 | ||
|
|
62173a924b | ||
|
|
0b7dcbe899 | ||
|
|
f51ac81749 | ||
|
|
a25286bdcb | ||
|
|
9f4d0af886 | ||
|
|
0d13eec9a2 | ||
|
|
4a9fdc8b55 | ||
|
|
b36aa745d8 | ||
|
|
da38ec03c0 | ||
|
|
798db1a8ea | ||
|
|
06d76307c3 | ||
|
|
953867f611 | ||
|
|
f237a58cbc | ||
|
|
24e4719abc | ||
|
|
cf3301c8bc | ||
|
|
50c4ac98af | ||
|
|
7a9214375b | ||
|
|
c38e05cfa0 | ||
|
|
c364346a59 | ||
|
|
20520a58b4 | ||
|
|
fca162f1ca | ||
|
|
8abb3c0d08 | ||
|
|
371a74d445 | ||
|
|
db550c4e9c | ||
|
|
21356e7838 | ||
|
|
1f3ed07188 | ||
|
|
922aef3e5a | ||
|
|
e40b3d78b2 | ||
|
|
3dc01a9362 | ||
|
|
c2b93676dd | ||
|
|
332347737a | ||
|
|
411ebb8195 | ||
|
|
07897465e7 | ||
|
|
113f0b7d77 | ||
|
|
58b6ca3f4b | ||
|
|
87e12319e4 | ||
|
|
efc7b01cf6 | ||
|
|
1591180070 |
@@ -17,7 +17,7 @@
|
||||
|
||||
# GraphQL generated output
|
||||
pkg/models/generated_*.go
|
||||
ui/v2.5/src/core/generated-*.tsx
|
||||
ui/v2.5/src/core/generated-graphql.ts
|
||||
|
||||
####
|
||||
# Jetbrains
|
||||
|
||||
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: stashapp
|
||||
# patreon: # Replace with a single Patreon username
|
||||
open_collective: stashapp
|
||||
# ko_fi: # Replace with a single Ko-fi username
|
||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
# liberapay: StashApp
|
||||
# issuehunt: # Replace with a single IssueHunt username
|
||||
# otechie: # Replace with a single Otechie username
|
||||
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
58
.github/workflows/build.yml
vendored
58
.github/workflows/build.yml
vendored
@@ -12,7 +12,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:7
|
||||
COMPILER_IMAGE: stashapp/compiler:9
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -23,6 +23,11 @@ jobs:
|
||||
- name: Checkout
|
||||
run: git fetch --prune --unshallow --tags
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Pull compiler image
|
||||
run: docker pull $COMPILER_IMAGE
|
||||
|
||||
@@ -84,13 +89,16 @@ jobs:
|
||||
|
||||
- name: Compile for all supported platforms
|
||||
run: |
|
||||
docker exec -t build /bin/bash -c "make cross-compile-windows"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-macos-intel"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-macos-applesilicon"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-linux"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-linux-arm64v8"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-linux-arm32v7"
|
||||
docker exec -t build /bin/bash -c "make cross-compile-linux-arm32v6"
|
||||
docker exec -t build /bin/bash -c "make build-cc-windows"
|
||||
docker exec -t build /bin/bash -c "make build-cc-macos"
|
||||
docker exec -t build /bin/bash -c "make build-cc-linux"
|
||||
docker exec -t build /bin/bash -c "make build-cc-linux-arm64v8"
|
||||
docker exec -t build /bin/bash -c "make build-cc-linux-arm32v7"
|
||||
docker exec -t build /bin/bash -c "make build-cc-linux-arm32v6"
|
||||
docker exec -t build /bin/bash -c "make build-cc-freebsd"
|
||||
|
||||
- name: Zip UI
|
||||
run: docker exec -t build /bin/bash -c "make zip-ui"
|
||||
|
||||
- name: Cleanup build container
|
||||
run: docker rm -f -v build
|
||||
@@ -98,34 +106,42 @@ jobs:
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
|
||||
sha1sum dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
|
||||
sha1sum dist/Stash.app.zip dist/stash-* dist/stash-ui.zip | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
|
||||
echo "STASH_VERSION=$(git describe --tags --exclude latest_develop)" >> $GITHUB_ENV
|
||||
echo "RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Windows binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stash-win.exe
|
||||
path: dist/stash-win.exe
|
||||
|
||||
- name: Upload OSX binary
|
||||
- name: Upload macOS binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stash-macos-intel
|
||||
path: dist/stash-macos-intel
|
||||
name: stash-macos
|
||||
path: dist/stash-macos
|
||||
|
||||
- name: Upload Linux binary
|
||||
# only upload binaries for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stash-linux
|
||||
path: dist/stash-linux
|
||||
|
||||
- name: Upload UI
|
||||
# only upload for pull requests
|
||||
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stash-ui.zip
|
||||
path: dist/stash-ui.zip
|
||||
|
||||
- name: Update latest_develop tag
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
run : git tag -f latest_develop; git push -f --tags
|
||||
@@ -139,13 +155,15 @@ jobs:
|
||||
automatic_release_tag: latest_develop
|
||||
title: "${{ env.STASH_VERSION }}: Latest development build"
|
||||
files: |
|
||||
dist/stash-macos-intel
|
||||
dist/stash-macos-applesilicon
|
||||
dist/Stash.app.zip
|
||||
dist/stash-macos
|
||||
dist/stash-win.exe
|
||||
dist/stash-linux
|
||||
dist/stash-linux-arm64v8
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-linux-arm32v6
|
||||
dist/stash-freebsd
|
||||
dist/stash-ui.zip
|
||||
CHECKSUMS_SHA1
|
||||
|
||||
- name: Master release
|
||||
@@ -157,13 +175,15 @@ jobs:
|
||||
token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
allow_override: true
|
||||
files: |
|
||||
dist/stash-macos-intel
|
||||
dist/stash-macos-applesilicon
|
||||
dist/Stash.app.zip
|
||||
dist/stash-macos
|
||||
dist/stash-win.exe
|
||||
dist/stash-linux
|
||||
dist/stash-linux-arm64v8
|
||||
dist/stash-linux-arm32v7
|
||||
dist/stash-linux-arm32v6
|
||||
dist/stash-freebsd
|
||||
dist/stash-ui.zip
|
||||
CHECKSUMS_SHA1
|
||||
gzip: false
|
||||
|
||||
|
||||
30
.github/workflows/golangci-lint.yml
vendored
30
.github/workflows/golangci-lint.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:7
|
||||
COMPILER_IMAGE: stashapp/compiler:9
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
@@ -21,6 +21,11 @@ jobs:
|
||||
- name: Checkout
|
||||
run: git fetch --prune --unshallow --tags
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Pull compiler image
|
||||
run: docker pull $COMPILER_IMAGE
|
||||
|
||||
@@ -33,7 +38,7 @@ jobs:
|
||||
run: docker exec -t build /bin/bash -c "make generate-backend"
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||
version: latest
|
||||
@@ -42,19 +47,26 @@ jobs:
|
||||
# working-directory: somedir
|
||||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
args: --modules-download-mode=vendor --timeout=5m
|
||||
#
|
||||
# Note: By default, the `.golangci.yml` file should be at the root of the repository.
|
||||
# The location of the configuration file can be changed by using `--config=`
|
||||
args: --timeout=5m
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
# Optional: if set to true then the action will use pre-installed Go.
|
||||
# skip-go-installation: true
|
||||
# Optional: if set to true, then all caching functionality will be completely disabled,
|
||||
# takes precedence over all other caching options.
|
||||
# skip-cache: true
|
||||
|
||||
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
|
||||
skip-pkg-cache: true
|
||||
# Optional: if set to true, then the action won't cache or restore ~/go/pkg.
|
||||
# skip-pkg-cache: true
|
||||
|
||||
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
|
||||
skip-build-cache: true
|
||||
# Optional: if set to true, then the action won't cache or restore ~/.cache/go-build.
|
||||
# skip-build-cache: true
|
||||
|
||||
# Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
|
||||
# install-mode: "goinstall"
|
||||
|
||||
- name: Cleanup build container
|
||||
run: docker rm -f -v build
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -2,6 +2,9 @@
|
||||
# Go
|
||||
####
|
||||
|
||||
# Vendored dependencies
|
||||
vendor
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
@@ -18,11 +21,6 @@
|
||||
# GraphQL generated output
|
||||
internal/api/generated_*.go
|
||||
|
||||
####
|
||||
# Jetbrains
|
||||
####
|
||||
|
||||
|
||||
####
|
||||
# Visual Studio
|
||||
####
|
||||
@@ -49,9 +47,6 @@ internal/api/generated_*.go
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Goland Junk
|
||||
pkg/pkg
|
||||
|
||||
####
|
||||
# Random
|
||||
####
|
||||
@@ -61,6 +56,7 @@ node_modules
|
||||
*.db
|
||||
|
||||
/stash
|
||||
/Stash.app
|
||||
/phasher
|
||||
dist
|
||||
.DS_Store
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# options for analysis running
|
||||
run:
|
||||
timeout: 5m
|
||||
modules-download-mode: vendor
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
@@ -16,11 +15,11 @@ linters:
|
||||
- unused
|
||||
# Linters added by the stash project.
|
||||
# - contextcheck
|
||||
- copyloopvar
|
||||
- dogsled
|
||||
- errchkjson
|
||||
- errorlint
|
||||
# - exhaustive
|
||||
- exportloopref
|
||||
- gocritic
|
||||
# - goerr113
|
||||
- gofmt
|
||||
|
||||
4
.mockery.yml
Normal file
4
.mockery.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
dir: ./pkg/models
|
||||
name: ".*ReaderWriter"
|
||||
outpkg: mocks
|
||||
output: ./pkg/models/mocks
|
||||
314
Makefile
314
Makefile
@@ -9,9 +9,11 @@ endif
|
||||
ifdef IS_WIN_SHELL
|
||||
RM := del /s /q
|
||||
RMDIR := rmdir /s /q
|
||||
NOOP := @@
|
||||
else
|
||||
RM := rm -f
|
||||
RMDIR := rm -rf
|
||||
NOOP := @:
|
||||
endif
|
||||
|
||||
# set LDFLAGS environment variable to any extra ldflags required
|
||||
@@ -36,7 +38,7 @@ GO_BUILD_FLAGS := $(GO_BUILD_FLAGS)
|
||||
|
||||
# set GO_BUILD_TAGS environment variable to any extra build tags required
|
||||
GO_BUILD_TAGS := $(GO_BUILD_TAGS)
|
||||
GO_BUILD_TAGS += sqlite_stat4
|
||||
GO_BUILD_TAGS += sqlite_stat4 sqlite_math_functions
|
||||
|
||||
# set STASH_NOLEGACY environment variable or uncomment to disable legacy browser support
|
||||
# STASH_NOLEGACY := true
|
||||
@@ -46,33 +48,49 @@ GO_BUILD_TAGS += sqlite_stat4
|
||||
|
||||
export CGO_ENABLED := 1
|
||||
|
||||
# define COMPILER_IMAGE for cross-compilation docker container
|
||||
ifndef COMPILER_IMAGE
|
||||
COMPILER_IMAGE := stashapp/compiler:latest
|
||||
endif
|
||||
|
||||
.PHONY: release
|
||||
release: pre-ui generate ui build-release
|
||||
|
||||
# targets to set various build flags
|
||||
# use combinations on the make command-line to configure a build, e.g.:
|
||||
# for a static-pie release build: `make flags-static-pie flags-release stash`
|
||||
# for a static windows debug build: `make flags-static-windows stash`
|
||||
|
||||
# $(NOOP) prevents "nothing to be done" warnings
|
||||
|
||||
.PHONY: flags-release
|
||||
flags-release:
|
||||
flags-release:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -s -w)
|
||||
$(eval GO_BUILD_FLAGS += -trimpath)
|
||||
|
||||
.PHONY: flags-pie
|
||||
flags-pie:
|
||||
$(NOOP)
|
||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||
|
||||
.PHONY: flags-static
|
||||
flags-static:
|
||||
flags-static:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -extldflags=-static)
|
||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
|
||||
|
||||
.PHONY: flags-static-pie
|
||||
flags-static-pie:
|
||||
flags-static-pie:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -extldflags=-static-pie)
|
||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
|
||||
|
||||
# identical to flags-static-pie, but excluding netgo, which is not needed on windows
|
||||
.PHONY: flags-static-windows
|
||||
flags-static-windows:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -extldflags=-static-pie)
|
||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo)
|
||||
@@ -105,166 +123,141 @@ build-flags: build-info
|
||||
stash: build-flags
|
||||
go build $(STASH_OUTPUT) $(BUILD_FLAGS) ./cmd/stash
|
||||
|
||||
.PHONY: stash-release
|
||||
stash-release: flags-release
|
||||
stash-release: flags-pie
|
||||
stash-release: stash
|
||||
|
||||
.PHONY: stash-release-static
|
||||
stash-release-static: flags-release
|
||||
stash-release-static: flags-static-pie
|
||||
stash-release-static: stash
|
||||
|
||||
.PHONY: stash-release-static-windows
|
||||
stash-release-static-windows: flags-release
|
||||
stash-release-static-windows: flags-static-windows
|
||||
stash-release-static-windows: stash
|
||||
|
||||
.PHONY: phasher
|
||||
phasher: build-flags
|
||||
go build $(PHASHER_OUTPUT) $(BUILD_FLAGS) ./cmd/phasher
|
||||
|
||||
.PHONY: phasher-release
|
||||
phasher-release: flags-release
|
||||
phasher-release: flags-pie
|
||||
phasher-release: phasher
|
||||
|
||||
.PHONY: phasher-release-static
|
||||
phasher-release-static: flags-release
|
||||
phasher-release-static: flags-static-pie
|
||||
phasher-release-static: phasher
|
||||
|
||||
.PHONY: phasher-release-static-windows
|
||||
phasher-release-static-windows: flags-release
|
||||
phasher-release-static-windows: flags-static-windows
|
||||
phasher-release-static-windows: phasher
|
||||
|
||||
# builds dynamically-linked debug binaries
|
||||
.PHONY: build
|
||||
build: stash phasher
|
||||
|
||||
# builds dynamically-linked release binaries
|
||||
# builds dynamically-linked PIE release binaries
|
||||
.PHONY: build-release
|
||||
build-release: stash-release phasher-release
|
||||
build-release: flags-release flags-pie build
|
||||
|
||||
# builds statically-linked release binaries
|
||||
.PHONY: build-release-static
|
||||
build-release-static: stash-release-static phasher-release-static
|
||||
# compile and bundle into Stash.app
|
||||
# for when on macOS itself
|
||||
.PHONY: stash-macapp
|
||||
stash-macapp: STASH_OUTPUT := -o stash
|
||||
stash-macapp: flags-release flags-pie stash
|
||||
rm -rf Stash.app
|
||||
cp -R scripts/macos-bundle Stash.app
|
||||
mkdir Stash.app/Contents/MacOS
|
||||
cp stash Stash.app/Contents/MacOS/stash
|
||||
|
||||
# build-release-static, but excluding netgo, which is not needed on windows
|
||||
.PHONY: build-release-static-windows
|
||||
build-release-static-windows: stash-release-static-windows phasher-release-static-windows
|
||||
# build-cc- targets should be run within the compiler docker container
|
||||
|
||||
# cross-compile- targets should be run within the compiler docker container
|
||||
.PHONY: cross-compile-windows
|
||||
cross-compile-windows: export GOOS := windows
|
||||
cross-compile-windows: export GOARCH := amd64
|
||||
cross-compile-windows: export CC := x86_64-w64-mingw32-gcc
|
||||
cross-compile-windows: export CXX := x86_64-w64-mingw32-g++
|
||||
cross-compile-windows: STASH_OUTPUT := -o dist/stash-win.exe
|
||||
cross-compile-windows: PHASHER_OUTPUT := -o dist/phasher-win.exe
|
||||
cross-compile-windows: flags-release
|
||||
cross-compile-windows: flags-static-windows
|
||||
cross-compile-windows: build
|
||||
.PHONY: build-cc-windows
|
||||
build-cc-windows: export GOOS := windows
|
||||
build-cc-windows: export GOARCH := amd64
|
||||
build-cc-windows: export CC := x86_64-w64-mingw32-gcc
|
||||
build-cc-windows: STASH_OUTPUT := -o dist/stash-win.exe
|
||||
build-cc-windows: PHASHER_OUTPUT :=-o dist/phasher-win.exe
|
||||
build-cc-windows: flags-release
|
||||
build-cc-windows: flags-static-windows
|
||||
build-cc-windows: build
|
||||
|
||||
.PHONY: cross-compile-macos-intel
|
||||
cross-compile-macos-intel: export GOOS := darwin
|
||||
cross-compile-macos-intel: export GOARCH := amd64
|
||||
cross-compile-macos-intel: export CC := o64-clang
|
||||
cross-compile-macos-intel: export CXX := o64-clang++
|
||||
cross-compile-macos-intel: STASH_OUTPUT := -o dist/stash-macos-intel
|
||||
cross-compile-macos-intel: PHASHER_OUTPUT := -o dist/phasher-macos-intel
|
||||
cross-compile-macos-intel: flags-release
|
||||
# can't use static build for OSX
|
||||
cross-compile-macos-intel: flags-pie
|
||||
cross-compile-macos-intel: build
|
||||
.PHONY: build-cc-macos-intel
|
||||
build-cc-macos-intel: export GOOS := darwin
|
||||
build-cc-macos-intel: export GOARCH := amd64
|
||||
build-cc-macos-intel: export CC := o64-clang
|
||||
build-cc-macos-intel: STASH_OUTPUT := -o dist/stash-macos-intel
|
||||
build-cc-macos-intel: PHASHER_OUTPUT := -o dist/phasher-macos-intel
|
||||
build-cc-macos-intel: flags-release
|
||||
# can't use static build for macOS
|
||||
build-cc-macos-intel: flags-pie
|
||||
build-cc-macos-intel: build
|
||||
|
||||
.PHONY: cross-compile-macos-applesilicon
|
||||
cross-compile-macos-applesilicon: export GOOS := darwin
|
||||
cross-compile-macos-applesilicon: export GOARCH := arm64
|
||||
cross-compile-macos-applesilicon: export CC := oa64e-clang
|
||||
cross-compile-macos-applesilicon: export CXX := oa64e-clang++
|
||||
cross-compile-macos-applesilicon: STASH_OUTPUT := -o dist/stash-macos-applesilicon
|
||||
cross-compile-macos-applesilicon: PHASHER_OUTPUT := -o dist/phasher-macos-applesilicon
|
||||
cross-compile-macos-applesilicon: flags-release
|
||||
# can't use static build for OSX
|
||||
cross-compile-macos-applesilicon: flags-pie
|
||||
cross-compile-macos-applesilicon: build
|
||||
.PHONY: build-cc-macos-arm
|
||||
build-cc-macos-arm: export GOOS := darwin
|
||||
build-cc-macos-arm: export GOARCH := arm64
|
||||
build-cc-macos-arm: export CC := oa64e-clang
|
||||
build-cc-macos-arm: STASH_OUTPUT := -o dist/stash-macos-arm
|
||||
build-cc-macos-arm: PHASHER_OUTPUT := -o dist/phasher-macos-arm
|
||||
build-cc-macos-arm: flags-release
|
||||
# can't use static build for macOS
|
||||
build-cc-macos-arm: flags-pie
|
||||
build-cc-macos-arm: build
|
||||
|
||||
.PHONY: build-cc-macos
|
||||
build-cc-macos:
|
||||
make build-cc-macos-arm
|
||||
make build-cc-macos-intel
|
||||
|
||||
# Combine into universal binaries
|
||||
lipo -create -output dist/stash-macos dist/stash-macos-intel dist/stash-macos-arm
|
||||
rm dist/stash-macos-intel dist/stash-macos-arm
|
||||
lipo -create -output dist/phasher-macos dist/phasher-macos-intel dist/phasher-macos-arm
|
||||
rm dist/phasher-macos-intel dist/phasher-macos-arm
|
||||
|
||||
.PHONY: cross-compile-macos
|
||||
cross-compile-macos:
|
||||
rm -rf dist/Stash.app dist/Stash-macos.zip
|
||||
make cross-compile-macos-applesilicon
|
||||
make cross-compile-macos-intel
|
||||
# Combine into one universal binary
|
||||
lipo -create -output dist/stash-macos-universal dist/stash-macos-intel dist/stash-macos-applesilicon
|
||||
rm dist/stash-macos-intel dist/stash-macos-applesilicon
|
||||
# Place into bundle and zip up
|
||||
rm -rf dist/Stash.app
|
||||
cp -R scripts/macos-bundle dist/Stash.app
|
||||
mkdir dist/Stash.app/Contents/MacOS
|
||||
mv dist/stash-macos-universal dist/Stash.app/Contents/MacOS/stash
|
||||
cd dist && zip -r Stash-macos.zip Stash.app && cd ..
|
||||
cp dist/stash-macos dist/Stash.app/Contents/MacOS/stash
|
||||
cd dist && rm -f Stash.app.zip && zip -r Stash.app.zip Stash.app
|
||||
rm -rf dist/Stash.app
|
||||
|
||||
.PHONY: cross-compile-freebsd
|
||||
cross-compile-freebsd: export GOOS := freebsd
|
||||
cross-compile-freebsd: export GOARCH := amd64
|
||||
cross-compile-freebsd: STASH_OUTPUT := -o dist/stash-freebsd
|
||||
cross-compile-freebsd: PHASHER_OUTPUT := -o dist/phasher-freebsd
|
||||
cross-compile-freebsd: flags-release
|
||||
cross-compile-freebsd: flags-static-pie
|
||||
cross-compile-freebsd: build
|
||||
.PHONY: build-cc-freebsd
|
||||
build-cc-freebsd: export GOOS := freebsd
|
||||
build-cc-freebsd: export GOARCH := amd64
|
||||
build-cc-freebsd: export CC := clang -target x86_64-unknown-freebsd12.0 --sysroot=/opt/cross-freebsd
|
||||
build-cc-freebsd: STASH_OUTPUT := -o dist/stash-freebsd
|
||||
build-cc-freebsd: PHASHER_OUTPUT := -o dist/phasher-freebsd
|
||||
build-cc-freebsd: flags-release
|
||||
build-cc-freebsd: flags-static-pie
|
||||
build-cc-freebsd: build
|
||||
|
||||
.PHONY: cross-compile-linux
|
||||
cross-compile-linux: export GOOS := linux
|
||||
cross-compile-linux: export GOARCH := amd64
|
||||
cross-compile-linux: STASH_OUTPUT := -o dist/stash-linux
|
||||
cross-compile-linux: PHASHER_OUTPUT := -o dist/phasher-linux
|
||||
cross-compile-linux: flags-release
|
||||
cross-compile-linux: flags-static-pie
|
||||
cross-compile-linux: build
|
||||
.PHONY: build-cc-linux
|
||||
build-cc-linux: export GOOS := linux
|
||||
build-cc-linux: export GOARCH := amd64
|
||||
build-cc-linux: STASH_OUTPUT := -o dist/stash-linux
|
||||
build-cc-linux: PHASHER_OUTPUT := -o dist/phasher-linux
|
||||
build-cc-linux: flags-release
|
||||
build-cc-linux: flags-static-pie
|
||||
build-cc-linux: build
|
||||
|
||||
.PHONY: cross-compile-linux-arm64v8
|
||||
cross-compile-linux-arm64v8: export GOOS := linux
|
||||
cross-compile-linux-arm64v8: export GOARCH := arm64
|
||||
cross-compile-linux-arm64v8: export CC := aarch64-linux-gnu-gcc
|
||||
cross-compile-linux-arm64v8: STASH_OUTPUT := -o dist/stash-linux-arm64v8
|
||||
cross-compile-linux-arm64v8: PHASHER_OUTPUT := -o dist/phasher-linux-arm64v8
|
||||
cross-compile-linux-arm64v8: flags-release
|
||||
cross-compile-linux-arm64v8: flags-static-pie
|
||||
cross-compile-linux-arm64v8: build
|
||||
.PHONY: build-cc-linux-arm64v8
|
||||
build-cc-linux-arm64v8: export GOOS := linux
|
||||
build-cc-linux-arm64v8: export GOARCH := arm64
|
||||
build-cc-linux-arm64v8: export CC := aarch64-linux-gnu-gcc
|
||||
build-cc-linux-arm64v8: STASH_OUTPUT := -o dist/stash-linux-arm64v8
|
||||
build-cc-linux-arm64v8: PHASHER_OUTPUT := -o dist/phasher-linux-arm64v8
|
||||
build-cc-linux-arm64v8: flags-release
|
||||
build-cc-linux-arm64v8: flags-static-pie
|
||||
build-cc-linux-arm64v8: build
|
||||
|
||||
.PHONY: cross-compile-linux-arm32v7
|
||||
cross-compile-linux-arm32v7: export GOOS := linux
|
||||
cross-compile-linux-arm32v7: export GOARCH := arm
|
||||
cross-compile-linux-arm32v7: export GOARM := 7
|
||||
cross-compile-linux-arm32v7: export CC := arm-linux-gnueabihf-gcc
|
||||
cross-compile-linux-arm32v7: STASH_OUTPUT := -o dist/stash-linux-arm32v7
|
||||
cross-compile-linux-arm32v7: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v7
|
||||
cross-compile-linux-arm32v7: flags-release
|
||||
cross-compile-linux-arm32v7: flags-static
|
||||
cross-compile-linux-arm32v7: build
|
||||
.PHONY: build-cc-linux-arm32v7
|
||||
build-cc-linux-arm32v7: export GOOS := linux
|
||||
build-cc-linux-arm32v7: export GOARCH := arm
|
||||
build-cc-linux-arm32v7: export GOARM := 7
|
||||
build-cc-linux-arm32v7: export CC := arm-linux-gnueabi-gcc -march=armv7-a
|
||||
build-cc-linux-arm32v7: STASH_OUTPUT := -o dist/stash-linux-arm32v7
|
||||
build-cc-linux-arm32v7: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v7
|
||||
build-cc-linux-arm32v7: flags-release
|
||||
build-cc-linux-arm32v7: flags-static
|
||||
build-cc-linux-arm32v7: build
|
||||
|
||||
.PHONY: cross-compile-linux-arm32v6
|
||||
cross-compile-linux-arm32v6: export GOOS := linux
|
||||
cross-compile-linux-arm32v6: export GOARCH := arm
|
||||
cross-compile-linux-arm32v6: export GOARM := 6
|
||||
cross-compile-linux-arm32v6: export CC := arm-linux-gnueabi-gcc
|
||||
cross-compile-linux-arm32v6: STASH_OUTPUT := -o dist/stash-linux-arm32v6
|
||||
cross-compile-linux-arm32v6: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v6
|
||||
cross-compile-linux-arm32v6: flags-release
|
||||
cross-compile-linux-arm32v6: flags-static
|
||||
cross-compile-linux-arm32v6: build
|
||||
.PHONY: build-cc-linux-arm32v6
|
||||
build-cc-linux-arm32v6: export GOOS := linux
|
||||
build-cc-linux-arm32v6: export GOARCH := arm
|
||||
build-cc-linux-arm32v6: export GOARM := 6
|
||||
build-cc-linux-arm32v6: export CC := arm-linux-gnueabi-gcc
|
||||
build-cc-linux-arm32v6: STASH_OUTPUT := -o dist/stash-linux-arm32v6
|
||||
build-cc-linux-arm32v6: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v6
|
||||
build-cc-linux-arm32v6: flags-release
|
||||
build-cc-linux-arm32v6: flags-static
|
||||
build-cc-linux-arm32v6: build
|
||||
|
||||
.PHONY: cross-compile-all
|
||||
cross-compile-all:
|
||||
make cross-compile-windows
|
||||
make cross-compile-macos-intel
|
||||
make cross-compile-macos-applesilicon
|
||||
make cross-compile-linux
|
||||
make cross-compile-linux-arm64v8
|
||||
make cross-compile-linux-arm32v7
|
||||
make cross-compile-linux-arm32v6
|
||||
.PHONY: build-cc-all
|
||||
build-cc-all:
|
||||
make build-cc-windows
|
||||
make build-cc-macos
|
||||
make build-cc-linux
|
||||
make build-cc-linux-arm64v8
|
||||
make build-cc-linux-arm32v7
|
||||
make build-cc-linux-arm32v6
|
||||
make build-cc-freebsd
|
||||
|
||||
.PHONY: touch-ui
|
||||
touch-ui:
|
||||
@@ -314,12 +307,13 @@ test:
|
||||
# runs all tests - including integration tests
|
||||
.PHONY: it
|
||||
it:
|
||||
go test -tags=integration ./...
|
||||
$(eval GO_BUILD_TAGS += integration)
|
||||
go test -tags "$(GO_BUILD_TAGS)" ./...
|
||||
|
||||
# generates test mocks
|
||||
.PHONY: generate-test-mocks
|
||||
generate-test-mocks:
|
||||
go run github.com/vektra/mockery/v2 --dir ./pkg/models --name '.*ReaderWriter' --outpkg mocks --output ./pkg/models/mocks
|
||||
go run github.com/vektra/mockery/v2
|
||||
|
||||
# runs server
|
||||
# sets the config file to use the local dev config
|
||||
@@ -360,13 +354,10 @@ endif
|
||||
ui: ui-env
|
||||
cd ui/v2.5 && yarn build
|
||||
|
||||
.PHONY: ui-nolegacy
|
||||
ui-nolegacy: STASH_NOLEGACY := true
|
||||
ui-nolegacy: ui
|
||||
|
||||
.PHONY: ui-sourcemaps
|
||||
ui-sourcemaps: STASH_SOURCEMAPS := true
|
||||
ui-sourcemaps: ui
|
||||
.PHONY: zip-ui
|
||||
zip-ui:
|
||||
rm -f dist/stash-ui.zip
|
||||
cd ui/v2.5/build && zip -r ../../../dist/stash-ui.zip .
|
||||
|
||||
.PHONY: ui-start
|
||||
ui-start: ui-env
|
||||
@@ -381,6 +372,20 @@ fmt-ui:
|
||||
validate-ui:
|
||||
cd ui/v2.5 && yarn run validate
|
||||
|
||||
# these targets run the same steps as fmt-ui and validate-ui, but only on files that have changed
|
||||
fmt-ui-quick:
|
||||
cd ui/v2.5 && yarn run prettier --write $$(git diff --name-only --relative --diff-filter d . ../../graphql)
|
||||
|
||||
# does not run tsc checks, as they are slow
|
||||
validate-ui-quick:
|
||||
cd ui/v2.5 && \
|
||||
tsfiles=$$(git diff --name-only --relative --diff-filter d src | grep -e "\.tsx\?\$$"); \
|
||||
scssfiles=$$(git diff --name-only --relative --diff-filter d src | grep "\.scss"); \
|
||||
prettyfiles=$$(git diff --name-only --relative --diff-filter d . ../../graphql); \
|
||||
if [ -n "$$tsfiles" ]; then yarn run eslint $$tsfiles; fi && \
|
||||
if [ -n "$$scssfiles" ]; then yarn run stylelint $$scssfiles; fi && \
|
||||
if [ -n "$$prettyfiles" ]; then yarn run prettier --check $$prettyfiles; fi
|
||||
|
||||
# runs all of the backend PR-acceptance steps
|
||||
.PHONY: validate-backend
|
||||
validate-backend: lint it
|
||||
@@ -398,3 +403,16 @@ docker-build: build-info
|
||||
.PHONY: docker-cuda-build
|
||||
docker-cuda-build: build-info
|
||||
docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/cuda-build -f docker/build/x86_64/Dockerfile-CUDA .
|
||||
|
||||
# start the build container - for cross compilation
|
||||
# this is adapted from the github actions build.yml file
|
||||
.PHONY: start-compiler-container
|
||||
start-compiler-container:
|
||||
docker run -d --name build --mount type=bind,source="$(PWD)",target=/stash,consistency=delegated $(EXTRA_CONTAINER_ARGS) -w /stash $(COMPILER_IMAGE) tail -f /dev/null
|
||||
|
||||
# run the cross-compilation using
|
||||
# docker exec -t build /bin/bash -c "make build-cc-<platform>"
|
||||
|
||||
.PHONY: remove-compiler-container
|
||||
remove-compiler-container:
|
||||
docker rm -f -v build
|
||||
42
README.md
42
README.md
@@ -1,5 +1,4 @@
|
||||
# Stash
|
||||
https://stashapp.cc
|
||||
|
||||
[](https://github.com/stashapp/stash/actions/workflows/build.yml)
|
||||
[](https://hub.docker.com/r/stashapp/stash 'DockerHub')
|
||||
@@ -21,19 +20,31 @@ https://stashapp.cc
|
||||
|
||||
You can [watch a SFW demo video](https://vimeo.com/545323354) to see it in action.
|
||||
|
||||
For further information you can [read the in-app manual](ui/v2.5/src/docs/en).
|
||||
For further information you can consult the [documentation](https://docs.stashapp.cc) or [read the in-app manual](ui/v2.5/src/docs/en).
|
||||
|
||||
# Installing Stash
|
||||
|
||||
<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
|
||||
#### Windows Users:
|
||||
|
||||
As of version 0.27.0, Stash doesn't support anymore _Windows 7, 8, Server 2008 and Server 2012._
|
||||
Windows 10 or Server 2016 are at least required.
|
||||
|
||||
<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
|
||||
:---:|:---:|:---:|:---:
|
||||
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release (Apple Silicon)](https://github.com/stashapp/stash/releases/latest/download/stash-macos-applesilicon) <br /> <sup><sub>[Development Preview (Apple Silicon)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-macos-applesilicon)</sub></sup> <br />[Latest Release (Intel)](https://github.com/stashapp/stash/releases/latest/download/stash-macos-intel) <br /> <sup><sub>[Development Preview (Intel)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-macos-intel)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub> [Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
|
||||
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release](https://github.com/stashapp/stash/releases/latest/download/Stash.app.zip) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/Stash.app.zip)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub>[Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
|
||||
|
||||
Download links for other platforms and architectures are available on the [Releases page](https://github.com/stashapp/stash/releases).
|
||||
|
||||
## First Run
|
||||
#### Windows Users: Security Prompt
|
||||
Running the app might present a security prompt since the binary isn't yet signed. Bypass this by clicking "more info" and then the "run anyway" button.
|
||||
#### FFMPEG
|
||||
Stash requires ffmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
|
||||
|
||||
#### Windows/macOS Users: Security Prompt
|
||||
|
||||
On Windows or macOS, running the app might present a security prompt since the binary isn't yet signed.
|
||||
|
||||
On Windows, bypass this by clicking "more info" and then the "run anyway" button. On macOS, Control+Click the app, click "Open", and then "Open" again.
|
||||
|
||||
#### FFmpeg
|
||||
Stash requires FFmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
|
||||
|
||||
# Usage
|
||||
|
||||
@@ -42,17 +53,20 @@ Stash is a web-based application. Once the application is running, the interface
|
||||
|
||||
On first run, Stash will prompt you for some configuration options and media directories to index, called "Scanning" in Stash. After scanning, your media will be available for browsing, curating, editing, and tagging.
|
||||
|
||||
Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en/Scraping.md), which integrate directly into Stash.
|
||||
|
||||
Many community-maintained scrapers are available for download from [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers). The community also maintains StashDB, a crowd-sourced repository of scene, studio, and performer information, that can automatically identify much of a typical media collection. Inquire in the Discord for details. Identifying an entire collection will typically require a mix of multiple sources.
|
||||
Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/docs/en/Manual/Scraping.md), which integrate directly into Stash. Identifying an entire collection will typically require a mix of multiple sources:
|
||||
- The project maintains [StashDB](https://stashdb.org/), a crowd-sourced repository of scene, studio, and performer information. Connecting it to Stash will allow you to automatically identify much of a typical media collection. It runs on our stash-box software and is primarily focused on mainstream digital scenes and studios. Instructions, invite codes, and more can be found in this guide to [Accessing StashDB](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stashdb/).
|
||||
- Several community-managed stash-box databases can also be connected to Stash in a similar manner. Each one serves a slightly different niche and follows their own methodology. A rundown of each stash-box, their differences, and the information you need to sign up can be found in this guide to [Accessing Stash-Boxes](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stash-boxes/).
|
||||
- Many community-maintained scrapers can also be downloaded, installed, and updated from within Stash, allowing you to pull data from a wide range of other websites and databases. They can be found by navigating to Settings -> Metadata Providers -> Available Scrapers -> Community (stable). These can be trickier to use than a stash-box because every scraper works a little differently. For more information, please visit the [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers).
|
||||
- All of the above methods of scraping data into Stash are also covered in more detail in our [Guide to Scraping](https://docs.stashapp.cc/beginner-guides/guide-to-scraping/).
|
||||
|
||||
<sub>[StashDB](http://stashdb.org) is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box).</sub>
|
||||
|
||||
# Translation
|
||||
[](https://translate.stashapp.cc/engage/stash/)
|
||||
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇪🇪 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇷🇺 🇪🇸 🇸🇪 🇹🇼 🇹🇷
|
||||
[](https://translate.codeberg.org/engage/stash/)
|
||||
|
||||
Stash is available in 25 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks!
|
||||
Stash is available in 32 languages (so far!) and it could be in your language too. We use Weblate to coordinate community translations. If you want to help us translate Stash into your language, you can make an account at [Codeberg's Weblate](https://translate.codeberg.org/projects/stash/stash/) to get started contributing new languages or improving existing ones. Thanks!
|
||||
|
||||
[](https://translate.codeberg.org/engage/stash/)
|
||||
|
||||
# Support (FAQ)
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/hash/videophash"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func customUsage() {
|
||||
@@ -18,7 +18,7 @@ func customUsage() {
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func printPhash(ff *ffmpeg.FFMpeg, ffp ffmpeg.FFProbe, inputfile string, quiet *bool) error {
|
||||
func printPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, quiet *bool) error {
|
||||
ffvideoFile, err := ffp.NewVideoFile(inputfile)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -28,8 +28,8 @@ func printPhash(ff *ffmpeg.FFMpeg, ffp ffmpeg.FFProbe, inputfile string, quiet *
|
||||
// videoFile.Path (from BaseFile)
|
||||
// videoFile.Duration
|
||||
// The rest of the struct isn't needed.
|
||||
vf := &file.VideoFile{
|
||||
BaseFile: &file.BaseFile{Path: inputfile},
|
||||
vf := &models.VideoFile{
|
||||
BaseFile: &models.BaseFile{Path: inputfile},
|
||||
Duration: ffvideoFile.FileDuration,
|
||||
}
|
||||
|
||||
@@ -46,6 +46,13 @@ func printPhash(ff *ffmpeg.FFMpeg, ffp ffmpeg.FFProbe, inputfile string, quiet *
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPaths() (string, string) {
|
||||
ffmpegPath, _ := exec.LookPath("ffmpeg")
|
||||
ffprobePath, _ := exec.LookPath("ffprobe")
|
||||
|
||||
return ffmpegPath, ffprobePath
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = customUsage
|
||||
quiet := flag.BoolP("quiet", "q", false, "print only the phash")
|
||||
@@ -66,14 +73,14 @@ func main() {
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
fmt.Fprintln(os.Stderr, "Files will be processed sequentially! Consier using GNU Parallel.")
|
||||
fmt.Fprintln(os.Stderr, "Files will be processed sequentially! If required, use e.g. GNU Parallel to run concurrently.")
|
||||
fmt.Fprintf(os.Stderr, "Example: parallel %v ::: *.mp4\n", os.Args[0])
|
||||
}
|
||||
|
||||
ffmpegPath, ffprobePath := ffmpeg.GetPaths(nil)
|
||||
ffmpegPath, ffprobePath := getPaths()
|
||||
encoder := ffmpeg.NewEncoder(ffmpegPath)
|
||||
encoder.InitHWSupport(context.TODO())
|
||||
ffprobe := ffmpeg.FFProbe(ffprobePath)
|
||||
// don't need to InitHWSupport, phashing doesn't use hw acceleration
|
||||
ffprobe := ffmpeg.NewFFProbe(ffprobePath)
|
||||
|
||||
for _, item := range args {
|
||||
if err := printPhash(encoder, ffprobe, item, quiet); err != nil {
|
||||
|
||||
@@ -1,68 +1,167 @@
|
||||
//go:generate go run -mod=vendor github.com/99designs/gqlgen
|
||||
//go:generate go run github.com/99designs/gqlgen
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/stashapp/stash/internal/api"
|
||||
"github.com/stashapp/stash/internal/build"
|
||||
"github.com/stashapp/stash/internal/desktop"
|
||||
"github.com/stashapp/stash/internal/log"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/ui"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
var exitCode = 0
|
||||
|
||||
func main() {
|
||||
defer recoverPanic()
|
||||
|
||||
_, err := manager.Initialize()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer recoverPanic()
|
||||
if err := api.Start(); err != nil {
|
||||
handleError(err)
|
||||
} else {
|
||||
manager.GetInstance().Shutdown(0)
|
||||
defer func() {
|
||||
if exitCode != 0 {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
}()
|
||||
|
||||
go handleSignals()
|
||||
desktop.Start(manager.GetInstance(), &ui.FaviconProvider)
|
||||
defer recoverPanic()
|
||||
|
||||
blockForever()
|
||||
initLogTemp()
|
||||
|
||||
helpFlag := false
|
||||
pflag.BoolVarP(&helpFlag, "help", "h", false, "show this help text and exit")
|
||||
|
||||
versionFlag := false
|
||||
pflag.BoolVarP(&versionFlag, "version", "v", false, "show version number and exit")
|
||||
|
||||
cpuProfilePath := ""
|
||||
pflag.StringVar(&cpuProfilePath, "cpuprofile", "", "write cpu profile to file")
|
||||
|
||||
pflag.Parse()
|
||||
|
||||
if helpFlag {
|
||||
pflag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
if versionFlag {
|
||||
fmt.Println(build.VersionString())
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Initialize()
|
||||
if err != nil {
|
||||
exitError(fmt.Errorf("config initialization error: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
l := initLog(cfg)
|
||||
|
||||
if cpuProfilePath != "" {
|
||||
if err := initProfiling(cpuProfilePath); err != nil {
|
||||
exitError(err)
|
||||
return
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
mgr, err := manager.Initialize(cfg, l)
|
||||
if err != nil {
|
||||
exitError(fmt.Errorf("manager initialization error: %w", err))
|
||||
return
|
||||
}
|
||||
defer mgr.Shutdown()
|
||||
|
||||
server, err := api.Initialize()
|
||||
if err != nil {
|
||||
exitError(fmt.Errorf("api initialization error: %w", err))
|
||||
return
|
||||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
exit := make(chan int)
|
||||
|
||||
go func() {
|
||||
err := server.Start()
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
exitError(fmt.Errorf("http server error: %w", err))
|
||||
exit <- 1
|
||||
}
|
||||
}()
|
||||
|
||||
go handleSignals(exit)
|
||||
desktop.Start(exit, &ui.FaviconProvider)
|
||||
|
||||
exitCode = <-exit
|
||||
}
|
||||
|
||||
// initLogTemp initializes a temporary logger for use before the config is loaded.
|
||||
// Logs only error level message to stderr.
|
||||
func initLogTemp() *log.Logger {
|
||||
l := log.NewLogger()
|
||||
l.Init("", true, "Error")
|
||||
logger.Logger = l
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func initLog(cfg *config.Config) *log.Logger {
|
||||
l := log.NewLogger()
|
||||
l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel())
|
||||
logger.Logger = l
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func initProfiling(path string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create CPU profile file: %v", err)
|
||||
}
|
||||
|
||||
if err = pprof.StartCPUProfile(f); err != nil {
|
||||
return fmt.Errorf("could not start CPU profiling: %v", err)
|
||||
}
|
||||
|
||||
logger.Infof("profiling to %s", path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func recoverPanic() {
|
||||
if p := recover(); p != nil {
|
||||
handleError(fmt.Errorf("Panic: %v", p))
|
||||
if err := recover(); err != nil {
|
||||
exitCode = 1
|
||||
logger.Errorf("panic: %v\n%s", err, debug.Stack())
|
||||
if desktop.IsDesktop() {
|
||||
desktop.FatalError(fmt.Errorf("Panic: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleError(err error) {
|
||||
func exitError(err error) {
|
||||
exitCode = 1
|
||||
logger.Error(err)
|
||||
if desktop.IsDesktop() {
|
||||
desktop.FatalError(err)
|
||||
manager.GetInstance().Shutdown(0)
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleSignals() {
|
||||
func handleSignals(exit chan<- int) {
|
||||
// handle signals
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
<-signals
|
||||
manager.GetInstance().Shutdown(0)
|
||||
}
|
||||
|
||||
func blockForever() {
|
||||
select {}
|
||||
exit <- 0
|
||||
}
|
||||
|
||||
@@ -16,12 +16,11 @@ ARG STASH_VERSION
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
|
||||
|
||||
# Build Backend
|
||||
FROM golang:1.19-alpine as backend
|
||||
FROM golang:1.22-alpine as backend
|
||||
RUN apk add --no-cache make alpine-sdk
|
||||
WORKDIR /stash
|
||||
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
||||
COPY ./scripts /stash/scripts/
|
||||
COPY ./vendor /stash/vendor/
|
||||
COPY ./pkg /stash/pkg/
|
||||
COPY ./cmd /stash/cmd
|
||||
COPY ./internal /stash/internal
|
||||
@@ -29,7 +28,7 @@ COPY --from=frontend /stash /stash/
|
||||
RUN make generate-backend
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN make stash-release
|
||||
RUN make flags-release flags-pie stash
|
||||
|
||||
# Final Runnable Image
|
||||
FROM alpine:latest
|
||||
|
||||
@@ -16,12 +16,11 @@ ARG STASH_VERSION
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
|
||||
|
||||
# Build Backend
|
||||
FROM golang:1.19-bullseye as backend
|
||||
FROM golang:1.22-bullseye as backend
|
||||
RUN apt update && apt install -y build-essential golang
|
||||
WORKDIR /stash
|
||||
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
||||
COPY ./scripts /stash/scripts/
|
||||
COPY ./vendor /stash/vendor/
|
||||
COPY ./pkg /stash/pkg/
|
||||
COPY ./cmd /stash/cmd
|
||||
COPY ./internal /stash/internal
|
||||
@@ -29,7 +28,7 @@ COPY --from=frontend /stash /stash/
|
||||
RUN make generate-backend
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN make stash-release
|
||||
RUN make flags-release flags-pie stash
|
||||
|
||||
# Final Runnable Image
|
||||
FROM nvidia/cuda:12.0.1-base-ubuntu22.04
|
||||
@@ -49,3 +48,5 @@ ENV NVIDIA_DRIVER_CAPABILITIES=video,utility
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
EXPOSE 9999
|
||||
ENTRYPOINT ["docker-entrypoint.sh", "stash"]
|
||||
|
||||
# vim: ft=dockerfile
|
||||
|
||||
@@ -11,12 +11,25 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-linux-arm32v6; \
|
||||
|
||||
FROM --platform=$TARGETPLATFORM alpine:latest AS app
|
||||
COPY --from=binary /stash /usr/bin/
|
||||
|
||||
# vips version 8.15.0-r0 breaks thumbnail generation on arm32v6
|
||||
# need to use 8.14.3-r0 from alpine 3.18 instead
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps gcc python3-dev musl-dev \
|
||||
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg vips-tools ruby tzdata \
|
||||
&& pip install mechanicalsoup cloudscraper bencoder.pyx \
|
||||
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg ruby tzdata \
|
||||
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.18/community vips=8.14.3-r0 vips-tools=8.14.3-r0 \
|
||||
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx stashapp-tools \
|
||||
&& gem install faraday \
|
||||
&& apk del .build-deps
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
# Basic build-time metadata as defined at https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
|
||||
LABEL org.opencontainers.image.title="Stash" \
|
||||
org.opencontainers.image.description="An organizer for your porn, written in Go." \
|
||||
org.opencontainers.image.url="https://stashapp.cc" \
|
||||
org.opencontainers.image.documentation="https://docs.stashapp.cc" \
|
||||
org.opencontainers.image.source="https://github.com/stashapp/stash" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0"
|
||||
|
||||
EXPOSE 9999
|
||||
CMD ["stash"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
This dockerfile is used by travis to build the stash image. It must be run after cross-compiling - that is, `stash-linux` must exist in the `dist` directory. This image must be built from the `dist` directory.
|
||||
This Dockerfile is used by CI to build the `stashapp/stash` Docker image. It must be run after cross-compiling - that is, `stash-linux` must exist in the `dist` directory. This image must be built from the `dist` directory.
|
||||
|
||||
@@ -1,67 +1,83 @@
|
||||
FROM golang:1.19
|
||||
FROM golang:1.22
|
||||
|
||||
LABEL maintainer="https://discord.gg/2TsNFKt"
|
||||
|
||||
# Install tools
|
||||
RUN apt-get update && apt-get install -y apt-transport-https
|
||||
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
RUN apt-get update && apt-get install -y apt-transport-https ca-certificates gnupg
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
|
||||
ADD https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key nodesource.gpg.key
|
||||
RUN cat nodesource.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && rm nodesource.gpg.key
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
|
||||
# prevent caching of the key
|
||||
ADD https://dl.yarnpkg.com/debian/pubkey.gpg yarn.gpg
|
||||
RUN cat yarn.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
rm yarn.gpg
|
||||
RUN cat yarn.gpg | gpg --dearmor -o /etc/apt/keyrings/yarn.gpg && rm yarn.gpg
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y automake autogen cmake \
|
||||
libtool libxml2-dev uuid-dev libssl-dev bash \
|
||||
patch make tar xz-utils bzip2 gzip zlib1g-dev sed cpio \
|
||||
gcc-10-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
|
||||
gcc-arm-linux-gnueabi libc-dev-armel-cross linux-libc-dev-armel-cross \
|
||||
gcc-arm-linux-gnueabihf libc-dev-armhf-cross \
|
||||
gcc-aarch64-linux-gnu libc-dev-arm64-cross \
|
||||
nodejs yarn zip --no-install-recommends || exit 1; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
apt-get install -y --no-install-recommends \
|
||||
git make tar bash nodejs yarn zip \
|
||||
clang llvm-dev cmake patch libxml2-dev uuid-dev libssl-dev xz-utils \
|
||||
bzip2 gzip sed cpio libbz2-dev zlib1g-dev \
|
||||
gcc-mingw-w64 \
|
||||
gcc-arm-linux-gnueabi libc-dev-armel-cross linux-libc-dev-armel-cross \
|
||||
gcc-aarch64-linux-gnu libc-dev-arm64-cross && \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
# Cross compile setup
|
||||
ENV OSX_SDK_VERSION 11.3
|
||||
ENV OSX_SDK_DOWNLOAD_FILE=MacOSX${OSX_SDK_VERSION}.sdk.tar.xz
|
||||
ENV OSX_SDK_DOWNLOAD_URL=https://github.com/phracker/MacOSX-SDKs/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
|
||||
ENV OSX_SDK_SHA=cd4f08a75577145b8f05245a2975f7c81401d75e9535dcffbb879ee1deefcbf4
|
||||
ENV OSX_SDK MacOSX$OSX_SDK_VERSION.sdk
|
||||
ENV OSX_NDK_X86 /usr/local/osx-ndk-x86
|
||||
# FreeBSD cross-compilation setup
|
||||
# https://github.com/smartmontools/docker-build/blob/6b8c92560d17d325310ba02d9f5a4b250cb0764a/Dockerfile#L66
|
||||
ENV FREEBSD_VERSION 12.4
|
||||
ENV FREEBSD_DOWNLOAD_URL http://ftp.plusline.de/FreeBSD/releases/amd64/${FREEBSD_VERSION}-RELEASE/base.txz
|
||||
ENV FREEBSD_SHA 581c7edacfd2fca2bdf5791f667402d22fccd8a5e184635e0cac075564d57aa8
|
||||
|
||||
RUN wget ${OSX_SDK_DOWNLOAD_URL}
|
||||
RUN echo "$OSX_SDK_SHA $OSX_SDK_DOWNLOAD_FILE" | sha256sum -c - || exit 1; \
|
||||
git clone https://github.com/tpoechtrager/osxcross.git; \
|
||||
mv $OSX_SDK_DOWNLOAD_FILE osxcross/tarballs/
|
||||
RUN cd /tmp && \
|
||||
curl -o base.txz $FREEBSD_DOWNLOAD_URL && \
|
||||
echo "$FREEBSD_SHA base.txz" | sha256sum -c - && \
|
||||
mkdir -p /opt/cross-freebsd && \
|
||||
cd /opt/cross-freebsd && \
|
||||
tar -xf /tmp/base.txz ./lib/ ./usr/lib/ ./usr/include/ && \
|
||||
rm -f /tmp/base.txz && \
|
||||
cd /opt/cross-freebsd/usr/lib && \
|
||||
find . -xtype l | xargs ls -l | grep ' /lib/' | awk '{print "ln -sf /opt/cross-freebsd"$11 " " $9}' | /bin/sh && \
|
||||
ln -s libc++.a libstdc++.a && \
|
||||
ln -s libc++.so libstdc++.so
|
||||
|
||||
RUN UNATTENDED=yes SDK_VERSION=${OSX_SDK_VERSION} OSX_VERSION_MIN=10.10 osxcross/build.sh || exit 1;
|
||||
RUN cp osxcross/target/lib/* /usr/lib/ ; \
|
||||
mv osxcross/target $OSX_NDK_X86; \
|
||||
rm -rf osxcross;
|
||||
# macOS cross-compilation setup
|
||||
ENV OSX_SDK_VERSION 11.3
|
||||
ENV OSX_SDK_DOWNLOAD_FILE MacOSX${OSX_SDK_VERSION}.sdk.tar.xz
|
||||
ENV OSX_SDK_DOWNLOAD_URL https://github.com/phracker/MacOSX-SDKs/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
|
||||
ENV OSX_SDK_SHA cd4f08a75577145b8f05245a2975f7c81401d75e9535dcffbb879ee1deefcbf4
|
||||
ENV OSXCROSS_REVISION 5e1b71fcceb23952f3229995edca1b6231525b5b
|
||||
ENV OSXCROSS_DOWNLOAD_URL https://codeload.github.com/tpoechtrager/osxcross/tar.gz/${OSXCROSS_REVISION}
|
||||
ENV OSXCROSS_SHA d3f771bbc20612fea577b18a71be3af2eb5ad2dd44624196cf55de866d008647
|
||||
|
||||
ENV PATH $OSX_NDK_X86/bin:$PATH
|
||||
RUN cd /tmp && \
|
||||
curl -o osxcross.tar.gz $OSXCROSS_DOWNLOAD_URL && \
|
||||
echo "$OSXCROSS_SHA osxcross.tar.gz" | sha256sum -c - && \
|
||||
mkdir osxcross && \
|
||||
tar --strip=1 -C osxcross -xf osxcross.tar.gz && \
|
||||
rm -f osxcross.tar.gz && \
|
||||
curl -Lo $OSX_SDK_DOWNLOAD_FILE $OSX_SDK_DOWNLOAD_URL && \
|
||||
echo "$OSX_SDK_SHA $OSX_SDK_DOWNLOAD_FILE" | sha256sum -c - && \
|
||||
mv $OSX_SDK_DOWNLOAD_FILE osxcross/tarballs/ && \
|
||||
UNATTENDED=yes SDK_VERSION=$OSX_SDK_VERSION OSX_VERSION_MIN=10.10 osxcross/build.sh && \
|
||||
cp osxcross/target/lib/* /usr/lib/ && \
|
||||
mv osxcross/target /opt/osx-ndk-x86 && \
|
||||
rm -rf /tmp/osxcross
|
||||
|
||||
RUN mkdir -p /root/.ssh; \
|
||||
chmod 0700 /root/.ssh; \
|
||||
ssh-keyscan github.com > /root/.ssh/known_hosts;
|
||||
ENV PATH /opt/osx-ndk-x86/bin:$PATH
|
||||
|
||||
# Notes for self:
|
||||
RUN mkdir -p /root/.ssh && \
|
||||
chmod 0700 /root/.ssh && \
|
||||
ssh-keyscan github.com > /root/.ssh/known_hosts
|
||||
|
||||
# ignore "dubious ownership" errors
|
||||
RUN git config --global safe.directory '*'
|
||||
|
||||
# To test locally:
|
||||
# make generate
|
||||
# make ui
|
||||
# cd docker/compiler
|
||||
# make build
|
||||
# docker run -it -v /PATH_TO_STASH:/go/stash stashapp/compiler:latest /bin/bash
|
||||
# cd stash
|
||||
# make cross-compile-all
|
||||
# docker run --rm -v /PATH_TO_STASH:/stash -w /stash -i -t stashapp/compiler:latest make build-cc-all
|
||||
# # binaries will show up in /dist
|
||||
|
||||
# Windows:
|
||||
# GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go build -ldflags "-extldflags '-static'" -tags extended
|
||||
|
||||
# Darwin
|
||||
# CC=o64-clang CXX=o64-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -tags extended
|
||||
# env goreleaser --config=goreleaser-extended.yml --skip-publish --skip-validate --rm-dist --release-notes=temp/0.48-relnotes-ready.md
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
user=stashapp
|
||||
repo=compiler
|
||||
version=7
|
||||
version=9
|
||||
|
||||
latest:
|
||||
docker build -t ${user}/${repo}:latest .
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Modified from https://github.com/bep/dockerfiles/tree/master/ci-goreleaser
|
||||
|
||||
When the dockerfile is changed, the version number should be incremented in the Makefile and the new version tag should be pushed to docker hub. The `scripts/cross-compile.sh` script should also be updated to use the new version number tag, and the github workflow files need to be updated to pull the correct image tag.
|
||||
When the Dockerfile is changed, the version number should be incremented in the Makefile and the new version tag should be pushed to Docker Hub. The GitHub workflow files also need to be updated to pull the correct image tag.
|
||||
|
||||
@@ -9,11 +9,11 @@ https://docs.docker.com/engine/install/
|
||||
|
||||
### Get the docker-compose.yml file
|
||||
|
||||
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml) in the repository, or if you have curl, you can make your Linux console do it for you:
|
||||
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/develop/docker/production/docker-compose.yml) in the repository, or if you have curl, you can make your Linux console do it for you:
|
||||
|
||||
```
|
||||
mkdir stashapp && cd stashapp
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/develop/docker/production/docker-compose.yml
|
||||
```
|
||||
|
||||
Once you have that file where you want it, modify the settings as you please, and then run:
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
## Goals and design vision
|
||||
|
||||
The goal of stash is to be:
|
||||
- an application for organising and viewing adult content - currently this is videos and images, in future this will be extended to include audio and text content
|
||||
- organising includes scraping of metadata from websites and metadata repositories
|
||||
- free and open-source
|
||||
- portable and offline - can be run on a USB stick without needing to install dependencies (with the exception of ffmpeg)
|
||||
- minimal, but highly extensible. The core feature set should be the minimum required to achieve the primary goal, while being extensible enough to extend via plugins
|
||||
- easy to learn and use, with minimal technical knowledge required
|
||||
|
||||
The core stash system is not intended for:
|
||||
- managing downloading of content
|
||||
- managing content on external websites
|
||||
- publically sharing content
|
||||
|
||||
Other requirements:
|
||||
- support as many video and image formats as possible
|
||||
- interfaces with external systems (for example stash-box) should be made as generic as possible.
|
||||
|
||||
Design considerations:
|
||||
- features are easy to add and difficult to remove. Large superfluous features should be scrutinised and avoided where possible (eg DLNA, filename parser). Such features should be considered for third-party plugins instead.
|
||||
|
||||
## Technical Debt
|
||||
Please be sure to consider how heavily your contribution impacts the maintainability of the project long term, sometimes less is more. We don't want to merge collossal pull requests with hundreds of dependencies by a driveby contributor.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
* [Go](https://golang.org/dl/)
|
||||
* [GolangCI](https://golangci-lint.run/) - A meta-linter which runs several linters in parallel
|
||||
* To install, follow the [local installation instructions](https://golangci-lint.run/usage/install/#local-installation)
|
||||
* To install, follow the [local installation instructions](https://golangci-lint.run/welcome/install/#local-installation)
|
||||
* [Yarn](https://yarnpkg.com/en/docs/install) - Yarn package manager
|
||||
|
||||
## Environment
|
||||
@@ -32,9 +32,24 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MinGW. For examp
|
||||
|
||||
#### Ubuntu
|
||||
|
||||
1. Install dependencies: `sudo apt-get install golang git gcc nodejs ffmpeg -y`
|
||||
2. Enable corepack in Node.js: `corepack enable`
|
||||
3. Install yarn: `corepack prepare yarn@stable --activate`
|
||||
1. Install dependencies: `sudo apt-get install golang git yarnpkg gcc nodejs ffmpeg -y`
|
||||
|
||||
### OpenBSD
|
||||
|
||||
1. Install dependencies `doas pkg_add gmake go git yarn node cmake`
|
||||
2. Compile a custom ffmpeg from ports. The default ffmpeg in OpenBSD's packages is not compiled with WebP support, which is required by Stash.
|
||||
- If you've already installed ffmpeg, uninstall it: `doas pkg_delete ffmpeg`
|
||||
- If you haven't already, [fetch the ports tree and verify](https://www.openbsd.org/faq/ports/ports.html#PortsFetch).
|
||||
- Find the ffmpeg port in `/usr/ports/graphics/ffmpeg`, and patch the Makefile to include libwebp
|
||||
- Add `webp` to `WANTLIB`
|
||||
- Add `graphics/libwebp` to the list in `LIB_DEPENDS`
|
||||
- Add `-lwebp -lwebpdecoder -lwebpdemux -lwebpmux` to `LIBavcodec_EXTRALIBS`
|
||||
- Add `--enable-libweb` to the list in `CONFIGURE_ARGS`
|
||||
- If you've already built ffmpeg from ports before, you may need to also increment `REVISION`
|
||||
- Run `doas make install`
|
||||
- Follow the instructions below to build a release, but replace the final step `make build-release` with `gmake flags-release stash`, to [avoid the PIE buildmode](https://github.com/golang/go/issues/59866).
|
||||
|
||||
NOTE: The `make` command in OpenBSD will be `gmake`. For example, `make pre-ui` will be `gmake pre-ui`.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -43,11 +58,10 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MinGW. For examp
|
||||
* `make generate-stash-box-client` - Generate Go files for the Stash-box client code.
|
||||
* `make ui` - Builds the UI. Requires `make pre-ui` to have been run.
|
||||
* `make stash` - Builds the `stash` binary (make sure to build the UI as well... see below)
|
||||
* `make stash-release` - Builds a release version the `stash` binary, with debug information removed
|
||||
* `make stash-macapp` - Builds the `Stash.app` macOS app (only works when on macOS, for cross-compilation see below)
|
||||
* `make phasher` - Builds the `phasher` binary
|
||||
* `make phasher-release` - Builds a release version the `phasher` binary, with debug information removed
|
||||
* `make build` - Builds both the `stash` and `phasher` binaries
|
||||
* `make build-release` - Builds release versions of both the `stash` and `phasher` binaries
|
||||
* `make build` - Builds both the `stash` and `phasher` binaries, alias for `make stash phasher`
|
||||
* `make build-release` - Builds release versions (debug information removed) of both the `stash` and `phasher` binaries, alias for `make flags-release flags-pie build`
|
||||
* `make docker-build` - Locally builds and tags a complete 'stash/build' docker image
|
||||
* `make docker-cuda-build` - Locally builds and tags a complete 'stash/cuda-build' docker image
|
||||
* `make validate` - Runs all of the tests and checks required to submit a PR
|
||||
@@ -55,9 +69,20 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MinGW. For examp
|
||||
* `make it` - Runs all unit and integration tests
|
||||
* `make fmt` - Formats the Go source code
|
||||
* `make fmt-ui` - Formats the UI source code
|
||||
* `make validate-ui` - Runs tests and checks for the UI only
|
||||
* `make fmt-ui-quick` - (experimental) Formats only changed UI source code
|
||||
* `make validate-ui-quick` - (experimental) Runs tests and checks of changed UI code
|
||||
* `make server-start` - Runs a development stash server in the `.local` directory
|
||||
* `make server-clean` - Removes the `.local` directory and all of its contents
|
||||
* `make ui-start` - Runs the UI in development mode. Requires a running Stash server to connect to. The server port can be changed from the default of `9999` using the environment variable `VITE_APP_PLATFORM_PORT`. The UI runs on port `3000` or the next available port.
|
||||
* `make ui-start` - Runs the UI in development mode. Requires a running Stash server to connect to - the server URL can be changed from the default of `http://localhost:9999` using the environment variable `VITE_APP_PLATFORM_URL`, but keep in mind that authentication cannot be used since the session authorization cookie cannot be sent cross-origin. The UI runs on port `3000` or the next available port.
|
||||
|
||||
When building, you can optionally prepend `flags-*` targets to the target list in your `make` command to use different build flags:
|
||||
|
||||
* `flags-release` (e.g. `make flags-release stash`) - Remove debug information from the binary.
|
||||
* `flags-pie` (e.g. `make flags-pie build`) - Build a PIE (Position Independent Executable) binary. This provides increased security, but it is unsupported on some systems (notably 32-bit ARM and OpenBSD).
|
||||
* `flags-static` (e.g. `make flags-static phasher`) - Build a statically linked binary (the default is a dynamically linked binary).
|
||||
* `flags-static-pie` (e.g. `make flags-static-pie stash`) - Build a statically linked PIE binary (using `flags-static` and `flags-pie` separately will not work).
|
||||
* `flags-static-windows` (e.g. `make flags-static-windows build`) - Identical to `flags-static-pie`, but does not enable the `netgo` build tag, which is not needed for static builds on Windows.
|
||||
|
||||
## Local development quickstart
|
||||
|
||||
@@ -95,13 +120,19 @@ Simply run `make` or `make release`, or equivalently:
|
||||
3. Run `make ui` to build the frontend
|
||||
4. Run `make build-release` to build a release executable for your current platform
|
||||
|
||||
## Cross compiling
|
||||
## Cross-compiling
|
||||
|
||||
This project uses a modification of the [CI-GoReleaser](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment
|
||||
where the app can be cross-compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following
|
||||
command to open a bash shell to the container to poke around:
|
||||
This project uses a modification of the [CI-GoReleaser](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) Docker container for cross-compilation, defined in `docker/compiler/Dockerfile`.
|
||||
|
||||
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashapp/compiler:latest /bin/bash`
|
||||
To cross-compile the app yourself:
|
||||
|
||||
1. Run `make pre-ui`, `make generate` and `make ui` outside the container, to generate files and build the UI.
|
||||
2. Pull the latest compiler image from Docker Hub: `docker pull stashapp/compiler`
|
||||
3. Run `docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -it stashapp/compiler /bin/bash` to open a shell inside the container.
|
||||
4. From inside the container, run `make build-cc-all` to build for all platforms, or run `make build-cc-{platform}` to build for a specific platform (have a look at the `Makefile` for the list of targets).
|
||||
5. You will find the compiled binaries in `dist/`.
|
||||
|
||||
NOTE: Since the container is run as UID 0 (root), the resulting binaries (and the `dist/` folder itself, if it had to be created) will be owned by root.
|
||||
|
||||
## Profiling
|
||||
|
||||
|
||||
152
go.mod
152
go.mod
@@ -1,115 +1,119 @@
|
||||
module github.com/stashapp/stash
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.2
|
||||
github.com/99designs/gqlgen v0.17.49
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552
|
||||
github.com/Yamashou/gqlgenc v0.0.6
|
||||
github.com/anacrolix/dms v1.2.2
|
||||
github.com/antchfx/htmlquery v1.2.5-0.20211125074323-810ee8082758
|
||||
github.com/chromedp/cdproto v0.0.0-20210622022015-fe1827b46b84
|
||||
github.com/chromedp/chromedp v0.7.3
|
||||
github.com/corona10/goimagehash v1.0.3
|
||||
github.com/disintegration/imaging v1.6.0
|
||||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0
|
||||
github.com/golang-migrate/migrate/v4 v4.15.0-beta.1
|
||||
github.com/antchfx/htmlquery v1.3.0
|
||||
github.com/asticode/go-astisub v0.25.1
|
||||
github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617
|
||||
github.com/chromedp/chromedp v0.9.2
|
||||
github.com/corona10/goimagehash v1.1.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
||||
github.com/doug-martin/goqu/v9 v9.18.0
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/httplog v0.3.1
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/gofrs/uuid/v5 v5.1.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
|
||||
github.com/jmoiron/sqlx v1.3.1
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
github.com/kermieisinthehouse/gosx-notifier v0.1.2
|
||||
github.com/kermieisinthehouse/systray v1.2.4
|
||||
github.com/knadh/koanf v1.5.0
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cast v1.6.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/tidwall/gjson v1.9.3
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.16.0
|
||||
github.com/vearutop/statigz v1.4.0
|
||||
github.com/vektah/dataloaden v0.3.0
|
||||
github.com/vektah/gqlparser/v2 v2.5.16
|
||||
github.com/vektra/mockery/v2 v2.10.0
|
||||
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
|
||||
golang.org/x/image v0.5.0
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/sys v0.5.0
|
||||
golang.org/x/term v0.5.0
|
||||
golang.org/x/text v0.7.0
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
|
||||
github.com/zencoder/go-dash/v3 v3.0.2
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/net v0.26.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/term v0.21.0
|
||||
golang.org/x/text v0.16.0
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552
|
||||
github.com/asticode/go-astisub v0.20.0
|
||||
github.com/doug-martin/goqu/v9 v9.18.0
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/httplog v0.2.1
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/kermieisinthehouse/gosx-notifier v0.1.1
|
||||
github.com/kermieisinthehouse/systray v1.2.4
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/spf13/cast v1.4.1
|
||||
github.com/vearutop/statigz v1.1.6
|
||||
github.com/vektah/dataloaden v0.3.0
|
||||
github.com/vektah/gqlparser/v2 v2.4.2
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
|
||||
github.com/zencoder/go-dash/v3 v3.0.2
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/antchfx/xpath v1.2.0 // indirect
|
||||
github.com/antchfx/xpath v1.2.3 // indirect
|
||||
github.com/asticode/go-astikit v0.20.0 // indirect
|
||||
github.com/asticode/go-astits v1.8.0 // indirect
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.1.0-rc.5 // indirect
|
||||
github.com/gobwas/ws v1.3.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matryer/moq v0.2.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/zerolog v1.26.1 // indirect
|
||||
github.com/rs/zerolog v1.30.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/cobra v1.4.0 // indirect
|
||||
github.com/sosodev/duration v1.3.1 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cobra v1.7.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/stretchr/objx v0.2.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/spf13/viper v1.16.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/urfave/cli/v2 v2.8.1 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.2 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
|
||||
|
||||
go 1.19
|
||||
|
||||
32
gqlgen.yml
32
gqlgen.yml
@@ -14,6 +14,7 @@ resolver:
|
||||
struct_tag: gqlgen
|
||||
|
||||
autobind:
|
||||
- github.com/stashapp/stash/internal/api
|
||||
- github.com/stashapp/stash/pkg/models
|
||||
- github.com/stashapp/stash/pkg/plugin
|
||||
- github.com/stashapp/stash/pkg/scraper
|
||||
@@ -23,16 +24,38 @@ autobind:
|
||||
|
||||
models:
|
||||
# Scalars
|
||||
ID:
|
||||
model:
|
||||
- github.com/99designs/gqlgen/graphql.ID
|
||||
- github.com/99designs/gqlgen/graphql.IntID
|
||||
- github.com/stashapp/stash/pkg/models.FileID
|
||||
- github.com/stashapp/stash/pkg/models.FolderID
|
||||
Int64:
|
||||
model: github.com/99designs/gqlgen/graphql.Int64
|
||||
Timestamp:
|
||||
model: github.com/stashapp/stash/internal/api.Timestamp
|
||||
BoolMap:
|
||||
model: github.com/stashapp/stash/internal/api.BoolMap
|
||||
PluginConfigMap:
|
||||
model: github.com/stashapp/stash/internal/api.PluginConfigMap
|
||||
# define to force resolvers
|
||||
Image:
|
||||
model: github.com/stashapp/stash/pkg/models.Image
|
||||
fields:
|
||||
title:
|
||||
resolver: true
|
||||
VideoFile:
|
||||
fields:
|
||||
# override float fields - #1572
|
||||
duration:
|
||||
fieldName: DurationFinite
|
||||
frame_rate:
|
||||
fieldName: FrameRateFinite
|
||||
# movie is group under the hood
|
||||
Movie:
|
||||
model: github.com/stashapp/stash/pkg/models.Group
|
||||
MovieFilterType:
|
||||
model: github.com/stashapp/stash/pkg/models.GroupFilterType
|
||||
# autobind on config causes generation issues
|
||||
BlobsStorageType:
|
||||
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
|
||||
@@ -52,6 +75,8 @@ models:
|
||||
model: github.com/stashapp/stash/internal/manager/config.ConfigDisableDropdownCreate
|
||||
ScanMetadataOptions:
|
||||
model: github.com/stashapp/stash/internal/manager/config.ScanMetadataOptions
|
||||
CleanGeneratedInput:
|
||||
model: github.com/stashapp/stash/internal/manager/task.CleanGeneratedOptions
|
||||
AutoTagMetadataOptions:
|
||||
model: github.com/stashapp/stash/internal/manager/config.AutoTagMetadataOptions
|
||||
SystemStatus:
|
||||
@@ -118,4 +143,11 @@ models:
|
||||
model: github.com/stashapp/stash/internal/identify.MetadataOptions
|
||||
ScraperSourceInput:
|
||||
model: github.com/stashapp/stash/pkg/scraper.Source
|
||||
SavedFindFilterType:
|
||||
model: github.com/stashapp/stash/pkg/models.FindFilterType
|
||||
# force resolvers
|
||||
ConfigResult:
|
||||
fields:
|
||||
plugins:
|
||||
resolver: true
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
fragment SavedFilterData on SavedFilter {
|
||||
id
|
||||
mode
|
||||
name
|
||||
filter
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
fragment SlimMovieData on Movie {
|
||||
id
|
||||
name
|
||||
front_image_path
|
||||
rating100
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
fragment MovieData on Movie {
|
||||
id
|
||||
name
|
||||
aliases
|
||||
duration
|
||||
date
|
||||
rating100
|
||||
director
|
||||
|
||||
studio {
|
||||
...SlimStudioData
|
||||
}
|
||||
|
||||
synopsis
|
||||
url
|
||||
front_image_path
|
||||
back_image_path
|
||||
scene_count
|
||||
|
||||
scenes {
|
||||
id
|
||||
title
|
||||
path
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
mutation MovieCreate($input: MovieCreateInput!) {
|
||||
movieCreate(input: $input) {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
|
||||
mutation MovieUpdate($input: MovieUpdateInput!) {
|
||||
movieUpdate(input: $input) {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
|
||||
mutation BulkMovieUpdate($input: BulkMovieUpdateInput!) {
|
||||
bulkMovieUpdate(input: $input) {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
|
||||
mutation MovieDestroy($id: ID!) {
|
||||
movieDestroy(input: { id: $id })
|
||||
}
|
||||
|
||||
mutation MoviesDestroy($ids: [ID!]!) {
|
||||
moviesDestroy(ids: $ids)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
mutation ReloadPlugins {
|
||||
reloadPlugins
|
||||
}
|
||||
|
||||
mutation RunPluginTask(
|
||||
$plugin_id: ID!
|
||||
$task_name: String!
|
||||
$args: [PluginArgInput!]
|
||||
) {
|
||||
runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args: $args)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
mutation ReloadScrapers {
|
||||
reloadScrapers
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
query FindGalleries(
|
||||
$filter: FindFilterType
|
||||
$gallery_filter: GalleryFilterType
|
||||
) {
|
||||
findGalleries(gallery_filter: $gallery_filter, filter: $filter) {
|
||||
count
|
||||
galleries {
|
||||
...SlimGalleryData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query FindGallery($id: ID!) {
|
||||
findGallery(id: $id) {
|
||||
...GalleryData
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
query FindMovies($filter: FindFilterType, $movie_filter: MovieFilterType) {
|
||||
findMovies(filter: $filter, movie_filter: $movie_filter) {
|
||||
count
|
||||
movies {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query FindMovie($id: ID!) {
|
||||
findMovie(id: $id) {
|
||||
...MovieData
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
query FindPerformers(
|
||||
$filter: FindFilterType
|
||||
$performer_filter: PerformerFilterType
|
||||
) {
|
||||
findPerformers(filter: $filter, performer_filter: $performer_filter) {
|
||||
count
|
||||
performers {
|
||||
...PerformerData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query FindPerformer($id: ID!) {
|
||||
findPerformer(id: $id) {
|
||||
...PerformerData
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
query Plugins {
|
||||
plugins {
|
||||
id
|
||||
name
|
||||
description
|
||||
url
|
||||
version
|
||||
|
||||
tasks {
|
||||
name
|
||||
description
|
||||
}
|
||||
|
||||
hooks {
|
||||
name
|
||||
description
|
||||
hooks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query PluginTasks {
|
||||
pluginTasks {
|
||||
name
|
||||
description
|
||||
plugin {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
query ScrapeFreeonesPerformers($q: String!) {
|
||||
scrapeFreeonesPerformerList(query: $q)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ type Query {
|
||||
findSavedFilter(id: ID!): SavedFilter
|
||||
findSavedFilters(mode: FilterMode): [SavedFilter!]!
|
||||
findDefaultFilter(mode: FilterMode!): SavedFilter
|
||||
@deprecated(reason: "default filter now stored in UI config")
|
||||
|
||||
"Find a scene by ID or Checksum"
|
||||
findScene(id: ID, checksum: String): Scene
|
||||
@@ -12,7 +13,8 @@ type Query {
|
||||
"A function which queries Scene objects"
|
||||
findScenes(
|
||||
scene_filter: SceneFilterType
|
||||
scene_ids: [Int!]
|
||||
scene_ids: [Int!] @deprecated(reason: "use ids")
|
||||
ids: [ID!]
|
||||
filter: FindFilterType
|
||||
): FindScenesResultType!
|
||||
|
||||
@@ -50,7 +52,8 @@ type Query {
|
||||
"A function which queries Scene objects"
|
||||
findImages(
|
||||
image_filter: ImageFilterType
|
||||
image_ids: [Int!]
|
||||
image_ids: [Int!] @deprecated(reason: "use ids")
|
||||
ids: [ID!]
|
||||
filter: FindFilterType
|
||||
): FindImagesResultType!
|
||||
|
||||
@@ -60,6 +63,8 @@ type Query {
|
||||
findPerformers(
|
||||
performer_filter: PerformerFilterType
|
||||
filter: FindFilterType
|
||||
performer_ids: [Int!] @deprecated(reason: "use ids")
|
||||
ids: [ID!]
|
||||
): FindPerformersResultType!
|
||||
|
||||
"Find a studio by ID"
|
||||
@@ -68,26 +73,39 @@ type Query {
|
||||
findStudios(
|
||||
studio_filter: StudioFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindStudiosResultType!
|
||||
|
||||
"Find a movie by ID"
|
||||
findMovie(id: ID!): Movie
|
||||
findMovie(id: ID!): Movie @deprecated(reason: "Use findGroup instead")
|
||||
"A function which queries Movie objects"
|
||||
findMovies(
|
||||
movie_filter: MovieFilterType
|
||||
filter: FindFilterType
|
||||
): FindMoviesResultType!
|
||||
ids: [ID!]
|
||||
): FindMoviesResultType! @deprecated(reason: "Use findGroups instead")
|
||||
|
||||
"Find a group by ID"
|
||||
findGroup(id: ID!): Group
|
||||
"A function which queries Group objects"
|
||||
findGroups(
|
||||
group_filter: GroupFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindGroupsResultType!
|
||||
|
||||
findGallery(id: ID!): Gallery
|
||||
findGalleries(
|
||||
gallery_filter: GalleryFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindGalleriesResultType!
|
||||
|
||||
findTag(id: ID!): Tag
|
||||
findTags(
|
||||
tag_filter: TagFilterType
|
||||
filter: FindFilterType
|
||||
ids: [ID!]
|
||||
): FindTagsResultType!
|
||||
|
||||
"Retrieve random scene markers for the wall"
|
||||
@@ -108,14 +126,6 @@ type Query {
|
||||
|
||||
"List available scrapers"
|
||||
listScrapers(types: [ScrapeContentType!]!): [Scraper!]!
|
||||
listPerformerScrapers: [Scraper!]!
|
||||
@deprecated(reason: "Use listScrapers(types: [PERFORMER])")
|
||||
listSceneScrapers: [Scraper!]!
|
||||
@deprecated(reason: "Use listScrapers(types: [SCENE])")
|
||||
listGalleryScrapers: [Scraper!]!
|
||||
@deprecated(reason: "Use listScrapers(types: [GALLERY])")
|
||||
listMovieScrapers: [Scraper!]!
|
||||
@deprecated(reason: "Use listScrapers(types: [MOVIE])")
|
||||
|
||||
"Scrape for a single scene"
|
||||
scrapeSingleScene(
|
||||
@@ -155,7 +165,13 @@ type Query {
|
||||
scrapeSingleMovie(
|
||||
source: ScraperSourceInput!
|
||||
input: ScrapeSingleMovieInput!
|
||||
): [ScrapedMovie!]!
|
||||
): [ScrapedMovie!]! @deprecated(reason: "Use scrapeSingleGroup instead")
|
||||
|
||||
"Scrape for a single group"
|
||||
scrapeSingleGroup(
|
||||
source: ScraperSourceInput!
|
||||
input: ScrapeSingleGroupInput!
|
||||
): [ScrapedGroup!]!
|
||||
|
||||
"Scrapes content based on a URL"
|
||||
scrapeURL(url: String!, ty: ScrapeContentType!): ScrapedContent
|
||||
@@ -168,27 +184,9 @@ type Query {
|
||||
scrapeGalleryURL(url: String!): ScrapedGallery
|
||||
"Scrapes a complete movie record based on a URL"
|
||||
scrapeMovieURL(url: String!): ScrapedMovie
|
||||
|
||||
"Scrape a list of performers based on name"
|
||||
scrapePerformerList(scraper_id: ID!, query: String!): [ScrapedPerformer!]!
|
||||
@deprecated(reason: "use scrapeSinglePerformer")
|
||||
"Scrapes a complete performer record based on a scrapePerformerList result"
|
||||
scrapePerformer(
|
||||
scraper_id: ID!
|
||||
scraped_performer: ScrapedPerformerInput!
|
||||
): ScrapedPerformer @deprecated(reason: "use scrapeSinglePerformer")
|
||||
"Scrapes a complete scene record based on an existing scene"
|
||||
scrapeScene(scraper_id: ID!, scene: SceneUpdateInput!): ScrapedScene
|
||||
@deprecated(reason: "use scrapeSingleScene")
|
||||
"Scrapes a complete gallery record based on an existing gallery"
|
||||
scrapeGallery(scraper_id: ID!, gallery: GalleryUpdateInput!): ScrapedGallery
|
||||
@deprecated(reason: "use scrapeSingleGallery")
|
||||
|
||||
"Scrape a list of performers from a query"
|
||||
scrapeFreeonesPerformerList(query: String!): [String!]!
|
||||
@deprecated(
|
||||
reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones"
|
||||
)
|
||||
@deprecated(reason: "Use scrapeGroupURL instead")
|
||||
"Scrapes a complete group record based on a URL"
|
||||
scrapeGroupURL(url: String!): ScrapedGroup
|
||||
|
||||
# Plugins
|
||||
"List loaded plugins"
|
||||
@@ -196,6 +194,12 @@ type Query {
|
||||
"List available plugin operations"
|
||||
pluginTasks: [PluginTask!]
|
||||
|
||||
# Packages
|
||||
"List installed packages"
|
||||
installedPackages(type: PackageType!): [Package!]!
|
||||
"List available packages"
|
||||
availablePackages(type: PackageType!, source: String!): [Package!]!
|
||||
|
||||
# Config
|
||||
"Returns the current, complete configuration"
|
||||
configuration: ConfigResult!
|
||||
@@ -219,14 +223,16 @@ type Query {
|
||||
|
||||
# Get everything
|
||||
|
||||
allScenes: [Scene!]!
|
||||
allScenes: [Scene!]! @deprecated(reason: "Use findScenes instead")
|
||||
allSceneMarkers: [SceneMarker!]!
|
||||
allImages: [Image!]!
|
||||
allGalleries: [Gallery!]!
|
||||
@deprecated(reason: "Use findSceneMarkers instead")
|
||||
allImages: [Image!]! @deprecated(reason: "Use findImages instead")
|
||||
allGalleries: [Gallery!]! @deprecated(reason: "Use findGalleries instead")
|
||||
|
||||
allPerformers: [Performer!]!
|
||||
allStudios: [Studio!]!
|
||||
allMovies: [Movie!]!
|
||||
allTags: [Tag!]!
|
||||
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
|
||||
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
|
||||
allMovies: [Movie!]! @deprecated(reason: "Use findGroups instead")
|
||||
|
||||
# Get everything with minimal metadata
|
||||
|
||||
@@ -239,7 +245,12 @@ type Query {
|
||||
|
||||
type Mutation {
|
||||
setup(input: SetupInput!): Boolean!
|
||||
migrate(input: MigrateInput!): Boolean!
|
||||
|
||||
"Migrates the schema to the required version. Returns the job ID"
|
||||
migrate(input: MigrateInput!): ID!
|
||||
|
||||
"Downloads and installs ffmpeg and ffprobe binaries into the configuration directory. Returns the job ID."
|
||||
downloadFFMpeg: ID!
|
||||
|
||||
sceneCreate(input: SceneCreateInput!): Scene
|
||||
sceneUpdate(input: SceneUpdateInput!): Scene
|
||||
@@ -250,17 +261,38 @@ type Mutation {
|
||||
scenesUpdate(input: [SceneUpdateInput!]!): [Scene]
|
||||
|
||||
"Increments the o-counter for a scene. Returns the new value"
|
||||
sceneIncrementO(id: ID!): Int!
|
||||
sceneIncrementO(id: ID!): Int! @deprecated(reason: "Use sceneAddO instead")
|
||||
"Decrements the o-counter for a scene. Returns the new value"
|
||||
sceneDecrementO(id: ID!): Int!
|
||||
sceneDecrementO(id: ID!): Int! @deprecated(reason: "Use sceneRemoveO instead")
|
||||
|
||||
"Increments the o-counter for a scene. Uses the current time if none provided."
|
||||
sceneAddO(id: ID!, times: [Timestamp!]): HistoryMutationResult!
|
||||
"Decrements the o-counter for a scene, removing the last recorded time if specific time not provided. Returns the new value"
|
||||
sceneDeleteO(id: ID!, times: [Timestamp!]): HistoryMutationResult!
|
||||
|
||||
"Resets the o-counter for a scene to 0. Returns the new value"
|
||||
sceneResetO(id: ID!): Int!
|
||||
|
||||
"Sets the resume time point (if provided) and adds the provided duration to the scene's play duration"
|
||||
sceneSaveActivity(id: ID!, resume_time: Float, playDuration: Float): Boolean!
|
||||
|
||||
"Resets the resume time point and play duration"
|
||||
sceneResetActivity(
|
||||
id: ID!
|
||||
reset_resume: Boolean
|
||||
reset_duration: Boolean
|
||||
): Boolean!
|
||||
|
||||
"Increments the play count for the scene. Returns the new play count value."
|
||||
sceneIncrementPlayCount(id: ID!): Int!
|
||||
@deprecated(reason: "Use sceneAddPlay instead")
|
||||
|
||||
"Increments the play count for the scene. Uses the current time if none provided."
|
||||
sceneAddPlay(id: ID!, times: [Timestamp!]): HistoryMutationResult!
|
||||
"Decrements the play count for the scene, removing the specific times or the last recorded time if not provided."
|
||||
sceneDeletePlay(id: ID!, times: [Timestamp!]): HistoryMutationResult!
|
||||
"Resets the play count for a scene to 0. Returns the new play count value."
|
||||
sceneResetPlayCount(id: ID!): Int!
|
||||
|
||||
"Generates screenshot at specified time in seconds. Leave empty to generate default screenshot"
|
||||
sceneGenerateScreenshot(id: ID!, at: Float): String!
|
||||
@@ -292,6 +324,8 @@ type Mutation {
|
||||
|
||||
addGalleryImages(input: GalleryAddInput!): Boolean!
|
||||
removeGalleryImages(input: GalleryRemoveInput!): Boolean!
|
||||
setGalleryCover(input: GallerySetCoverInput!): Boolean!
|
||||
resetGalleryCover(input: GalleryResetCoverInput!): Boolean!
|
||||
|
||||
galleryChapterCreate(input: GalleryChapterCreateInput!): GalleryChapter
|
||||
galleryChapterUpdate(input: GalleryChapterUpdateInput!): GalleryChapter
|
||||
@@ -309,16 +343,34 @@ type Mutation {
|
||||
studiosDestroy(ids: [ID!]!): Boolean!
|
||||
|
||||
movieCreate(input: MovieCreateInput!): Movie
|
||||
@deprecated(reason: "Use groupCreate instead")
|
||||
movieUpdate(input: MovieUpdateInput!): Movie
|
||||
@deprecated(reason: "Use groupUpdate instead")
|
||||
movieDestroy(input: MovieDestroyInput!): Boolean!
|
||||
@deprecated(reason: "Use groupDestroy instead")
|
||||
moviesDestroy(ids: [ID!]!): Boolean!
|
||||
@deprecated(reason: "Use groupsDestroy instead")
|
||||
bulkMovieUpdate(input: BulkMovieUpdateInput!): [Movie!]
|
||||
@deprecated(reason: "Use bulkGroupUpdate instead")
|
||||
|
||||
groupCreate(input: GroupCreateInput!): Group
|
||||
groupUpdate(input: GroupUpdateInput!): Group
|
||||
groupDestroy(input: GroupDestroyInput!): Boolean!
|
||||
groupsDestroy(ids: [ID!]!): Boolean!
|
||||
bulkGroupUpdate(input: BulkGroupUpdateInput!): [Group!]
|
||||
|
||||
addGroupSubGroups(input: GroupSubGroupAddInput!): Boolean!
|
||||
removeGroupSubGroups(input: GroupSubGroupRemoveInput!): Boolean!
|
||||
|
||||
"Reorder sub groups within a group. Returns true if successful."
|
||||
reorderSubGroups(input: ReorderSubGroupsInput!): Boolean!
|
||||
|
||||
tagCreate(input: TagCreateInput!): Tag
|
||||
tagUpdate(input: TagUpdateInput!): Tag
|
||||
tagDestroy(input: TagDestroyInput!): Boolean!
|
||||
tagsDestroy(ids: [ID!]!): Boolean!
|
||||
tagsMerge(input: TagsMergeInput!): Tag
|
||||
bulkTagUpdate(input: BulkTagUpdateInput!): [Tag!]
|
||||
|
||||
"""
|
||||
Moves the given files to the given destination. Returns true if successful.
|
||||
@@ -332,10 +384,13 @@ type Mutation {
|
||||
moveFiles(input: MoveFilesInput!): Boolean!
|
||||
deleteFiles(ids: [ID!]!): Boolean!
|
||||
|
||||
fileSetFingerprints(input: FileSetFingerprintsInput!): Boolean!
|
||||
|
||||
# Saved filters
|
||||
saveFilter(input: SaveFilterInput!): SavedFilter!
|
||||
destroySavedFilter(input: DestroyFilterInput!): Boolean!
|
||||
setDefaultFilter(input: SetDefaultFilterInput!): Boolean!
|
||||
@deprecated(reason: "now uses UI config")
|
||||
|
||||
"Change general configuration options"
|
||||
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
|
||||
@@ -346,9 +401,19 @@ type Mutation {
|
||||
input: ConfigDefaultSettingsInput!
|
||||
): ConfigDefaultSettingsResult!
|
||||
|
||||
# overwrites the entire UI configuration
|
||||
configureUI(input: Map!): Map!
|
||||
# sets a single UI key value
|
||||
"overwrites the entire plugin configuration for the given plugin"
|
||||
configurePlugin(plugin_id: ID!, input: Map!): Map!
|
||||
|
||||
"""
|
||||
overwrites the UI configuration
|
||||
if input is provided, then the entire UI configuration is replaced
|
||||
if partial is provided, then the partial UI configuration is merged into the existing UI configuration
|
||||
"""
|
||||
configureUI(input: Map, partial: Map): Map!
|
||||
"""
|
||||
sets a single UI key value
|
||||
key is a dot separated path to the value
|
||||
"""
|
||||
configureUISetting(key: String!, value: Any): Map!
|
||||
|
||||
"Generate and set (or clear) API key"
|
||||
@@ -372,6 +437,8 @@ type Mutation {
|
||||
metadataAutoTag(input: AutoTagMetadataInput!): ID!
|
||||
"Clean metadata. Returns the job ID"
|
||||
metadataClean(input: CleanMetadataInput!): ID!
|
||||
"Clean generated files. Returns the job ID"
|
||||
metadataCleanGenerated(input: CleanGeneratedInput!): ID!
|
||||
"Identifies scenes using scrapers. Returns the job ID"
|
||||
metadataIdentify(input: IdentifyMetadataInput!): ID!
|
||||
|
||||
@@ -391,14 +458,60 @@ type Mutation {
|
||||
"Reload scrapers"
|
||||
reloadScrapers: Boolean!
|
||||
|
||||
"Run plugin task. Returns the job ID"
|
||||
"""
|
||||
Enable/disable plugins - enabledMap is a map of plugin IDs to enabled booleans.
|
||||
Plugins not in the map are not affected.
|
||||
"""
|
||||
setPluginsEnabled(enabledMap: BoolMap!): Boolean!
|
||||
|
||||
"""
|
||||
Run a plugin task.
|
||||
If task_name is provided, then the task must exist in the plugin config and the tasks configuration
|
||||
will be used to run the plugin.
|
||||
If no task_name is provided, then the plugin will be executed with the arguments provided only.
|
||||
Returns the job ID
|
||||
"""
|
||||
runPluginTask(
|
||||
plugin_id: ID!
|
||||
task_name: String!
|
||||
args: [PluginArgInput!]
|
||||
"if provided, then the default args will be applied"
|
||||
task_name: String
|
||||
"displayed in the task queue"
|
||||
description: String
|
||||
args: [PluginArgInput!] @deprecated(reason: "Use args_map instead")
|
||||
args_map: Map
|
||||
): ID!
|
||||
|
||||
"""
|
||||
Runs a plugin operation. The operation is run immediately and does not use the job queue.
|
||||
Returns a map of the result.
|
||||
"""
|
||||
runPluginOperation(plugin_id: ID!, args: Map): Any
|
||||
|
||||
reloadPlugins: Boolean!
|
||||
|
||||
"""
|
||||
Installs the given packages.
|
||||
If a package is already installed, it will be updated if needed..
|
||||
If an error occurs when installing a package, the job will continue to install the remaining packages.
|
||||
Returns the job ID
|
||||
"""
|
||||
installPackages(type: PackageType!, packages: [PackageSpecInput!]!): ID!
|
||||
"""
|
||||
Updates the given packages.
|
||||
If a package is not installed, it will not be installed.
|
||||
If a package does not need to be updated, it will not be updated.
|
||||
If no packages are provided, all packages of the given type will be updated.
|
||||
If an error occurs when updating a package, the job will continue to update the remaining packages.
|
||||
Returns the job ID.
|
||||
"""
|
||||
updatePackages(type: PackageType!, packages: [PackageSpecInput!]): ID!
|
||||
"""
|
||||
Uninstalls the given packages.
|
||||
If an error occurs when uninstalling a package, the job will continue to uninstall the remaining packages.
|
||||
Returns the job ID
|
||||
"""
|
||||
uninstallPackages(type: PackageType!, packages: [PackageSpecInput!]!): ID!
|
||||
|
||||
stopJob(job_id: ID!): Boolean!
|
||||
stopAllJobs: Boolean!
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ input SetupInput {
|
||||
generatedLocation: String!
|
||||
"Empty to indicate default"
|
||||
cacheLocation: String!
|
||||
"Empty to indicate database storage for blobs"
|
||||
storeBlobsInDatabase: Boolean!
|
||||
"Empty to indicate default - only applicable if storeBlobsInDatabase is false"
|
||||
blobsLocation: String!
|
||||
}
|
||||
|
||||
@@ -72,12 +73,18 @@ input ConfigGeneralInput {
|
||||
metadataPath: String
|
||||
"Path to scrapers"
|
||||
scrapersPath: String
|
||||
"Path to plugins"
|
||||
pluginsPath: String
|
||||
"Path to cache"
|
||||
cachePath: String
|
||||
"Path to blobs - required for filesystem blob storage"
|
||||
blobsPath: String
|
||||
"Where to store blobs"
|
||||
blobsStorage: BlobsStorageType
|
||||
"Path to the ffmpeg binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffmpegPath: String
|
||||
"Path to the ffprobe binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffprobePath: String
|
||||
"Whether to calculate MD5 checksums for scene video files"
|
||||
calculateMD5: Boolean
|
||||
"Hash algorithm to use for generated file naming"
|
||||
@@ -138,8 +145,6 @@ input ConfigGeneralInput {
|
||||
password: String
|
||||
"Maximum session cookie age"
|
||||
maxSessionAge: Int
|
||||
"Comma separated list of proxies to allow traffic from"
|
||||
trustedProxies: [String!] @deprecated(reason: "no longer supported")
|
||||
"Name of the log file"
|
||||
logFile: String
|
||||
"Whether to also output to stderr"
|
||||
@@ -164,25 +169,15 @@ input ConfigGeneralInput {
|
||||
imageExcludes: [String!]
|
||||
"Custom Performer Image Location"
|
||||
customPerformerImageLocation: String
|
||||
"Scraper user agent string"
|
||||
scraperUserAgent: String
|
||||
@deprecated(
|
||||
reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead"
|
||||
)
|
||||
"Scraper CDP path. Path to chrome executable or remote address"
|
||||
scraperCDPPath: String
|
||||
@deprecated(
|
||||
reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead"
|
||||
)
|
||||
"Whether the scraper should check for invalid certificates"
|
||||
scraperCertCheck: Boolean
|
||||
@deprecated(
|
||||
reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead"
|
||||
)
|
||||
"Stash-box instances used for tagging"
|
||||
stashBoxes: [StashBoxInput!]
|
||||
"Python path - resolved using path if unset"
|
||||
pythonPath: String
|
||||
|
||||
"Source of scraper packages"
|
||||
scraperPackageSources: [PackageSourceInput!]
|
||||
"Source of plugin packages"
|
||||
pluginPackageSources: [PackageSourceInput!]
|
||||
}
|
||||
|
||||
type ConfigGeneralResult {
|
||||
@@ -200,12 +195,18 @@ type ConfigGeneralResult {
|
||||
configFilePath: String!
|
||||
"Path to scrapers"
|
||||
scrapersPath: String!
|
||||
"Path to plugins"
|
||||
pluginsPath: String!
|
||||
"Path to cache"
|
||||
cachePath: String!
|
||||
"Path to blobs - required for filesystem blob storage"
|
||||
blobsPath: String!
|
||||
"Where to store blobs"
|
||||
blobsStorage: BlobsStorageType!
|
||||
"Path to the ffmpeg binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffmpegPath: String!
|
||||
"Path to the ffprobe binary. If empty, stash will attempt to find it in the path or config directory"
|
||||
ffprobePath: String!
|
||||
"Whether to calculate MD5 checksums for scene video files"
|
||||
calculateMD5: Boolean!
|
||||
"Hash algorithm to use for generated file naming"
|
||||
@@ -268,8 +269,6 @@ type ConfigGeneralResult {
|
||||
password: String!
|
||||
"Maximum session cookie age"
|
||||
maxSessionAge: Int!
|
||||
"Comma separated list of proxies to allow traffic from"
|
||||
trustedProxies: [String!] @deprecated(reason: "no longer supported")
|
||||
"Name of the log file"
|
||||
logFile: String
|
||||
"Whether to also output to stderr"
|
||||
@@ -294,19 +293,15 @@ type ConfigGeneralResult {
|
||||
imageExcludes: [String!]!
|
||||
"Custom Performer Image Location"
|
||||
customPerformerImageLocation: String
|
||||
"Scraper user agent string"
|
||||
scraperUserAgent: String
|
||||
@deprecated(reason: "use ConfigResult.scraping instead")
|
||||
"Scraper CDP path. Path to chrome executable or remote address"
|
||||
scraperCDPPath: String
|
||||
@deprecated(reason: "use ConfigResult.scraping instead")
|
||||
"Whether the scraper should check for invalid certificates"
|
||||
scraperCertCheck: Boolean!
|
||||
@deprecated(reason: "use ConfigResult.scraping instead")
|
||||
"Stash-box instances used for tagging"
|
||||
stashBoxes: [StashBox!]!
|
||||
"Python path - resolved using path if unset"
|
||||
pythonPath: String!
|
||||
|
||||
"Source of scraper packages"
|
||||
scraperPackageSources: [PackageSource!]!
|
||||
"Source of plugin packages"
|
||||
pluginPackageSources: [PackageSource!]!
|
||||
}
|
||||
|
||||
input ConfigDisableDropdownCreateInput {
|
||||
@@ -387,9 +382,6 @@ input ConfigInterfaceInput {
|
||||
"Interface language"
|
||||
language: String
|
||||
|
||||
"Slideshow Delay"
|
||||
slideshowDelay: Int @deprecated(reason: "Use imageLightbox.slideshowDelay")
|
||||
|
||||
imageLightbox: ConfigImageLightboxInput
|
||||
|
||||
"Set to true to disable creating new objects via the dropdown menus"
|
||||
@@ -460,15 +452,10 @@ type ConfigInterfaceResult {
|
||||
"Interface language"
|
||||
language: String
|
||||
|
||||
"Slideshow Delay"
|
||||
slideshowDelay: Int @deprecated(reason: "Use imageLightbox.slideshowDelay")
|
||||
|
||||
imageLightbox: ConfigImageLightboxResult!
|
||||
|
||||
"Fields are true if creating via dropdown menus are disabled"
|
||||
disableDropdownCreate: ConfigDisableDropdownCreate!
|
||||
disabledDropdownCreate: ConfigDisableDropdownCreate!
|
||||
@deprecated(reason: "Use disableDropdownCreate")
|
||||
|
||||
"Handy Connection Key"
|
||||
handyKey: String
|
||||
@@ -482,6 +469,8 @@ input ConfigDLNAInput {
|
||||
serverName: String
|
||||
"True if DLNA service should be enabled by default"
|
||||
enabled: Boolean
|
||||
"Defaults to 1338"
|
||||
port: Int
|
||||
"List of IPs whitelisted for DLNA service"
|
||||
whitelistedIPs: [String!]
|
||||
"List of interfaces to run DLNA on. Empty for all"
|
||||
@@ -494,6 +483,8 @@ type ConfigDLNAResult {
|
||||
serverName: String!
|
||||
"True if DLNA service should be enabled by default"
|
||||
enabled: Boolean!
|
||||
"Defaults to 1338"
|
||||
port: Int!
|
||||
"List of IPs whitelisted for DLNA service"
|
||||
whitelistedIPs: [String!]!
|
||||
"List of interfaces to run DLNA on. Empty for all"
|
||||
@@ -556,6 +547,7 @@ type ConfigResult {
|
||||
scraping: ConfigScrapingResult!
|
||||
defaults: ConfigDefaultSettingsResult!
|
||||
ui: Map!
|
||||
plugins(include: [ID!]): PluginConfigMap!
|
||||
}
|
||||
|
||||
"Directory structure of a path"
|
||||
|
||||
@@ -27,6 +27,7 @@ interface BaseFile {
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprint(type: String!): String
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
created_at: Time!
|
||||
@@ -44,6 +45,7 @@ type VideoFile implements BaseFile {
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprint(type: String!): String
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
format: String!
|
||||
@@ -70,6 +72,7 @@ type ImageFile implements BaseFile {
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprint(type: String!): String
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
width: Int!
|
||||
@@ -92,6 +95,7 @@ type GalleryFile implements BaseFile {
|
||||
mod_time: Time!
|
||||
size: Int64!
|
||||
|
||||
fingerprint(type: String!): String
|
||||
fingerprints: [Fingerprint!]!
|
||||
|
||||
created_at: Time!
|
||||
@@ -109,3 +113,15 @@ input MoveFilesInput {
|
||||
"valid only for single file id. If empty, existing basename is used"
|
||||
destination_basename: String
|
||||
}
|
||||
|
||||
input SetFingerprintsInput {
|
||||
type: String!
|
||||
"an null value will remove the fingerprint"
|
||||
value: String
|
||||
}
|
||||
|
||||
input FileSetFingerprintsInput {
|
||||
id: ID!
|
||||
"only supplied fingerprint types will be modified"
|
||||
fingerprints: [SetFingerprintsInput!]!
|
||||
}
|
||||
|
||||
@@ -8,6 +8,18 @@ input FindFilterType {
|
||||
page: Int
|
||||
"use per_page = -1 to indicate all results. Defaults to 25."
|
||||
per_page: Int
|
||||
# TODO - this should be refactored to not use a string
|
||||
sort: String
|
||||
direction: SortDirectionEnum
|
||||
}
|
||||
|
||||
type SavedFindFilterType {
|
||||
q: String
|
||||
page: Int
|
||||
"""
|
||||
use per_page = -1 to indicate all results. Defaults to 25.
|
||||
"""
|
||||
per_page: Int
|
||||
sort: String
|
||||
direction: SortDirectionEnum
|
||||
}
|
||||
@@ -50,6 +62,19 @@ input ResolutionCriterionInput {
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
enum OrientationEnum {
|
||||
"Landscape"
|
||||
LANDSCAPE
|
||||
"Portrait"
|
||||
PORTRAIT
|
||||
"Square"
|
||||
SQUARE
|
||||
}
|
||||
|
||||
input OrientationCriterionInput {
|
||||
value: [OrientationEnum!]!
|
||||
}
|
||||
|
||||
input PHashDuplicationCriterionInput {
|
||||
duplicated: Boolean
|
||||
"Currently unimplemented"
|
||||
@@ -87,8 +112,6 @@ input PerformerFilterType {
|
||||
country: StringCriterionInput
|
||||
"Filter by eye color"
|
||||
eye_color: StringCriterionInput
|
||||
"Filter by height"
|
||||
height: StringCriterionInput @deprecated(reason: "Use height_cm instead")
|
||||
"Filter by height in cm"
|
||||
height_cm: IntCriterionInput
|
||||
"Filter by measurements"
|
||||
@@ -121,16 +144,12 @@ input PerformerFilterType {
|
||||
image_count: IntCriterionInput
|
||||
"Filter by gallery count"
|
||||
gallery_count: IntCriterionInput
|
||||
"Filter by play count"
|
||||
play_count: IntCriterionInput
|
||||
"Filter by o count"
|
||||
o_counter: IntCriterionInput
|
||||
"Filter by StashID"
|
||||
stash_id: StringCriterionInput
|
||||
@deprecated(reason: "Use stash_id_endpoint instead")
|
||||
"Filter by StashID"
|
||||
stash_id_endpoint: StashIDCriterionInput
|
||||
"Filter by rating"
|
||||
rating: IntCriterionInput
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter by url"
|
||||
@@ -151,6 +170,14 @@ input PerformerFilterType {
|
||||
birthdate: DateCriterionInput
|
||||
"Filter by death date"
|
||||
death_date: DateCriterionInput
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related images that meet this criteria"
|
||||
images_filter: ImageFilterType
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by related tags that meet this criteria"
|
||||
tags_filter: TagFilterType
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
@@ -158,14 +185,14 @@ input PerformerFilterType {
|
||||
}
|
||||
|
||||
input SceneMarkerFilterType {
|
||||
"Filter to only include scene markers with this tag"
|
||||
tag_id: ID @deprecated(reason: "use tags filter instead")
|
||||
"Filter to only include scene markers with these tags"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter to only include scene markers attached to a scene with these tags"
|
||||
scene_tags: HierarchicalMultiCriterionInput
|
||||
"Filter to only include scene markers with these performers"
|
||||
performers: MultiCriterionInput
|
||||
"Filter to only include scene markers from these scenes"
|
||||
scenes: MultiCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
@@ -176,6 +203,8 @@ input SceneMarkerFilterType {
|
||||
scene_created_at: TimestampCriterionInput
|
||||
"Filter by lscene ast update time"
|
||||
scene_updated_at: TimestampCriterionInput
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scene_filter: SceneFilterType
|
||||
}
|
||||
|
||||
input SceneFilterType {
|
||||
@@ -201,9 +230,6 @@ input SceneFilterType {
|
||||
path: StringCriterionInput
|
||||
"Filter by file count"
|
||||
file_count: IntCriterionInput
|
||||
"Filter by rating"
|
||||
rating: IntCriterionInput
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter by organized"
|
||||
@@ -214,6 +240,12 @@ input SceneFilterType {
|
||||
duplicated: PHashDuplicationCriterionInput
|
||||
"Filter by resolution"
|
||||
resolution: ResolutionCriterionInput
|
||||
"Filter by orientation"
|
||||
orientation: OrientationCriterionInput
|
||||
"Filter by frame rate"
|
||||
framerate: IntCriterionInput
|
||||
"Filter by bit rate"
|
||||
bitrate: IntCriterionInput
|
||||
"Filter by video codec"
|
||||
video_codec: StringCriterionInput
|
||||
"Filter by audio codec"
|
||||
@@ -227,7 +259,11 @@ input SceneFilterType {
|
||||
"Filter to only include scenes with this studio"
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"Filter to only include scenes with this movie"
|
||||
movies: MultiCriterionInput
|
||||
movies: MultiCriterionInput @deprecated(reason: "use groups instead")
|
||||
"Filter to only include scenes with this group"
|
||||
groups: HierarchicalMultiCriterionInput
|
||||
"Filter to only include scenes with this gallery"
|
||||
galleries: MultiCriterionInput
|
||||
"Filter to only include scenes with these tags"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter by tag count"
|
||||
@@ -243,9 +279,6 @@ input SceneFilterType {
|
||||
"Filter by performer count"
|
||||
performer_count: IntCriterionInput
|
||||
"Filter by StashID"
|
||||
stash_id: StringCriterionInput
|
||||
@deprecated(reason: "Use stash_id_endpoint instead")
|
||||
"Filter by StashID"
|
||||
stash_id_endpoint: StashIDCriterionInput
|
||||
"Filter by url"
|
||||
url: StringCriterionInput
|
||||
@@ -261,24 +294,43 @@ input SceneFilterType {
|
||||
play_count: IntCriterionInput
|
||||
"Filter by play duration (in seconds)"
|
||||
play_duration: IntCriterionInput
|
||||
"Filter by scene last played time"
|
||||
last_played_at: TimestampCriterionInput
|
||||
"Filter by date"
|
||||
date: DateCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by related performers that meet this criteria"
|
||||
performers_filter: PerformerFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
"Filter by related tags that meet this criteria"
|
||||
tags_filter: TagFilterType
|
||||
"Filter by related movies that meet this criteria"
|
||||
movies_filter: MovieFilterType
|
||||
@deprecated(reason: "use groups_filter instead")
|
||||
"Filter by related groups that meet this criteria"
|
||||
groups_filter: GroupFilterType
|
||||
"Filter by related markers that meet this criteria"
|
||||
markers_filter: SceneMarkerFilterType
|
||||
}
|
||||
|
||||
input MovieFilterType {
|
||||
AND: MovieFilterType
|
||||
OR: MovieFilterType
|
||||
NOT: MovieFilterType
|
||||
|
||||
name: StringCriterionInput
|
||||
director: StringCriterionInput
|
||||
synopsis: StringCriterionInput
|
||||
|
||||
"Filter by duration (in seconds)"
|
||||
duration: IntCriterionInput
|
||||
"Filter by rating"
|
||||
rating: IntCriterionInput
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter to only include movies with this studio"
|
||||
@@ -289,12 +341,68 @@ input MovieFilterType {
|
||||
url: StringCriterionInput
|
||||
"Filter to only include movies where performer appears in a scene"
|
||||
performers: MultiCriterionInput
|
||||
"Filter to only include movies with these tags"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter by tag count"
|
||||
tag_count: IntCriterionInput
|
||||
"Filter by date"
|
||||
date: DateCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
}
|
||||
|
||||
input GroupFilterType {
|
||||
AND: GroupFilterType
|
||||
OR: GroupFilterType
|
||||
NOT: GroupFilterType
|
||||
|
||||
name: StringCriterionInput
|
||||
director: StringCriterionInput
|
||||
synopsis: StringCriterionInput
|
||||
|
||||
"Filter by duration (in seconds)"
|
||||
duration: IntCriterionInput
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter to only include groups with this studio"
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"Filter to only include groups missing this property"
|
||||
is_missing: String
|
||||
"Filter by url"
|
||||
url: StringCriterionInput
|
||||
"Filter to only include groups where performer appears in a scene"
|
||||
performers: MultiCriterionInput
|
||||
"Filter to only include groups with these tags"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter by tag count"
|
||||
tag_count: IntCriterionInput
|
||||
"Filter by date"
|
||||
date: DateCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
|
||||
"Filter by containing groups"
|
||||
containing_groups: HierarchicalMultiCriterionInput
|
||||
"Filter by sub groups"
|
||||
sub_groups: HierarchicalMultiCriterionInput
|
||||
"Filter by number of containing groups the group has"
|
||||
containing_group_count: IntCriterionInput
|
||||
"Filter by number of sub-groups the group has"
|
||||
sub_group_count: IntCriterionInput
|
||||
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
}
|
||||
|
||||
input StudioFilterType {
|
||||
@@ -307,29 +415,37 @@ input StudioFilterType {
|
||||
"Filter to only include studios with this parent studio"
|
||||
parents: MultiCriterionInput
|
||||
"Filter by StashID"
|
||||
stash_id: StringCriterionInput
|
||||
@deprecated(reason: "Use stash_id_endpoint instead")
|
||||
"Filter by StashID"
|
||||
stash_id_endpoint: StashIDCriterionInput
|
||||
"Filter to only include studios with these tags"
|
||||
tags: HierarchicalMultiCriterionInput
|
||||
"Filter to only include studios missing this property"
|
||||
is_missing: String
|
||||
"Filter by rating"
|
||||
rating: IntCriterionInput
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter by favorite"
|
||||
favorite: Boolean
|
||||
"Filter by scene count"
|
||||
scene_count: IntCriterionInput
|
||||
"Filter by image count"
|
||||
image_count: IntCriterionInput
|
||||
"Filter by gallery count"
|
||||
gallery_count: IntCriterionInput
|
||||
"Filter by tag count"
|
||||
tag_count: IntCriterionInput
|
||||
"Filter by url"
|
||||
url: StringCriterionInput
|
||||
"Filter by studio aliases"
|
||||
aliases: StringCriterionInput
|
||||
"Filter by subsidiary studio count"
|
||||
child_count: IntCriterionInput
|
||||
"Filter by autotag ignore value"
|
||||
ignore_auto_tag: Boolean
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related images that meet this criteria"
|
||||
images_filter: ImageFilterType
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
@@ -355,9 +471,6 @@ input GalleryFilterType {
|
||||
is_missing: String
|
||||
"Filter to include/exclude galleries that were created from zip"
|
||||
is_zip: Boolean
|
||||
"Filter by rating"
|
||||
rating: IntCriterionInput
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter by organized"
|
||||
@@ -366,6 +479,8 @@ input GalleryFilterType {
|
||||
average_resolution: ResolutionCriterionInput
|
||||
"Filter to only include galleries that have chapters. `true` or `false`"
|
||||
has_chapters: String
|
||||
"Filter to only include galleries with these scenes"
|
||||
scenes: MultiCriterionInput
|
||||
"Filter to only include galleries with this studio"
|
||||
studios: HierarchicalMultiCriterionInput
|
||||
"Filter to only include galleries with these tags"
|
||||
@@ -392,6 +507,21 @@ input GalleryFilterType {
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
"Filter by studio code"
|
||||
code: StringCriterionInput
|
||||
"Filter by photographer"
|
||||
photographer: StringCriterionInput
|
||||
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related images that meet this criteria"
|
||||
images_filter: ImageFilterType
|
||||
"Filter by related performers that meet this criteria"
|
||||
performers_filter: PerformerFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
"Filter by related tags that meet this criteria"
|
||||
tags_filter: TagFilterType
|
||||
}
|
||||
|
||||
input TagFilterType {
|
||||
@@ -405,6 +535,9 @@ input TagFilterType {
|
||||
"Filter by tag aliases"
|
||||
aliases: StringCriterionInput
|
||||
|
||||
"Filter by favorite"
|
||||
favorite: Boolean
|
||||
|
||||
"Filter by tag description"
|
||||
description: StringCriterionInput
|
||||
|
||||
@@ -423,6 +556,15 @@ input TagFilterType {
|
||||
"Filter by number of performers with this tag"
|
||||
performer_count: IntCriterionInput
|
||||
|
||||
"Filter by number of studios with this tag"
|
||||
studio_count: IntCriterionInput
|
||||
|
||||
"Filter by number of movies with this tag"
|
||||
movie_count: IntCriterionInput
|
||||
|
||||
"Filter by number of group with this tag"
|
||||
group_count: IntCriterionInput
|
||||
|
||||
"Filter by number of markers with this tag"
|
||||
marker_count: IntCriterionInput
|
||||
|
||||
@@ -441,6 +583,13 @@ input TagFilterType {
|
||||
"Filter by autotag ignore value"
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related images that meet this criteria"
|
||||
images_filter: ImageFilterType
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
|
||||
@@ -454,6 +603,7 @@ input ImageFilterType {
|
||||
NOT: ImageFilterType
|
||||
|
||||
title: StringCriterionInput
|
||||
details: StringCriterionInput
|
||||
|
||||
" Filter by image id"
|
||||
id: IntCriterionInput
|
||||
@@ -463,9 +613,6 @@ input ImageFilterType {
|
||||
path: StringCriterionInput
|
||||
"Filter by file count"
|
||||
file_count: IntCriterionInput
|
||||
"Filter by rating"
|
||||
rating: IntCriterionInput
|
||||
@deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: IntCriterionInput
|
||||
"Filter by date"
|
||||
@@ -478,6 +625,8 @@ input ImageFilterType {
|
||||
o_counter: IntCriterionInput
|
||||
"Filter by resolution"
|
||||
resolution: ResolutionCriterionInput
|
||||
"Filter by orientation"
|
||||
orientation: OrientationCriterionInput
|
||||
"Filter to only include images missing this property"
|
||||
is_missing: String
|
||||
"Filter to only include images with this studio"
|
||||
@@ -494,12 +643,27 @@ input ImageFilterType {
|
||||
performer_count: IntCriterionInput
|
||||
"Filter images that have performers that have been favorited"
|
||||
performer_favorite: Boolean
|
||||
"Filter images by performer age at time of image"
|
||||
performer_age: IntCriterionInput
|
||||
"Filter to only include images with these galleries"
|
||||
galleries: MultiCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
"Filter by studio code"
|
||||
code: StringCriterionInput
|
||||
"Filter by photographer"
|
||||
photographer: StringCriterionInput
|
||||
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by related performers that meet this criteria"
|
||||
performers_filter: PerformerFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
"Filter by related tags that meet this criteria"
|
||||
tags_filter: TagFilterType
|
||||
}
|
||||
|
||||
enum CriterionModifier {
|
||||
@@ -554,6 +718,7 @@ input MultiCriterionInput {
|
||||
|
||||
input GenderCriterionInput {
|
||||
value: GenderEnum
|
||||
value_list: [GenderEnum!]
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
@@ -594,6 +759,7 @@ enum FilterMode {
|
||||
GALLERIES
|
||||
SCENE_MARKERS
|
||||
MOVIES
|
||||
GROUPS
|
||||
TAGS
|
||||
IMAGES
|
||||
}
|
||||
@@ -604,6 +770,13 @@ type SavedFilter {
|
||||
name: String!
|
||||
"JSON-encoded filter string"
|
||||
filter: String!
|
||||
@deprecated(reason: "use find_filter and object_filter instead")
|
||||
find_filter: SavedFindFilterType
|
||||
# maps to any of the AnyFilterInput types
|
||||
# using a generic Map instead of creating and maintaining match types for inputs
|
||||
object_filter: Map
|
||||
# generic map for ui options
|
||||
ui_options: Map
|
||||
}
|
||||
|
||||
input SaveFilterInput {
|
||||
@@ -611,8 +784,10 @@ input SaveFilterInput {
|
||||
id: ID
|
||||
mode: FilterMode!
|
||||
name: String!
|
||||
"JSON-encoded filter string"
|
||||
filter: String!
|
||||
find_filter: FindFilterType
|
||||
object_filter: Map
|
||||
# generic map for ui options
|
||||
ui_options: Map
|
||||
}
|
||||
|
||||
input DestroyFilterInput {
|
||||
@@ -621,6 +796,9 @@ input DestroyFilterInput {
|
||||
|
||||
input SetDefaultFilterInput {
|
||||
mode: FilterMode!
|
||||
"JSON-encoded filter string - null to clear"
|
||||
filter: String
|
||||
"null to clear"
|
||||
find_filter: FindFilterType
|
||||
object_filter: Map
|
||||
# generic map for ui options
|
||||
ui_options: Map
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
type GalleryPathsType {
|
||||
cover: String!
|
||||
preview: String! # Resolver
|
||||
}
|
||||
|
||||
"Gallery type"
|
||||
type Gallery {
|
||||
id: ID!
|
||||
checksum: String! @deprecated(reason: "Use files.fingerprints")
|
||||
path: String @deprecated(reason: "Use files.path")
|
||||
title: String
|
||||
url: String
|
||||
code: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]!
|
||||
date: String
|
||||
details: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
photographer: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean!
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
file_mod_time: Time @deprecated(reason: "Use files.mod_time")
|
||||
|
||||
files: [GalleryFile!]!
|
||||
folder: Folder
|
||||
@@ -26,18 +29,20 @@ type Gallery {
|
||||
tags: [Tag!]!
|
||||
performers: [Performer!]!
|
||||
|
||||
"The images in the gallery"
|
||||
images: [Image!]! @deprecated(reason: "Use findImages")
|
||||
cover: Image
|
||||
|
||||
paths: GalleryPathsType! # Resolver
|
||||
image(index: Int!): Image!
|
||||
}
|
||||
|
||||
input GalleryCreateInput {
|
||||
title: String!
|
||||
url: String
|
||||
code: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
details: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
photographer: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
@@ -51,11 +56,12 @@ input GalleryUpdateInput {
|
||||
clientMutationId: String
|
||||
id: ID!
|
||||
title: String
|
||||
url: String
|
||||
code: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
details: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
photographer: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
@@ -70,11 +76,12 @@ input GalleryUpdateInput {
|
||||
input BulkGalleryUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
url: String
|
||||
code: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: BulkUpdateStrings
|
||||
date: String
|
||||
details: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
photographer: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
@@ -109,3 +116,12 @@ input GalleryRemoveInput {
|
||||
gallery_id: ID!
|
||||
image_ids: [ID!]!
|
||||
}
|
||||
|
||||
input GallerySetCoverInput {
|
||||
gallery_id: ID!
|
||||
cover_image_id: ID!
|
||||
}
|
||||
|
||||
input GalleryResetCoverInput {
|
||||
gallery_id: ID!
|
||||
}
|
||||
|
||||
137
graphql/schema/types/group.graphql
Normal file
137
graphql/schema/types/group.graphql
Normal file
@@ -0,0 +1,137 @@
|
||||
"GroupDescription represents a relationship to a group with a description of the relationship"
|
||||
type GroupDescription {
|
||||
group: Group!
|
||||
description: String
|
||||
}
|
||||
|
||||
type Group {
|
||||
id: ID!
|
||||
name: String!
|
||||
aliases: String
|
||||
"Duration in seconds"
|
||||
duration: Int
|
||||
date: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio: Studio
|
||||
director: String
|
||||
synopsis: String
|
||||
urls: [String!]!
|
||||
tags: [Tag!]!
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
|
||||
containing_groups: [GroupDescription!]!
|
||||
sub_groups: [GroupDescription!]!
|
||||
|
||||
front_image_path: String # Resolver
|
||||
back_image_path: String # Resolver
|
||||
scene_count(depth: Int): Int! # Resolver
|
||||
sub_group_count(depth: Int): Int! # Resolver
|
||||
scenes: [Scene!]!
|
||||
}
|
||||
|
||||
input GroupDescriptionInput {
|
||||
group_id: ID!
|
||||
description: String
|
||||
}
|
||||
|
||||
input GroupCreateInput {
|
||||
name: String!
|
||||
aliases: String
|
||||
"Duration in seconds"
|
||||
duration: Int
|
||||
date: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
urls: [String!]
|
||||
tag_ids: [ID!]
|
||||
|
||||
containing_groups: [GroupDescriptionInput!]
|
||||
sub_groups: [GroupDescriptionInput!]
|
||||
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input GroupUpdateInput {
|
||||
id: ID!
|
||||
name: String
|
||||
aliases: String
|
||||
duration: Int
|
||||
date: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
urls: [String!]
|
||||
tag_ids: [ID!]
|
||||
|
||||
containing_groups: [GroupDescriptionInput!]
|
||||
sub_groups: [GroupDescriptionInput!]
|
||||
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input BulkUpdateGroupDescriptionsInput {
|
||||
groups: [GroupDescriptionInput!]!
|
||||
mode: BulkUpdateIdMode!
|
||||
}
|
||||
|
||||
input BulkGroupUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
urls: BulkUpdateStrings
|
||||
tag_ids: BulkUpdateIds
|
||||
|
||||
containing_groups: BulkUpdateGroupDescriptionsInput
|
||||
sub_groups: BulkUpdateGroupDescriptionsInput
|
||||
}
|
||||
|
||||
input GroupDestroyInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input ReorderSubGroupsInput {
|
||||
"ID of the group to reorder sub groups for"
|
||||
group_id: ID!
|
||||
"""
|
||||
IDs of the sub groups to reorder. These must be a subset of the current sub groups.
|
||||
Sub groups will be inserted in this order at the insert_index
|
||||
"""
|
||||
sub_group_ids: [ID!]!
|
||||
"The sub-group ID at which to insert the sub groups"
|
||||
insert_at_id: ID!
|
||||
"If true, the sub groups will be inserted after the insert_index, otherwise they will be inserted before"
|
||||
insert_after: Boolean
|
||||
}
|
||||
|
||||
type FindGroupsResultType {
|
||||
count: Int!
|
||||
groups: [Group!]!
|
||||
}
|
||||
|
||||
input GroupSubGroupAddInput {
|
||||
containing_group_id: ID!
|
||||
sub_groups: [GroupDescriptionInput!]!
|
||||
"The index at which to insert the sub groups. If not provided, the sub groups will be appended to the end"
|
||||
insert_index: Int
|
||||
}
|
||||
|
||||
input GroupSubGroupRemoveInput {
|
||||
containing_group_id: ID!
|
||||
sub_group_ids: [ID!]!
|
||||
}
|
||||
@@ -1,22 +1,19 @@
|
||||
type Image {
|
||||
id: ID!
|
||||
checksum: String @deprecated(reason: "Use files.fingerprints")
|
||||
title: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
code: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]!
|
||||
date: String
|
||||
details: String
|
||||
photographer: String
|
||||
o_counter: Int
|
||||
organized: Boolean!
|
||||
path: String! @deprecated(reason: "Use files.path")
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
|
||||
file_mod_time: Time @deprecated(reason: "Use files.mod_time")
|
||||
|
||||
file: ImageFileType! @deprecated(reason: "Use visual_files")
|
||||
files: [ImageFile!]! @deprecated(reason: "Use visual_files")
|
||||
visual_files: [VisualFile!]!
|
||||
paths: ImagePathsType! # Resolver
|
||||
@@ -43,13 +40,15 @@ input ImageUpdateInput {
|
||||
clientMutationId: String
|
||||
id: ID!
|
||||
title: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
code: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
details: String
|
||||
photographer: String
|
||||
|
||||
studio_id: ID
|
||||
performer_ids: [ID!]
|
||||
@@ -63,13 +62,15 @@ input BulkImageUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
title: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
code: String
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: BulkUpdateStrings
|
||||
date: String
|
||||
details: String
|
||||
photographer: String
|
||||
|
||||
studio_id: ID
|
||||
performer_ids: BulkUpdateIds
|
||||
|
||||
@@ -4,6 +4,7 @@ enum JobStatus {
|
||||
FINISHED
|
||||
STOPPING
|
||||
CANCELLED
|
||||
FAILED
|
||||
}
|
||||
|
||||
type Job {
|
||||
@@ -15,6 +16,7 @@ type Job {
|
||||
startTime: Time
|
||||
endTime: Time
|
||||
addTime: Time!
|
||||
error: String
|
||||
}
|
||||
|
||||
input FindJobInput {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
"Log entries"
|
||||
scalar Time
|
||||
|
||||
enum LogLevel {
|
||||
Trace
|
||||
Debug
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
scalar Upload
|
||||
|
||||
input GenerateMetadataInput {
|
||||
covers: Boolean
|
||||
sprites: Boolean
|
||||
@@ -14,6 +12,7 @@ input GenerateMetadataInput {
|
||||
forceTranscodes: Boolean
|
||||
phashes: Boolean
|
||||
interactiveHeatmapsSpeeds: Boolean
|
||||
imageThumbnails: Boolean
|
||||
clipPreviews: Boolean
|
||||
|
||||
"scene ids to generate for"
|
||||
@@ -50,6 +49,7 @@ type GenerateMetadataOptions {
|
||||
transcodes: Boolean
|
||||
phashes: Boolean
|
||||
interactiveHeatmapsSpeeds: Boolean
|
||||
imageThumbnails: Boolean
|
||||
clipPreviews: Boolean
|
||||
}
|
||||
|
||||
@@ -75,19 +75,8 @@ input ScanMetaDataFilterInput {
|
||||
input ScanMetadataInput {
|
||||
paths: [String!]
|
||||
|
||||
# useFileMetadata is deprecated with the new file management system
|
||||
# if this functionality is desired, then we can make a built in scraper instead.
|
||||
|
||||
"Set name, date, details from metadata (if present)"
|
||||
useFileMetadata: Boolean @deprecated(reason: "Not implemented")
|
||||
|
||||
# stripFileExtension is deprecated since we no longer set the title from the
|
||||
# filename - it is automatically returned if the object has no title. If this
|
||||
# functionality is desired, then we could make this an option to not include
|
||||
# the extension in the auto-generated title.
|
||||
|
||||
"Strip file extension from title"
|
||||
stripFileExtension: Boolean @deprecated(reason: "Not implemented")
|
||||
"Forces a rescan on files even if modification time is unchanged"
|
||||
rescan: Boolean
|
||||
"Generate covers during scan"
|
||||
scanGenerateCovers: Boolean
|
||||
"Generate previews during scan"
|
||||
@@ -108,10 +97,8 @@ input ScanMetadataInput {
|
||||
}
|
||||
|
||||
type ScanMetadataOptions {
|
||||
"Set name, date, details from metadata (if present)"
|
||||
useFileMetadata: Boolean! @deprecated(reason: "Not implemented")
|
||||
"Strip file extension from title"
|
||||
stripFileExtension: Boolean! @deprecated(reason: "Not implemented")
|
||||
"Forces a rescan on files even if modification time is unchanged"
|
||||
rescan: Boolean!
|
||||
"Generate covers during scan"
|
||||
scanGenerateCovers: Boolean!
|
||||
"Generate previews during scan"
|
||||
@@ -135,6 +122,26 @@ input CleanMetadataInput {
|
||||
dryRun: Boolean!
|
||||
}
|
||||
|
||||
input CleanGeneratedInput {
|
||||
"Clean blob files without blob entries"
|
||||
blobFiles: Boolean
|
||||
"Clean sprite and vtt files without scene entries"
|
||||
sprites: Boolean
|
||||
"Clean preview files without scene entries"
|
||||
screenshots: Boolean
|
||||
"Clean scene transcodes without scene entries"
|
||||
transcodes: Boolean
|
||||
|
||||
"Clean marker files without marker entries"
|
||||
markers: Boolean
|
||||
|
||||
"Clean image thumbnails/clips without image entries"
|
||||
imageThumbnails: Boolean
|
||||
|
||||
"Do a dry run. Don't delete any files"
|
||||
dryRun: Boolean
|
||||
}
|
||||
|
||||
input AutoTagMetadataInput {
|
||||
"Paths to tag, null for all files"
|
||||
paths: [String!]
|
||||
@@ -277,7 +284,8 @@ input ExportObjectsInput {
|
||||
studios: ExportObjectTypeInput
|
||||
performers: ExportObjectTypeInput
|
||||
tags: ExportObjectTypeInput
|
||||
movies: ExportObjectTypeInput
|
||||
groups: ExportObjectTypeInput
|
||||
movies: ExportObjectTypeInput @deprecated(reason: "Use groups instead")
|
||||
galleries: ExportObjectTypeInput
|
||||
includeDependencies: Boolean
|
||||
}
|
||||
@@ -320,6 +328,11 @@ type SystemStatus {
|
||||
configPath: String
|
||||
appSchema: Int!
|
||||
status: SystemStatusEnum!
|
||||
os: String!
|
||||
workingDir: String!
|
||||
homeDir: String!
|
||||
ffmpegPath: String
|
||||
ffprobePath: String
|
||||
}
|
||||
|
||||
input MigrateInput {
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
type Movie {
|
||||
id: ID!
|
||||
name: String!
|
||||
checksum: String! @deprecated(reason: "MD5 hash of name, use name directly")
|
||||
aliases: String
|
||||
"Duration in seconds"
|
||||
duration: Int
|
||||
date: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio: Studio
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]!
|
||||
tags: [Tag!]!
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
|
||||
front_image_path: String # Resolver
|
||||
back_image_path: String # Resolver
|
||||
scene_count: Int! # Resolver
|
||||
scene_count(depth: Int): Int! # Resolver
|
||||
scenes: [Scene!]!
|
||||
}
|
||||
|
||||
@@ -29,14 +28,14 @@ input MovieCreateInput {
|
||||
"Duration in seconds"
|
||||
duration: Int
|
||||
date: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
@@ -49,14 +48,14 @@ input MovieUpdateInput {
|
||||
aliases: String
|
||||
duration: Int
|
||||
date: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
synopsis: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
@@ -66,12 +65,12 @@ input MovieUpdateInput {
|
||||
input BulkMovieUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
studio_id: ID
|
||||
director: String
|
||||
urls: BulkUpdateStrings
|
||||
tag_ids: BulkUpdateIds
|
||||
}
|
||||
|
||||
input MovieDestroyInput {
|
||||
|
||||
36
graphql/schema/types/package.graphql
Normal file
36
graphql/schema/types/package.graphql
Normal file
@@ -0,0 +1,36 @@
|
||||
enum PackageType {
|
||||
Scraper
|
||||
Plugin
|
||||
}
|
||||
|
||||
type Package {
|
||||
package_id: String!
|
||||
name: String!
|
||||
version: String
|
||||
date: Timestamp
|
||||
requires: [Package!]!
|
||||
|
||||
sourceURL: String!
|
||||
|
||||
"The version of this package currently available from the remote source"
|
||||
source_package: Package
|
||||
|
||||
metadata: Map!
|
||||
}
|
||||
|
||||
input PackageSpecInput {
|
||||
id: String!
|
||||
sourceURL: String!
|
||||
}
|
||||
|
||||
type PackageSource {
|
||||
name: String
|
||||
url: String!
|
||||
local_path: String
|
||||
}
|
||||
|
||||
input PackageSourceInput {
|
||||
name: String
|
||||
url: String!
|
||||
local_path: String
|
||||
}
|
||||
@@ -14,18 +14,17 @@ enum CircumisedEnum {
|
||||
|
||||
type Performer {
|
||||
id: ID!
|
||||
checksum: String @deprecated(reason: "Not used")
|
||||
name: String!
|
||||
disambiguation: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
gender: GenderEnum
|
||||
twitter: String
|
||||
instagram: String
|
||||
twitter: String @deprecated(reason: "Use urls")
|
||||
instagram: String @deprecated(reason: "Use urls")
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
@@ -34,7 +33,6 @@ type Performer {
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: [String!]!
|
||||
favorite: Boolean!
|
||||
tags: [Tag!]!
|
||||
@@ -44,13 +42,12 @@ type Performer {
|
||||
scene_count: Int! # Resolver
|
||||
image_count: Int! # Resolver
|
||||
gallery_count: Int! # Resolver
|
||||
movie_count: Int! # Resolver
|
||||
group_count: Int! # Resolver
|
||||
movie_count: Int! @deprecated(reason: "use group_count instead") # Resolver
|
||||
performer_count: Int! # Resolver
|
||||
o_counter: Int # Resolver
|
||||
scenes: [Scene!]!
|
||||
stash_ids: [StashID!]!
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
@@ -59,20 +56,20 @@ type Performer {
|
||||
weight: Int
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
movies: [Movie!]!
|
||||
groups: [Group!]!
|
||||
movies: [Movie!]! @deprecated(reason: "use groups instead")
|
||||
}
|
||||
|
||||
input PerformerCreateInput {
|
||||
name: String!
|
||||
disambiguation: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
# height must be parsable into an integer
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
@@ -81,17 +78,14 @@ input PerformerCreateInput {
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: [String!]
|
||||
twitter: String
|
||||
instagram: String
|
||||
twitter: String @deprecated(reason: "Use urls")
|
||||
instagram: String @deprecated(reason: "Use urls")
|
||||
favorite: Boolean
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
@@ -105,14 +99,13 @@ input PerformerUpdateInput {
|
||||
id: ID!
|
||||
name: String
|
||||
disambiguation: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
# height must be parsable into an integer
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
@@ -121,17 +114,14 @@ input PerformerUpdateInput {
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: [String!]
|
||||
twitter: String
|
||||
instagram: String
|
||||
twitter: String @deprecated(reason: "Use urls")
|
||||
instagram: String @deprecated(reason: "Use urls")
|
||||
favorite: Boolean
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
@@ -150,14 +140,13 @@ input BulkPerformerUpdateInput {
|
||||
clientMutationId: String
|
||||
ids: [ID!]
|
||||
disambiguation: String
|
||||
url: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: BulkUpdateStrings
|
||||
gender: GenderEnum
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
eye_color: String
|
||||
# height must be parsable into an integer
|
||||
height: String @deprecated(reason: "Use height_cm")
|
||||
height_cm: Int
|
||||
measurements: String
|
||||
fake_tits: String
|
||||
@@ -166,14 +155,11 @@ input BulkPerformerUpdateInput {
|
||||
career_length: String
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String @deprecated(reason: "Use alias_list")
|
||||
alias_list: BulkUpdateStrings
|
||||
twitter: String
|
||||
instagram: String
|
||||
twitter: String @deprecated(reason: "Use urls")
|
||||
instagram: String @deprecated(reason: "Use urls")
|
||||
favorite: Boolean
|
||||
tag_ids: BulkUpdateIds
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
details: String
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
type PluginPaths {
|
||||
# path to javascript files
|
||||
javascript: [String!]
|
||||
# path to css files
|
||||
css: [String!]
|
||||
}
|
||||
|
||||
type Plugin {
|
||||
id: ID!
|
||||
name: String!
|
||||
@@ -5,8 +12,19 @@ type Plugin {
|
||||
url: String
|
||||
version: String
|
||||
|
||||
enabled: Boolean!
|
||||
|
||||
tasks: [PluginTask!]
|
||||
hooks: [PluginHook!]
|
||||
settings: [PluginSetting!]
|
||||
|
||||
"""
|
||||
Plugin IDs of plugins that this plugin depends on.
|
||||
Applies only for UI plugins to indicate css/javascript load order.
|
||||
"""
|
||||
requires: [ID!]
|
||||
|
||||
paths: PluginPaths!
|
||||
}
|
||||
|
||||
type PluginTask {
|
||||
@@ -40,3 +58,16 @@ input PluginValueInput {
|
||||
o: [PluginArgInput!]
|
||||
a: [PluginValueInput!]
|
||||
}
|
||||
|
||||
enum PluginSettingTypeEnum {
|
||||
STRING
|
||||
NUMBER
|
||||
BOOLEAN
|
||||
}
|
||||
|
||||
type PluginSetting {
|
||||
name: String!
|
||||
display_name: String
|
||||
description: String
|
||||
type: PluginSettingTypeEnum!
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"An RFC3339 timestamp"
|
||||
scalar Time
|
||||
|
||||
"""
|
||||
Timestamp is a point in time. It is always output as RFC3339-compatible time points.
|
||||
It can be input as a RFC3339 string, or as "<4h" for "4 hours in the past" or ">5m"
|
||||
@@ -5,9 +8,18 @@ for "5 minutes in the future"
|
||||
"""
|
||||
scalar Timestamp
|
||||
|
||||
# generic JSON object
|
||||
"A String -> Any map"
|
||||
scalar Map
|
||||
|
||||
"A String -> Boolean map"
|
||||
scalar BoolMap
|
||||
|
||||
"A plugin ID -> Map (String -> Any map) map"
|
||||
scalar PluginConfigMap
|
||||
|
||||
scalar Any
|
||||
|
||||
scalar Int64
|
||||
|
||||
"A multipart file upload"
|
||||
scalar Upload
|
||||
|
||||
@@ -15,7 +15,6 @@ type ScenePathsType {
|
||||
stream: String # Resolver
|
||||
webp: String # Resolver
|
||||
vtt: String # Resolver
|
||||
chapters_vtt: String @deprecated
|
||||
sprite: String # Resolver
|
||||
funscript: String # Resolver
|
||||
interactive_heatmap: String # Resolver
|
||||
@@ -27,6 +26,11 @@ type SceneMovie {
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
type SceneGroup {
|
||||
group: Group!
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
type VideoCaption {
|
||||
language_code: String!
|
||||
caption_type: String!
|
||||
@@ -34,29 +38,22 @@ type VideoCaption {
|
||||
|
||||
type Scene {
|
||||
id: ID!
|
||||
checksum: String @deprecated(reason: "Use files.fingerprints")
|
||||
oshash: String @deprecated(reason: "Use files.fingerprints")
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
director: String
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
urls: [String!]!
|
||||
date: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean!
|
||||
o_counter: Int
|
||||
path: String! @deprecated(reason: "Use files.path")
|
||||
phash: String @deprecated(reason: "Use files.fingerprints")
|
||||
interactive: Boolean!
|
||||
interactive_speed: Int
|
||||
captions: [VideoCaption!]
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
file_mod_time: Time
|
||||
"The last time play count was updated"
|
||||
last_played_at: Time
|
||||
"The time index a scene was left at"
|
||||
@@ -66,13 +63,18 @@ type Scene {
|
||||
"The number ot times a scene has been played"
|
||||
play_count: Int
|
||||
|
||||
file: SceneFileType! @deprecated(reason: "Use files")
|
||||
"Times a scene was played"
|
||||
play_history: [Time!]!
|
||||
"Times the o counter was incremented"
|
||||
o_history: [Time!]!
|
||||
|
||||
files: [VideoFile!]!
|
||||
paths: ScenePathsType! # Resolver
|
||||
scene_markers: [SceneMarker!]!
|
||||
galleries: [Gallery!]!
|
||||
studio: Studio
|
||||
movies: [SceneMovie!]!
|
||||
groups: [SceneGroup!]!
|
||||
movies: [SceneMovie!]! @deprecated(reason: "Use groups")
|
||||
tags: [Tag!]!
|
||||
performers: [Performer!]!
|
||||
stash_ids: [StashID!]!
|
||||
@@ -86,6 +88,11 @@ input SceneMovieInput {
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
input SceneGroupInput {
|
||||
group_id: ID!
|
||||
scene_index: Int
|
||||
}
|
||||
|
||||
input SceneCreateInput {
|
||||
title: String
|
||||
code: String
|
||||
@@ -94,15 +101,14 @@ input SceneCreateInput {
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
studio_id: ID
|
||||
gallery_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
movies: [SceneMovieInput!]
|
||||
groups: [SceneGroupInput!]
|
||||
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
cover_image: String
|
||||
@@ -126,16 +132,16 @@ input SceneUpdateInput {
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
o_counter: Int
|
||||
@deprecated(reason: "Unsupported - Use sceneIncrementO/sceneDecrementO")
|
||||
organized: Boolean
|
||||
studio_id: ID
|
||||
gallery_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
movies: [SceneMovieInput!]
|
||||
groups: [SceneGroupInput!]
|
||||
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
|
||||
tag_ids: [ID!]
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
cover_image: String
|
||||
@@ -147,6 +153,9 @@ input SceneUpdateInput {
|
||||
play_duration: Float
|
||||
"The number ot times a scene has been played"
|
||||
play_count: Int
|
||||
@deprecated(
|
||||
reason: "Unsupported - Use sceneIncrementPlayCount/sceneDecrementPlayCount"
|
||||
)
|
||||
|
||||
primary_file_id: ID
|
||||
}
|
||||
@@ -172,8 +181,6 @@ input BulkSceneUpdateInput {
|
||||
url: String @deprecated(reason: "Use urls")
|
||||
urls: BulkUpdateStrings
|
||||
date: String
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
organized: Boolean
|
||||
@@ -181,7 +188,8 @@ input BulkSceneUpdateInput {
|
||||
gallery_ids: BulkUpdateIds
|
||||
performer_ids: BulkUpdateIds
|
||||
tag_ids: BulkUpdateIds
|
||||
movie_ids: BulkUpdateIds
|
||||
group_ids: BulkUpdateIds
|
||||
movie_ids: BulkUpdateIds @deprecated(reason: "Use group_ids")
|
||||
}
|
||||
|
||||
input SceneDestroyInput {
|
||||
@@ -266,4 +274,13 @@ input SceneMergeInput {
|
||||
destination: ID!
|
||||
# values defined here will override values in the destination
|
||||
values: SceneUpdateInput
|
||||
|
||||
# if true, the source history will be combined with the destination
|
||||
play_history: Boolean
|
||||
o_history: Boolean
|
||||
}
|
||||
|
||||
type HistoryMutationResult {
|
||||
count: Int!
|
||||
history: [Time!]!
|
||||
}
|
||||
|
||||
65
graphql/schema/types/scraped-group.graphql
Normal file
65
graphql/schema/types/scraped-group.graphql
Normal file
@@ -0,0 +1,65 @@
|
||||
"A movie from a scraping operation..."
|
||||
type ScrapedMovie {
|
||||
stored_id: ID
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
synopsis: String
|
||||
studio: ScrapedStudio
|
||||
tags: [ScrapedTag!]
|
||||
|
||||
"This should be a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a base64 encoded data URL"
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input ScrapedMovieInput {
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
synopsis: String
|
||||
# not including tags for the input
|
||||
}
|
||||
|
||||
"A group from a scraping operation..."
|
||||
type ScrapedGroup {
|
||||
stored_id: ID
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
urls: [String!]
|
||||
synopsis: String
|
||||
studio: ScrapedStudio
|
||||
tags: [ScrapedTag!]
|
||||
|
||||
"This should be a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a base64 encoded data URL"
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input ScrapedGroupInput {
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
urls: [String!]
|
||||
synopsis: String
|
||||
# not including tags for the input
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
"A movie from a scraping operation..."
|
||||
type ScrapedMovie {
|
||||
stored_id: ID
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
url: String
|
||||
synopsis: String
|
||||
studio: ScrapedStudio
|
||||
|
||||
"This should be a base64 encoded data URL"
|
||||
front_image: String
|
||||
"This should be a base64 encoded data URL"
|
||||
back_image: String
|
||||
}
|
||||
|
||||
input ScrapedMovieInput {
|
||||
name: String
|
||||
aliases: String
|
||||
duration: String
|
||||
date: String
|
||||
rating: String
|
||||
director: String
|
||||
url: String
|
||||
synopsis: String
|
||||
}
|
||||
@@ -5,9 +5,10 @@ type ScrapedPerformer {
|
||||
name: String
|
||||
disambiguation: String
|
||||
gender: String
|
||||
url: String
|
||||
twitter: String
|
||||
instagram: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
twitter: String @deprecated(reason: "use urls")
|
||||
instagram: String @deprecated(reason: "use urls")
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
@@ -40,9 +41,10 @@ input ScrapedPerformerInput {
|
||||
name: String
|
||||
disambiguation: String
|
||||
gender: String
|
||||
url: String
|
||||
twitter: String
|
||||
instagram: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
twitter: String @deprecated(reason: "use urls")
|
||||
instagram: String @deprecated(reason: "use urls")
|
||||
birthdate: String
|
||||
ethnicity: String
|
||||
country: String
|
||||
|
||||
@@ -11,6 +11,7 @@ enum ScrapeType {
|
||||
enum ScrapeContentType {
|
||||
GALLERY
|
||||
MOVIE
|
||||
GROUP
|
||||
PERFORMER
|
||||
SCENE
|
||||
}
|
||||
@@ -22,6 +23,7 @@ union ScrapedContent =
|
||||
| ScrapedScene
|
||||
| ScrapedGallery
|
||||
| ScrapedMovie
|
||||
| ScrapedGroup
|
||||
| ScrapedPerformer
|
||||
|
||||
type ScraperSpec {
|
||||
@@ -40,7 +42,9 @@ type Scraper {
|
||||
"Details for gallery scraper"
|
||||
gallery: ScraperSpec
|
||||
"Details for movie scraper"
|
||||
movie: ScraperSpec
|
||||
movie: ScraperSpec @deprecated(reason: "use group")
|
||||
"Details for group scraper"
|
||||
group: ScraperSpec
|
||||
}
|
||||
|
||||
type ScrapedStudio {
|
||||
@@ -76,7 +80,8 @@ type ScrapedScene {
|
||||
studio: ScrapedStudio
|
||||
tags: [ScrapedTag!]
|
||||
performers: [ScrapedPerformer!]
|
||||
movies: [ScrapedMovie!]
|
||||
movies: [ScrapedMovie!] @deprecated(reason: "use groups")
|
||||
groups: [ScrapedGroup!]
|
||||
|
||||
remote_site_id: String
|
||||
duration: Int
|
||||
@@ -99,8 +104,11 @@ input ScrapedSceneInput {
|
||||
|
||||
type ScrapedGallery {
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
url: String
|
||||
photographer: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
|
||||
studio: ScrapedStudio
|
||||
@@ -110,8 +118,11 @@ type ScrapedGallery {
|
||||
|
||||
input ScrapedGalleryInput {
|
||||
title: String
|
||||
code: String
|
||||
details: String
|
||||
url: String
|
||||
photographer: String
|
||||
url: String @deprecated(reason: "use urls")
|
||||
urls: [String!]
|
||||
date: String
|
||||
|
||||
# no studio, tags or performers
|
||||
@@ -122,7 +133,7 @@ input ScraperSourceInput {
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
"Stash-box endpoint"
|
||||
stash_box_endpoint: String
|
||||
"Scraper ID to scrape with. Should be unset if stash_box_index is set"
|
||||
"Scraper ID to scrape with. Should be unset if stash_box_endpoint/stash_box_index is set"
|
||||
scraper_id: ID
|
||||
}
|
||||
|
||||
@@ -131,7 +142,7 @@ type ScraperSource {
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
"Stash-box endpoint"
|
||||
stash_box_endpoint: String
|
||||
"Scraper ID to scrape with. Should be unset if stash_box_index is set"
|
||||
"Scraper ID to scrape with. Should be unset if stash_box_endpoint/stash_box_index is set"
|
||||
scraper_id: ID
|
||||
}
|
||||
|
||||
@@ -184,13 +195,24 @@ input ScrapeSingleMovieInput {
|
||||
query: String
|
||||
"Instructs to query by movie id"
|
||||
movie_id: ID
|
||||
"Instructs to query by gallery fragment"
|
||||
"Instructs to query by movie fragment"
|
||||
movie_input: ScrapedMovieInput
|
||||
}
|
||||
|
||||
input ScrapeSingleGroupInput {
|
||||
"Instructs to query by string"
|
||||
query: String
|
||||
"Instructs to query by group id"
|
||||
group_id: ID
|
||||
"Instructs to query by group fragment"
|
||||
group_input: ScrapedGroupInput
|
||||
}
|
||||
|
||||
input StashBoxSceneQueryInput {
|
||||
"Index of the configured stash-box instance to use"
|
||||
stash_box_index: Int!
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
"Endpoint of the stash-box instance to use"
|
||||
stash_box_endpoint: String
|
||||
"Instructs query by scene fingerprints"
|
||||
scene_ids: [ID!]
|
||||
"Query by query string"
|
||||
@@ -199,7 +221,9 @@ input StashBoxSceneQueryInput {
|
||||
|
||||
input StashBoxPerformerQueryInput {
|
||||
"Index of the configured stash-box instance to use"
|
||||
stash_box_index: Int!
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
"Endpoint of the stash-box instance to use"
|
||||
stash_box_endpoint: String
|
||||
"Instructs query by scene fingerprints"
|
||||
performer_ids: [ID!]
|
||||
"Query by query string"
|
||||
@@ -220,7 +244,9 @@ type StashBoxFingerprint {
|
||||
"If neither ids nor names are set, tag all items"
|
||||
input StashBoxBatchTagInput {
|
||||
"Stash endpoint to use for the tagging"
|
||||
endpoint: Int!
|
||||
endpoint: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
"Endpoint of the stash-box instance to use"
|
||||
stash_box_endpoint: String
|
||||
"Fields to exclude when executing the tagging"
|
||||
exclude_fields: [String!]
|
||||
"Refresh items already tagged by StashBox if true. Only tag items with no StashBox tagging if false"
|
||||
|
||||
@@ -22,10 +22,12 @@ input StashIDInput {
|
||||
|
||||
input StashBoxFingerprintSubmissionInput {
|
||||
scene_ids: [String!]!
|
||||
stash_box_index: Int!
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
stash_box_endpoint: String
|
||||
}
|
||||
|
||||
input StashBoxDraftSubmissionInput {
|
||||
id: String!
|
||||
stash_box_index: Int!
|
||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||
stash_box_endpoint: String
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ type StatsResultType {
|
||||
gallery_count: Int!
|
||||
performer_count: Int!
|
||||
studio_count: Int!
|
||||
movie_count: Int!
|
||||
group_count: Int!
|
||||
movie_count: Int! @deprecated(reason: "use group_count instead")
|
||||
tag_count: Int!
|
||||
total_o_count: Int!
|
||||
total_play_duration: Float!
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
type Studio {
|
||||
id: ID!
|
||||
name: String!
|
||||
checksum: String! @deprecated(reason: "MD5 hash of name, use name directly")
|
||||
url: String
|
||||
parent_studio: Studio
|
||||
child_studios: [Studio!]!
|
||||
aliases: [String!]!
|
||||
tags: [Tag!]!
|
||||
ignore_auto_tag: Boolean!
|
||||
|
||||
image_path: String # Resolver
|
||||
@@ -13,16 +13,17 @@ type Studio {
|
||||
image_count(depth: Int): Int! # Resolver
|
||||
gallery_count(depth: Int): Int! # Resolver
|
||||
performer_count(depth: Int): Int! # Resolver
|
||||
movie_count(depth: Int): Int! # Resolver
|
||||
group_count(depth: Int): Int! # Resolver
|
||||
movie_count(depth: Int): Int! @deprecated(reason: "use group_count instead") # Resolver
|
||||
stash_ids: [StashID!]!
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
favorite: Boolean!
|
||||
details: String
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
movies: [Movie!]!
|
||||
groups: [Group!]!
|
||||
movies: [Movie!]! @deprecated(reason: "use groups instead")
|
||||
}
|
||||
|
||||
input StudioCreateInput {
|
||||
@@ -32,12 +33,12 @@ input StudioCreateInput {
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
favorite: Boolean
|
||||
details: String
|
||||
aliases: [String!]
|
||||
tag_ids: [ID!]
|
||||
ignore_auto_tag: Boolean
|
||||
}
|
||||
|
||||
@@ -49,12 +50,12 @@ input StudioUpdateInput {
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
# rating expressed as 1-5
|
||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||
# rating expressed as 1-100
|
||||
rating100: Int
|
||||
favorite: Boolean
|
||||
details: String
|
||||
aliases: [String!]
|
||||
tag_ids: [ID!]
|
||||
ignore_auto_tag: Boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -6,15 +6,21 @@ type Tag {
|
||||
ignore_auto_tag: Boolean!
|
||||
created_at: Time!
|
||||
updated_at: Time!
|
||||
|
||||
favorite: Boolean!
|
||||
image_path: String # Resolver
|
||||
scene_count(depth: Int): Int! # Resolver
|
||||
scene_marker_count(depth: Int): Int! # Resolver
|
||||
image_count(depth: Int): Int! # Resolver
|
||||
gallery_count(depth: Int): Int! # Resolver
|
||||
performer_count(depth: Int): Int! # Resolver
|
||||
studio_count(depth: Int): Int! # Resolver
|
||||
group_count(depth: Int): Int! # Resolver
|
||||
movie_count(depth: Int): Int! @deprecated(reason: "use group_count instead") # Resolver
|
||||
parents: [Tag!]!
|
||||
children: [Tag!]!
|
||||
|
||||
parent_count: Int! # Resolver
|
||||
child_count: Int! # Resolver
|
||||
}
|
||||
|
||||
input TagCreateInput {
|
||||
@@ -22,7 +28,7 @@ input TagCreateInput {
|
||||
description: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
favorite: Boolean
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
|
||||
@@ -36,7 +42,7 @@ input TagUpdateInput {
|
||||
description: String
|
||||
aliases: [String!]
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
favorite: Boolean
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
image: String
|
||||
|
||||
@@ -57,3 +63,14 @@ input TagsMergeInput {
|
||||
source: [ID!]!
|
||||
destination: ID!
|
||||
}
|
||||
|
||||
input BulkTagUpdateInput {
|
||||
ids: [ID!]
|
||||
description: String
|
||||
aliases: BulkUpdateStrings
|
||||
ignore_auto_tag: Boolean
|
||||
favorite: Boolean
|
||||
|
||||
parent_ids: BulkUpdateIds
|
||||
child_ids: BulkUpdateIds
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func authenticateHandler() func(http.Handler) http.Handler {
|
||||
|
||||
userID, err := manager.GetInstance().SessionStore.Authenticate(w, r)
|
||||
if err != nil {
|
||||
if errors.Is(err, session.ErrUnauthorized) {
|
||||
if !errors.Is(err, session.ErrUnauthorized) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
38
internal/api/bool_map.go
Normal file
38
internal/api/bool_map.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
)
|
||||
|
||||
func MarshalBoolMap(val map[string]bool) graphql.Marshaler {
|
||||
return graphql.WriterFunc(func(w io.Writer) {
|
||||
err := json.NewEncoder(w).Encode(val)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func UnmarshalBoolMap(v interface{}) (map[string]bool, error) {
|
||||
m, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T is not a map", v)
|
||||
}
|
||||
|
||||
result := make(map[string]bool)
|
||||
for k, v := range m {
|
||||
key := k
|
||||
val, ok := v.(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("key %s (%T) is not a bool", k, v)
|
||||
}
|
||||
|
||||
result[key] = val
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
const updateInputField = "input"
|
||||
@@ -91,7 +93,7 @@ func (t changesetTranslator) getFields() []string {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t changesetTranslator) string(value *string, field string) string {
|
||||
func (t changesetTranslator) string(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -127,7 +129,7 @@ func (t changesetTranslator) optionalDate(value *string, field string) (models.O
|
||||
return models.NewOptionalDate(date), nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) datePtr(value *string, field string) (*models.Date, error) {
|
||||
func (t changesetTranslator) datePtr(value *string) (*models.Date, error) {
|
||||
if value == nil || *value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -139,7 +141,7 @@ func (t changesetTranslator) datePtr(value *string, field string) (*models.Date,
|
||||
return &date, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) intPtrFromString(value *string, field string) (*int, error) {
|
||||
func (t changesetTranslator) intPtrFromString(value *string) (*int, error) {
|
||||
if value == nil || *value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -151,40 +153,6 @@ func (t changesetTranslator) intPtrFromString(value *string, field string) (*int
|
||||
return &vv, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) ratingConversionInt(legacyValue *int, rating100Value *int) *int {
|
||||
const (
|
||||
legacyField = "rating"
|
||||
rating100Field = "rating100"
|
||||
)
|
||||
|
||||
legacyRating := t.optionalInt(legacyValue, legacyField)
|
||||
if legacyRating.Set && !(legacyRating.Null) {
|
||||
ret := int(models.Rating5To100(int(legacyRating.Value)))
|
||||
return &ret
|
||||
}
|
||||
|
||||
o := t.optionalInt(rating100Value, rating100Field)
|
||||
if o.Set && !(o.Null) {
|
||||
return &o.Value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) ratingConversionOptional(legacyValue *int, rating100Value *int) models.OptionalInt {
|
||||
const (
|
||||
legacyField = "rating"
|
||||
rating100Field = "rating100"
|
||||
)
|
||||
|
||||
legacyRating := t.optionalInt(legacyValue, legacyField)
|
||||
if legacyRating.Set && !(legacyRating.Null) {
|
||||
legacyRating.Value = int(models.Rating5To100(int(legacyRating.Value)))
|
||||
return legacyRating
|
||||
}
|
||||
return t.optionalInt(rating100Value, rating100Field)
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalInt(value *int, field string) models.OptionalInt {
|
||||
if !t.hasField(field) {
|
||||
return models.OptionalInt{}
|
||||
@@ -212,7 +180,7 @@ func (t changesetTranslator) optionalIntFromString(value *string, field string)
|
||||
return models.NewOptionalInt(vv), nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) bool(value *bool, field string) bool {
|
||||
func (t changesetTranslator) bool(value *bool) bool {
|
||||
if value == nil {
|
||||
return false
|
||||
}
|
||||
@@ -235,3 +203,295 @@ func (t changesetTranslator) optionalFloat64(value *float64, field string) model
|
||||
|
||||
return models.NewOptionalFloat64Ptr(value)
|
||||
}
|
||||
|
||||
func (t changesetTranslator) fileIDPtrFromString(value *string) (*models.FileID, error) {
|
||||
if value == nil || *value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
vv, err := strconv.Atoi(*value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting %v to int: %w", *value, err)
|
||||
}
|
||||
|
||||
id := models.FileID(vv)
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) fileIDSliceFromStringSlice(value []string) ([]models.FileID, error) {
|
||||
ints, err := stringslice.StringSliceToIntSlice(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileIDs := make([]models.FileID, len(ints))
|
||||
for i, v := range ints {
|
||||
fileIDs[i] = models.FileID(v)
|
||||
}
|
||||
|
||||
return fileIDs, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) relatedIds(value []string) (models.RelatedIDs, error) {
|
||||
ids, err := stringslice.StringSliceToIntSlice(value)
|
||||
if err != nil {
|
||||
return models.RelatedIDs{}, err
|
||||
}
|
||||
|
||||
return models.NewRelatedIDs(ids), nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateIds(value []string, field string) (*models.UpdateIDs, error) {
|
||||
if !t.hasField(field) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ids, err := stringslice.StringSliceToIntSlice(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UpdateIDs{
|
||||
IDs: ids,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateIdsBulk(value *BulkUpdateIds, field string) (*models.UpdateIDs, error) {
|
||||
if !t.hasField(field) || value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ids, err := stringslice.StringSliceToIntSlice(value.Ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting ids [%v]: %w", value.Ids, err)
|
||||
}
|
||||
|
||||
return &models.UpdateIDs{
|
||||
IDs: ids,
|
||||
Mode: value.Mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalURLs(value []string, legacyValue *string) *models.UpdateStrings {
|
||||
const (
|
||||
legacyField = "url"
|
||||
field = "urls"
|
||||
)
|
||||
|
||||
// prefer urls over url
|
||||
if t.hasField(field) {
|
||||
return t.updateStrings(value, field)
|
||||
} else if t.hasField(legacyField) {
|
||||
var valueSlice []string
|
||||
if legacyValue != nil {
|
||||
valueSlice = []string{*legacyValue}
|
||||
}
|
||||
return t.updateStrings(valueSlice, legacyField)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) optionalURLsBulk(value *BulkUpdateStrings, legacyValue *string) *models.UpdateStrings {
|
||||
const (
|
||||
legacyField = "url"
|
||||
field = "urls"
|
||||
)
|
||||
|
||||
// prefer urls over url
|
||||
if t.hasField("urls") {
|
||||
return t.updateStringsBulk(value, field)
|
||||
} else if t.hasField(legacyField) {
|
||||
var valueSlice []string
|
||||
if legacyValue != nil {
|
||||
valueSlice = []string{*legacyValue}
|
||||
}
|
||||
return t.updateStrings(valueSlice, legacyField)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateStrings(value []string, field string) *models.UpdateStrings {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.UpdateStrings{
|
||||
Values: value,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateStringsBulk(value *BulkUpdateStrings, field string) *models.UpdateStrings {
|
||||
if !t.hasField(field) || value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.UpdateStrings{
|
||||
Values: value.Values,
|
||||
Mode: value.Mode,
|
||||
}
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateStashIDs(value []models.StashID, field string) *models.UpdateStashIDs {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.UpdateStashIDs{
|
||||
StashIDs: value,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
}
|
||||
|
||||
func (t changesetTranslator) relatedGroupsFromMovies(value []models.SceneMovieInput) (models.RelatedGroups, error) {
|
||||
groupsScenes, err := models.GroupsScenesFromInput(value)
|
||||
if err != nil {
|
||||
return models.RelatedGroups{}, err
|
||||
}
|
||||
|
||||
return models.NewRelatedGroups(groupsScenes), nil
|
||||
}
|
||||
|
||||
func groupsScenesFromGroupInput(input []models.SceneGroupInput) ([]models.GroupsScenes, error) {
|
||||
ret := make([]models.GroupsScenes, len(input))
|
||||
|
||||
for i, v := range input {
|
||||
mID, err := strconv.Atoi(v.GroupID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid group ID: %s", v.GroupID)
|
||||
}
|
||||
|
||||
ret[i] = models.GroupsScenes{
|
||||
GroupID: mID,
|
||||
SceneIndex: v.SceneIndex,
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) relatedGroups(value []models.SceneGroupInput) (models.RelatedGroups, error) {
|
||||
groupsScenes, err := groupsScenesFromGroupInput(value)
|
||||
if err != nil {
|
||||
return models.RelatedGroups{}, err
|
||||
}
|
||||
|
||||
return models.NewRelatedGroups(groupsScenes), nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateGroupIDsFromMovies(value []models.SceneMovieInput, field string) (*models.UpdateGroupIDs, error) {
|
||||
if !t.hasField(field) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
groupsScenes, err := models.GroupsScenesFromInput(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UpdateGroupIDs{
|
||||
Groups: groupsScenes,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateGroupIDs(value []models.SceneGroupInput, field string) (*models.UpdateGroupIDs, error) {
|
||||
if !t.hasField(field) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
groupsScenes, err := groupsScenesFromGroupInput(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UpdateGroupIDs{
|
||||
Groups: groupsScenes,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateGroupIDsBulk(value *BulkUpdateIds, field string) (*models.UpdateGroupIDs, error) {
|
||||
if !t.hasField(field) || value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ids, err := stringslice.StringSliceToIntSlice(value.Ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting ids [%v]: %w", value.Ids, err)
|
||||
}
|
||||
|
||||
groups := make([]models.GroupsScenes, len(ids))
|
||||
for i, id := range ids {
|
||||
groups[i] = models.GroupsScenes{GroupID: id}
|
||||
}
|
||||
|
||||
return &models.UpdateGroupIDs{
|
||||
Groups: groups,
|
||||
Mode: value.Mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func groupsDescriptionsFromGroupInput(input []*GroupDescriptionInput) ([]models.GroupIDDescription, error) {
|
||||
ret := make([]models.GroupIDDescription, len(input))
|
||||
|
||||
for i, v := range input {
|
||||
gID, err := strconv.Atoi(v.GroupID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid group ID: %s", v.GroupID)
|
||||
}
|
||||
|
||||
ret[i] = models.GroupIDDescription{
|
||||
GroupID: gID,
|
||||
}
|
||||
if v.Description != nil {
|
||||
ret[i].Description = *v.Description
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) groupIDDescriptions(value []*GroupDescriptionInput) (models.RelatedGroupDescriptions, error) {
|
||||
groupsScenes, err := groupsDescriptionsFromGroupInput(value)
|
||||
if err != nil {
|
||||
return models.RelatedGroupDescriptions{}, err
|
||||
}
|
||||
|
||||
return models.NewRelatedGroupDescriptions(groupsScenes), nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateGroupIDDescriptions(value []*GroupDescriptionInput, field string) (*models.UpdateGroupDescriptions, error) {
|
||||
if !t.hasField(field) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
groupsScenes, err := groupsDescriptionsFromGroupInput(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UpdateGroupDescriptions{
|
||||
Groups: groupsScenes,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateGroupIDDescriptionsBulk(value *BulkUpdateGroupDescriptionsInput, field string) (*models.UpdateGroupDescriptions, error) {
|
||||
if !t.hasField(field) || value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
groups, err := groupsDescriptionsFromGroupInput(value.Groups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UpdateGroupDescriptions{
|
||||
Groups: groups,
|
||||
Mode: value.Mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ const defaultSHLength int = 8 // default length of SHA short hash returned by <g
|
||||
|
||||
var stashReleases = func() map[string]string {
|
||||
return map[string]string{
|
||||
"darwin/amd64": "stash-osx",
|
||||
"darwin/arm64": "stash-osx-applesilicon",
|
||||
"darwin/amd64": "stash-macos",
|
||||
"darwin/arm64": "stash-macos",
|
||||
"linux/amd64": "stash-linux",
|
||||
"windows/amd64": "stash-win.exe",
|
||||
"linux/arm": "stash-linux-arm32v6",
|
||||
|
||||
@@ -5,12 +5,13 @@ package api
|
||||
type key int
|
||||
|
||||
const (
|
||||
// galleryKey key = 0
|
||||
performerKey key = iota + 1
|
||||
galleryKey key = 0
|
||||
performerKey
|
||||
sceneKey
|
||||
studioKey
|
||||
movieKey
|
||||
groupKey
|
||||
tagKey
|
||||
downloadKey
|
||||
imageKey
|
||||
pluginKey
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/collate"
|
||||
)
|
||||
@@ -25,13 +26,27 @@ func (s dirLister) Bytes(i int) []byte {
|
||||
// listDir will return the contents of a given directory path as a string slice
|
||||
func listDir(col *collate.Collator, path string) ([]string, error) {
|
||||
var dirPaths []string
|
||||
dirPath := path
|
||||
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
path = filepath.Dir(path)
|
||||
files, err = os.ReadDir(path)
|
||||
dirPath = filepath.Dir(path)
|
||||
dirFiles, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return dirPaths, err
|
||||
}
|
||||
|
||||
// Filter dir contents by last path fragment if the dir isn't an exact match
|
||||
base := strings.ToLower(filepath.Base(path))
|
||||
if base != "." && base != string(filepath.Separator) {
|
||||
for _, file := range dirFiles {
|
||||
if strings.HasPrefix(strings.ToLower(file.Name()), base) {
|
||||
files = append(files, file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files = dirFiles
|
||||
}
|
||||
}
|
||||
|
||||
if col != nil {
|
||||
@@ -42,7 +57,7 @@ func listDir(col *collate.Collator, path string) ([]string, error) {
|
||||
if !file.IsDir() {
|
||||
continue
|
||||
}
|
||||
dirPaths = append(dirPaths, filepath.Join(path, file.Name()))
|
||||
dirPaths = append(dirPaths, filepath.Join(dirPath, file.Name()))
|
||||
}
|
||||
return dirPaths, nil
|
||||
}
|
||||
|
||||
2
internal/api/doc.go
Normal file
2
internal/api/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package api provides the HTTP and Graphql API for the application.
|
||||
package api
|
||||
@@ -1,12 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/hash"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
@@ -18,7 +19,7 @@ type imageBox struct {
|
||||
files []string
|
||||
}
|
||||
|
||||
var imageExtensions = []string{
|
||||
var imageBoxExts = []string{
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
@@ -42,7 +43,7 @@ func newImageBox(box fs.FS) (*imageBox, error) {
|
||||
}
|
||||
|
||||
baseName := strings.ToLower(d.Name())
|
||||
for _, ext := range imageExtensions {
|
||||
for _, ext := range imageBoxExts {
|
||||
if strings.HasSuffix(baseName, ext) {
|
||||
ret.files = append(ret.files, path)
|
||||
break
|
||||
@@ -55,65 +56,14 @@ func newImageBox(box fs.FS) (*imageBox, error) {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
var performerBox *imageBox
|
||||
var performerBoxMale *imageBox
|
||||
var performerBoxCustom *imageBox
|
||||
|
||||
func initialiseImages() {
|
||||
var err error
|
||||
performerBox, err = newImageBox(&static.Performer)
|
||||
if err != nil {
|
||||
logger.Warnf("error loading performer images: %v", err)
|
||||
}
|
||||
performerBoxMale, err = newImageBox(&static.PerformerMale)
|
||||
if err != nil {
|
||||
logger.Warnf("error loading male performer images: %v", err)
|
||||
}
|
||||
initialiseCustomImages()
|
||||
}
|
||||
|
||||
func initialiseCustomImages() {
|
||||
customPath := config.GetInstance().GetCustomPerformerImageLocation()
|
||||
if customPath != "" {
|
||||
logger.Debugf("Loading custom performer images from %s", customPath)
|
||||
// We need to set performerBoxCustom at runtime, as this is a custom path, and store it in a pointer.
|
||||
var err error
|
||||
performerBoxCustom, err = newImageBox(os.DirFS(customPath))
|
||||
if err != nil {
|
||||
logger.Warnf("error loading custom performer from %s: %v", customPath, err)
|
||||
}
|
||||
} else {
|
||||
performerBoxCustom = nil
|
||||
}
|
||||
}
|
||||
|
||||
func getRandomPerformerImageUsingName(name string, gender *models.GenderEnum, customPath string) ([]byte, error) {
|
||||
var box *imageBox
|
||||
|
||||
// If we have a custom path, we should return a new box in the given path.
|
||||
if performerBoxCustom != nil && len(performerBoxCustom.files) > 0 {
|
||||
box = performerBoxCustom
|
||||
func (box *imageBox) GetRandomImageByName(name string) ([]byte, error) {
|
||||
files := box.files
|
||||
if len(files) == 0 {
|
||||
return nil, errors.New("box is empty")
|
||||
}
|
||||
|
||||
var g models.GenderEnum
|
||||
if gender != nil {
|
||||
g = *gender
|
||||
}
|
||||
|
||||
if box == nil {
|
||||
switch g {
|
||||
case models.GenderEnumFemale, models.GenderEnumTransgenderFemale:
|
||||
box = performerBox
|
||||
case models.GenderEnumMale, models.GenderEnumTransgenderMale:
|
||||
box = performerBoxMale
|
||||
default:
|
||||
box = performerBox
|
||||
}
|
||||
}
|
||||
|
||||
imageFiles := box.files
|
||||
index := hash.IntFromString(name) % uint64(len(imageFiles))
|
||||
img, err := box.box.Open(imageFiles[index])
|
||||
index := hash.IntFromString(name) % uint64(len(files))
|
||||
img, err := box.box.Open(files[index])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -121,3 +71,64 @@ func getRandomPerformerImageUsingName(name string, gender *models.GenderEnum, cu
|
||||
|
||||
return io.ReadAll(img)
|
||||
}
|
||||
|
||||
var performerBox *imageBox
|
||||
var performerBoxMale *imageBox
|
||||
var performerBoxCustom *imageBox
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
performerBox, err = newImageBox(static.Sub(static.Performer))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("loading performer images: %v", err))
|
||||
}
|
||||
performerBoxMale, err = newImageBox(static.Sub(static.PerformerMale))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("loading male performer images: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func initCustomPerformerImages(customPath string) {
|
||||
if customPath != "" {
|
||||
logger.Debugf("Loading custom performer images from %s", customPath)
|
||||
var err error
|
||||
performerBoxCustom, err = newImageBox(os.DirFS(customPath))
|
||||
if err != nil {
|
||||
logger.Warnf("error loading custom performer images from %s: %v", customPath, err)
|
||||
}
|
||||
} else {
|
||||
performerBoxCustom = nil
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultPerformerImage(name string, gender *models.GenderEnum) []byte {
|
||||
// try the custom box first if we have one
|
||||
if performerBoxCustom != nil {
|
||||
ret, err := performerBoxCustom.GetRandomImageByName(name)
|
||||
if err == nil {
|
||||
return ret
|
||||
}
|
||||
logger.Warnf("error loading custom default performer image: %v", err)
|
||||
}
|
||||
|
||||
var g models.GenderEnum
|
||||
if gender != nil {
|
||||
g = *gender
|
||||
}
|
||||
|
||||
var box *imageBox
|
||||
switch g {
|
||||
case models.GenderEnumFemale, models.GenderEnumTransgenderFemale:
|
||||
box = performerBox
|
||||
case models.GenderEnumMale, models.GenderEnumTransgenderMale:
|
||||
box = performerBoxMale
|
||||
default:
|
||||
box = performerBox
|
||||
}
|
||||
|
||||
ret, err := box.GetRandomImageByName(name)
|
||||
if err != nil {
|
||||
logger.Warnf("error loading default performer image: %v", err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden SceneLoader int *github.com/stashapp/stash/pkg/models.Scene
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden GalleryLoader int *github.com/stashapp/stash/pkg/models.Gallery
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden ImageLoader int *github.com/stashapp/stash/pkg/models.Image
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden PerformerLoader int *github.com/stashapp/stash/pkg/models.Performer
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden StudioLoader int *github.com/stashapp/stash/pkg/models.Studio
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden TagLoader int *github.com/stashapp/stash/pkg/models.Tag
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden MovieLoader int *github.com/stashapp/stash/pkg/models.Movie
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden FileLoader github.com/stashapp/stash/pkg/file.ID github.com/stashapp/stash/pkg/file.File
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
|
||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
|
||||
// Package loaders contains the dataloaders used by the resolver in [api].
|
||||
// They are generated with `make generate-dataloaders`.
|
||||
// The dataloaders are used to batch requests to the database.
|
||||
|
||||
//go:generate go run github.com/vektah/dataloaden SceneLoader int *github.com/stashapp/stash/pkg/models.Scene
|
||||
//go:generate go run github.com/vektah/dataloaden GalleryLoader int *github.com/stashapp/stash/pkg/models.Gallery
|
||||
//go:generate go run github.com/vektah/dataloaden ImageLoader int *github.com/stashapp/stash/pkg/models.Image
|
||||
//go:generate go run github.com/vektah/dataloaden PerformerLoader int *github.com/stashapp/stash/pkg/models.Performer
|
||||
//go:generate go run github.com/vektah/dataloaden StudioLoader int *github.com/stashapp/stash/pkg/models.Studio
|
||||
//go:generate go run github.com/vektah/dataloaden TagLoader int *github.com/stashapp/stash/pkg/models.Tag
|
||||
//go:generate go run github.com/vektah/dataloaden GroupLoader int *github.com/stashapp/stash/pkg/models.Group
|
||||
//go:generate go run github.com/vektah/dataloaden FileLoader github.com/stashapp/stash/pkg/models.FileID github.com/stashapp/stash/pkg/models.File
|
||||
//go:generate go run github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden SceneOCountLoader int int
|
||||
//go:generate go run github.com/vektah/dataloaden ScenePlayCountLoader int int
|
||||
//go:generate go run github.com/vektah/dataloaden SceneOHistoryLoader int []time.Time
|
||||
//go:generate go run github.com/vektah/dataloaden ScenePlayHistoryLoader int []time.Time
|
||||
//go:generate go run github.com/vektah/dataloaden SceneLastPlayedLoader int *time.Time
|
||||
package loaders
|
||||
|
||||
import (
|
||||
@@ -17,10 +25,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
type contextKey struct{ name string }
|
||||
@@ -35,8 +40,14 @@ const (
|
||||
)
|
||||
|
||||
type Loaders struct {
|
||||
SceneByID *SceneLoader
|
||||
SceneFiles *SceneFileIDsLoader
|
||||
SceneByID *SceneLoader
|
||||
SceneFiles *SceneFileIDsLoader
|
||||
ScenePlayCount *ScenePlayCountLoader
|
||||
SceneOCount *SceneOCountLoader
|
||||
ScenePlayHistory *ScenePlayHistoryLoader
|
||||
SceneOHistory *SceneOHistoryLoader
|
||||
SceneLastPlayed *SceneLastPlayedLoader
|
||||
|
||||
ImageFiles *ImageFileIDsLoader
|
||||
GalleryFiles *GalleryFileIDsLoader
|
||||
|
||||
@@ -45,13 +56,12 @@ type Loaders struct {
|
||||
PerformerByID *PerformerLoader
|
||||
StudioByID *StudioLoader
|
||||
TagByID *TagLoader
|
||||
MovieByID *MovieLoader
|
||||
GroupByID *GroupLoader
|
||||
FileByID *FileLoader
|
||||
}
|
||||
|
||||
type Middleware struct {
|
||||
DatabaseProvider txn.DatabaseProvider
|
||||
Repository manager.Repository
|
||||
Repository models.Repository
|
||||
}
|
||||
|
||||
func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
@@ -88,10 +98,10 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchTags(ctx),
|
||||
},
|
||||
MovieByID: &MovieLoader{
|
||||
GroupByID: &GroupLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchMovies(ctx),
|
||||
fetch: m.fetchGroups(ctx),
|
||||
},
|
||||
FileByID: &FileLoader{
|
||||
wait: wait,
|
||||
@@ -113,6 +123,31 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGalleriesFileIDs(ctx),
|
||||
},
|
||||
ScenePlayCount: &ScenePlayCountLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesPlayCount(ctx),
|
||||
},
|
||||
SceneOCount: &SceneOCountLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesOCount(ctx),
|
||||
},
|
||||
ScenePlayHistory: &ScenePlayHistoryLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesPlayHistory(ctx),
|
||||
},
|
||||
SceneLastPlayed: &SceneLastPlayedLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesLastPlayed(ctx),
|
||||
},
|
||||
SceneOHistory: &SceneOHistoryLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchScenesOHistory(ctx),
|
||||
},
|
||||
}
|
||||
|
||||
newCtx := context.WithValue(r.Context(), loadersCtxKey, ldrs)
|
||||
@@ -132,13 +167,9 @@ func toErrorSlice(err error) []error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Middleware) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
return txn.WithDatabase(ctx, m.DatabaseProvider, fn)
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models.Scene, []error) {
|
||||
return func(keys []int) (ret []*models.Scene, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -149,7 +180,7 @@ func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models
|
||||
|
||||
func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models.Image, []error) {
|
||||
return func(keys []int) (ret []*models.Image, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Image.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -161,7 +192,7 @@ func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models
|
||||
|
||||
func (m Middleware) fetchGalleries(ctx context.Context) func(keys []int) ([]*models.Gallery, []error) {
|
||||
return func(keys []int) (ret []*models.Gallery, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Gallery.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -173,7 +204,7 @@ func (m Middleware) fetchGalleries(ctx context.Context) func(keys []int) ([]*mod
|
||||
|
||||
func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*models.Performer, []error) {
|
||||
return func(keys []int) (ret []*models.Performer, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Performer.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -185,7 +216,7 @@ func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*mo
|
||||
|
||||
func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*models.Studio, []error) {
|
||||
return func(keys []int) (ret []*models.Studio, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Studio.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -196,7 +227,7 @@ func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*model
|
||||
|
||||
func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.Tag, []error) {
|
||||
return func(keys []int) (ret []*models.Tag, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Tag.FindMany(ctx, keys)
|
||||
return err
|
||||
@@ -205,20 +236,20 @@ func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.T
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models.Movie, []error) {
|
||||
return func(keys []int) (ret []*models.Movie, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
func (m Middleware) fetchGroups(ctx context.Context) func(keys []int) ([]*models.Group, []error) {
|
||||
return func(keys []int) (ret []*models.Group, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Movie.FindMany(ctx, keys)
|
||||
ret, err = m.Repository.Group.FindMany(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchFiles(ctx context.Context) func(keys []file.ID) ([]file.File, []error) {
|
||||
return func(keys []file.ID) (ret []file.File, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
func (m Middleware) fetchFiles(ctx context.Context) func(keys []models.FileID) ([]models.File, []error) {
|
||||
return func(keys []models.FileID) (ret []models.File, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.File.Find(ctx, keys...)
|
||||
return err
|
||||
@@ -227,9 +258,9 @@ func (m Middleware) fetchFiles(ctx context.Context) func(keys []file.ID) ([]file
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
|
||||
return func(keys []int) (ret [][]file.ID, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
|
||||
return func(keys []int) (ret [][]models.FileID, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyFileIDs(ctx, keys)
|
||||
return err
|
||||
@@ -238,9 +269,9 @@ func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([]
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
|
||||
return func(keys []int) (ret [][]file.ID, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
|
||||
return func(keys []int) (ret [][]models.FileID, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Image.GetManyFileIDs(ctx, keys)
|
||||
return err
|
||||
@@ -249,9 +280,9 @@ func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([]
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
|
||||
return func(keys []int) (ret [][]file.ID, errs []error) {
|
||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||
func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
|
||||
return func(keys []int) (ret [][]models.FileID, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Gallery.GetManyFileIDs(ctx, keys)
|
||||
return err
|
||||
@@ -259,3 +290,58 @@ func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int)
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesOCount(ctx context.Context) func(keys []int) ([]int, []error) {
|
||||
return func(keys []int) (ret []int, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyOCount(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesPlayCount(ctx context.Context) func(keys []int) ([]int, []error) {
|
||||
return func(keys []int) (ret []int, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyViewCount(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesOHistory(ctx context.Context) func(keys []int) ([][]time.Time, []error) {
|
||||
return func(keys []int) (ret [][]time.Time, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyODates(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesPlayHistory(ctx context.Context) func(keys []int) ([][]time.Time, []error) {
|
||||
return func(keys []int) (ret [][]time.Time, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyViewDates(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesLastPlayed(ctx context.Context) func(keys []int) ([]*time.Time, []error) {
|
||||
return func(keys []int) (ret []*time.Time, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetManyLastViewed(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// FileLoaderConfig captures the config to create a new FileLoader
|
||||
type FileLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []file.ID) ([]file.File, []error)
|
||||
Fetch func(keys []models.FileID) ([]models.File, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
@@ -33,7 +33,7 @@ func NewFileLoader(config FileLoaderConfig) *FileLoader {
|
||||
// FileLoader batches and caches requests
|
||||
type FileLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []file.ID) ([]file.File, []error)
|
||||
fetch func(keys []models.FileID) ([]models.File, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
@@ -44,7 +44,7 @@ type FileLoader struct {
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[file.ID]file.File
|
||||
cache map[models.FileID]models.File
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
@@ -55,26 +55,26 @@ type FileLoader struct {
|
||||
}
|
||||
|
||||
type fileLoaderBatch struct {
|
||||
keys []file.ID
|
||||
data []file.File
|
||||
keys []models.FileID
|
||||
data []models.File
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a File by key, batching and caching will be applied automatically
|
||||
func (l *FileLoader) Load(key file.ID) (file.File, error) {
|
||||
func (l *FileLoader) Load(key models.FileID) (models.File, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a File.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *FileLoader) LoadThunk(key file.ID) func() (file.File, error) {
|
||||
func (l *FileLoader) LoadThunk(key models.FileID) func() (models.File, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (file.File, error) {
|
||||
return func() (models.File, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
@@ -85,10 +85,10 @@ func (l *FileLoader) LoadThunk(key file.ID) func() (file.File, error) {
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (file.File, error) {
|
||||
return func() (models.File, error) {
|
||||
<-batch.done
|
||||
|
||||
var data file.File
|
||||
var data models.File
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
@@ -113,14 +113,14 @@ func (l *FileLoader) LoadThunk(key file.ID) func() (file.File, error) {
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *FileLoader) LoadAll(keys []file.ID) ([]file.File, []error) {
|
||||
results := make([]func() (file.File, error), len(keys))
|
||||
func (l *FileLoader) LoadAll(keys []models.FileID) ([]models.File, []error) {
|
||||
results := make([]func() (models.File, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
files := make([]file.File, len(keys))
|
||||
files := make([]models.File, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
files[i], errors[i] = thunk()
|
||||
@@ -131,13 +131,13 @@ func (l *FileLoader) LoadAll(keys []file.ID) ([]file.File, []error) {
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Files.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *FileLoader) LoadAllThunk(keys []file.ID) func() ([]file.File, []error) {
|
||||
results := make([]func() (file.File, error), len(keys))
|
||||
func (l *FileLoader) LoadAllThunk(keys []models.FileID) func() ([]models.File, []error) {
|
||||
results := make([]func() (models.File, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]file.File, []error) {
|
||||
files := make([]file.File, len(keys))
|
||||
return func() ([]models.File, []error) {
|
||||
files := make([]models.File, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
files[i], errors[i] = thunk()
|
||||
@@ -149,7 +149,7 @@ func (l *FileLoader) LoadAllThunk(keys []file.ID) func() ([]file.File, []error)
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *FileLoader) Prime(key file.ID, value file.File) bool {
|
||||
func (l *FileLoader) Prime(key models.FileID, value models.File) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
@@ -160,22 +160,22 @@ func (l *FileLoader) Prime(key file.ID, value file.File) bool {
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *FileLoader) Clear(key file.ID) {
|
||||
func (l *FileLoader) Clear(key models.FileID) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *FileLoader) unsafeSet(key file.ID, value file.File) {
|
||||
func (l *FileLoader) unsafeSet(key models.FileID, value models.File) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[file.ID]file.File{}
|
||||
l.cache = map[models.FileID]models.File{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *fileLoaderBatch) keyIndex(l *FileLoader, key file.ID) int {
|
||||
func (b *fileLoaderBatch) keyIndex(l *FileLoader, key models.FileID) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// GalleryFileIDsLoaderConfig captures the config to create a new GalleryFileIDsLoader
|
||||
type GalleryFileIDsLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([][]file.ID, []error)
|
||||
Fetch func(keys []int) ([][]models.FileID, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
@@ -33,7 +33,7 @@ func NewGalleryFileIDsLoader(config GalleryFileIDsLoaderConfig) *GalleryFileIDsL
|
||||
// GalleryFileIDsLoader batches and caches requests
|
||||
type GalleryFileIDsLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([][]file.ID, []error)
|
||||
fetch func(keys []int) ([][]models.FileID, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
@@ -44,7 +44,7 @@ type GalleryFileIDsLoader struct {
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int][]file.ID
|
||||
cache map[int][]models.FileID
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
@@ -56,25 +56,25 @@ type GalleryFileIDsLoader struct {
|
||||
|
||||
type galleryFileIDsLoaderBatch struct {
|
||||
keys []int
|
||||
data [][]file.ID
|
||||
data [][]models.FileID
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a ID by key, batching and caching will be applied automatically
|
||||
func (l *GalleryFileIDsLoader) Load(key int) ([]file.ID, error) {
|
||||
// Load a FileID by key, batching and caching will be applied automatically
|
||||
func (l *GalleryFileIDsLoader) Load(key int) ([]models.FileID, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a ID.
|
||||
// LoadThunk returns a function that when called will block waiting for a FileID.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *GalleryFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
func (l *GalleryFileIDsLoader) LoadThunk(key int) func() ([]models.FileID, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]file.ID, error) {
|
||||
return func() ([]models.FileID, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
@@ -85,10 +85,10 @@ func (l *GalleryFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]file.ID, error) {
|
||||
return func() ([]models.FileID, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []file.ID
|
||||
var data []models.FileID
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
@@ -113,49 +113,49 @@ func (l *GalleryFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *GalleryFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
func (l *GalleryFileIDsLoader) LoadAll(keys []int) ([][]models.FileID, []error) {
|
||||
results := make([]func() ([]models.FileID, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
fileIDs := make([][]models.FileID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
fileIDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, errors
|
||||
return fileIDs, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a IDs.
|
||||
// LoadAllThunk returns a function that when called will block waiting for a FileIDs.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *GalleryFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
func (l *GalleryFileIDsLoader) LoadAllThunk(keys []int) func() ([][]models.FileID, []error) {
|
||||
results := make([]func() ([]models.FileID, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]file.ID, []error) {
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
return func() ([][]models.FileID, []error) {
|
||||
fileIDs := make([][]models.FileID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
fileIDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, errors
|
||||
return fileIDs, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *GalleryFileIDsLoader) Prime(key int, value []file.ID) bool {
|
||||
func (l *GalleryFileIDsLoader) Prime(key int, value []models.FileID) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]file.ID, len(value))
|
||||
cpy := make([]models.FileID, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
@@ -170,9 +170,9 @@ func (l *GalleryFileIDsLoader) Clear(key int) {
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *GalleryFileIDsLoader) unsafeSet(key int, value []file.ID) {
|
||||
func (l *GalleryFileIDsLoader) unsafeSet(key int, value []models.FileID) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int][]file.ID{}
|
||||
l.cache = map[int][]models.FileID{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// MovieLoaderConfig captures the config to create a new MovieLoader
|
||||
type MovieLoaderConfig struct {
|
||||
// GroupLoaderConfig captures the config to create a new GroupLoader
|
||||
type GroupLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*models.Movie, []error)
|
||||
Fetch func(keys []int) ([]*models.Group, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
@@ -21,19 +21,19 @@ type MovieLoaderConfig struct {
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewMovieLoader creates a new MovieLoader given a fetch, wait, and maxBatch
|
||||
func NewMovieLoader(config MovieLoaderConfig) *MovieLoader {
|
||||
return &MovieLoader{
|
||||
// NewGroupLoader creates a new GroupLoader given a fetch, wait, and maxBatch
|
||||
func NewGroupLoader(config GroupLoaderConfig) *GroupLoader {
|
||||
return &GroupLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// MovieLoader batches and caches requests
|
||||
type MovieLoader struct {
|
||||
// GroupLoader batches and caches requests
|
||||
type GroupLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]*models.Movie, []error)
|
||||
fetch func(keys []int) ([]*models.Group, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
@@ -44,51 +44,51 @@ type MovieLoader struct {
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]*models.Movie
|
||||
cache map[int]*models.Group
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *movieLoaderBatch
|
||||
batch *groupLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type movieLoaderBatch struct {
|
||||
type groupLoaderBatch struct {
|
||||
keys []int
|
||||
data []*models.Movie
|
||||
data []*models.Group
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Movie by key, batching and caching will be applied automatically
|
||||
func (l *MovieLoader) Load(key int) (*models.Movie, error) {
|
||||
// Load a Group by key, batching and caching will be applied automatically
|
||||
func (l *GroupLoader) Load(key int) (*models.Group, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Movie.
|
||||
// LoadThunk returns a function that when called will block waiting for a Group.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *MovieLoader) LoadThunk(key int) func() (*models.Movie, error) {
|
||||
func (l *GroupLoader) LoadThunk(key int) func() (*models.Group, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (*models.Movie, error) {
|
||||
return func() (*models.Group, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &movieLoaderBatch{done: make(chan struct{})}
|
||||
l.batch = &groupLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (*models.Movie, error) {
|
||||
return func() (*models.Group, error) {
|
||||
<-batch.done
|
||||
|
||||
var data *models.Movie
|
||||
var data *models.Group
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
@@ -113,43 +113,43 @@ func (l *MovieLoader) LoadThunk(key int) func() (*models.Movie, error) {
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *MovieLoader) LoadAll(keys []int) ([]*models.Movie, []error) {
|
||||
results := make([]func() (*models.Movie, error), len(keys))
|
||||
func (l *GroupLoader) LoadAll(keys []int) ([]*models.Group, []error) {
|
||||
results := make([]func() (*models.Group, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
movies := make([]*models.Movie, len(keys))
|
||||
groups := make([]*models.Group, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
movies[i], errors[i] = thunk()
|
||||
groups[i], errors[i] = thunk()
|
||||
}
|
||||
return movies, errors
|
||||
return groups, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Movies.
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Groups.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *MovieLoader) LoadAllThunk(keys []int) func() ([]*models.Movie, []error) {
|
||||
results := make([]func() (*models.Movie, error), len(keys))
|
||||
func (l *GroupLoader) LoadAllThunk(keys []int) func() ([]*models.Group, []error) {
|
||||
results := make([]func() (*models.Group, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]*models.Movie, []error) {
|
||||
movies := make([]*models.Movie, len(keys))
|
||||
return func() ([]*models.Group, []error) {
|
||||
groups := make([]*models.Group, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
movies[i], errors[i] = thunk()
|
||||
groups[i], errors[i] = thunk()
|
||||
}
|
||||
return movies, errors
|
||||
return groups, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *MovieLoader) Prime(key int, value *models.Movie) bool {
|
||||
func (l *GroupLoader) Prime(key int, value *models.Group) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
@@ -163,22 +163,22 @@ func (l *MovieLoader) Prime(key int, value *models.Movie) bool {
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *MovieLoader) Clear(key int) {
|
||||
func (l *GroupLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *MovieLoader) unsafeSet(key int, value *models.Movie) {
|
||||
func (l *GroupLoader) unsafeSet(key int, value *models.Group) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]*models.Movie{}
|
||||
l.cache = map[int]*models.Group{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *movieLoaderBatch) keyIndex(l *MovieLoader, key int) int {
|
||||
func (b *groupLoaderBatch) keyIndex(l *GroupLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
@@ -202,7 +202,7 @@ func (b *movieLoaderBatch) keyIndex(l *MovieLoader, key int) int {
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *movieLoaderBatch) startTimer(l *MovieLoader) {
|
||||
func (b *groupLoaderBatch) startTimer(l *GroupLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
@@ -218,7 +218,7 @@ func (b *movieLoaderBatch) startTimer(l *MovieLoader) {
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *movieLoaderBatch) end(l *MovieLoader) {
|
||||
func (b *groupLoaderBatch) end(l *GroupLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// ImageFileIDsLoaderConfig captures the config to create a new ImageFileIDsLoader
|
||||
type ImageFileIDsLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([][]file.ID, []error)
|
||||
Fetch func(keys []int) ([][]models.FileID, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
@@ -33,7 +33,7 @@ func NewImageFileIDsLoader(config ImageFileIDsLoaderConfig) *ImageFileIDsLoader
|
||||
// ImageFileIDsLoader batches and caches requests
|
||||
type ImageFileIDsLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([][]file.ID, []error)
|
||||
fetch func(keys []int) ([][]models.FileID, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
@@ -44,7 +44,7 @@ type ImageFileIDsLoader struct {
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int][]file.ID
|
||||
cache map[int][]models.FileID
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
@@ -56,25 +56,25 @@ type ImageFileIDsLoader struct {
|
||||
|
||||
type imageFileIDsLoaderBatch struct {
|
||||
keys []int
|
||||
data [][]file.ID
|
||||
data [][]models.FileID
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a ID by key, batching and caching will be applied automatically
|
||||
func (l *ImageFileIDsLoader) Load(key int) ([]file.ID, error) {
|
||||
// Load a FileID by key, batching and caching will be applied automatically
|
||||
func (l *ImageFileIDsLoader) Load(key int) ([]models.FileID, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a ID.
|
||||
// LoadThunk returns a function that when called will block waiting for a FileID.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *ImageFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
func (l *ImageFileIDsLoader) LoadThunk(key int) func() ([]models.FileID, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]file.ID, error) {
|
||||
return func() ([]models.FileID, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
@@ -85,10 +85,10 @@ func (l *ImageFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]file.ID, error) {
|
||||
return func() ([]models.FileID, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []file.ID
|
||||
var data []models.FileID
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
@@ -113,49 +113,49 @@ func (l *ImageFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *ImageFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
func (l *ImageFileIDsLoader) LoadAll(keys []int) ([][]models.FileID, []error) {
|
||||
results := make([]func() ([]models.FileID, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
fileIDs := make([][]models.FileID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
fileIDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, errors
|
||||
return fileIDs, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a IDs.
|
||||
// LoadAllThunk returns a function that when called will block waiting for a FileIDs.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *ImageFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
func (l *ImageFileIDsLoader) LoadAllThunk(keys []int) func() ([][]models.FileID, []error) {
|
||||
results := make([]func() ([]models.FileID, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]file.ID, []error) {
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
return func() ([][]models.FileID, []error) {
|
||||
fileIDs := make([][]models.FileID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
fileIDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, errors
|
||||
return fileIDs, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *ImageFileIDsLoader) Prime(key int, value []file.ID) bool {
|
||||
func (l *ImageFileIDsLoader) Prime(key int, value []models.FileID) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]file.ID, len(value))
|
||||
cpy := make([]models.FileID, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
@@ -170,9 +170,9 @@ func (l *ImageFileIDsLoader) Clear(key int) {
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *ImageFileIDsLoader) unsafeSet(key int, value []file.ID) {
|
||||
func (l *ImageFileIDsLoader) unsafeSet(key int, value []models.FileID) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int][]file.ID{}
|
||||
l.cache = map[int][]models.FileID{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// SceneFileIDsLoaderConfig captures the config to create a new SceneFileIDsLoader
|
||||
type SceneFileIDsLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([][]file.ID, []error)
|
||||
Fetch func(keys []int) ([][]models.FileID, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
@@ -33,7 +33,7 @@ func NewSceneFileIDsLoader(config SceneFileIDsLoaderConfig) *SceneFileIDsLoader
|
||||
// SceneFileIDsLoader batches and caches requests
|
||||
type SceneFileIDsLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([][]file.ID, []error)
|
||||
fetch func(keys []int) ([][]models.FileID, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
@@ -44,7 +44,7 @@ type SceneFileIDsLoader struct {
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int][]file.ID
|
||||
cache map[int][]models.FileID
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
@@ -56,25 +56,25 @@ type SceneFileIDsLoader struct {
|
||||
|
||||
type sceneFileIDsLoaderBatch struct {
|
||||
keys []int
|
||||
data [][]file.ID
|
||||
data [][]models.FileID
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a ID by key, batching and caching will be applied automatically
|
||||
func (l *SceneFileIDsLoader) Load(key int) ([]file.ID, error) {
|
||||
// Load a FileID by key, batching and caching will be applied automatically
|
||||
func (l *SceneFileIDsLoader) Load(key int) ([]models.FileID, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a ID.
|
||||
// LoadThunk returns a function that when called will block waiting for a FileID.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *SceneFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
func (l *SceneFileIDsLoader) LoadThunk(key int) func() ([]models.FileID, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]file.ID, error) {
|
||||
return func() ([]models.FileID, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
@@ -85,10 +85,10 @@ func (l *SceneFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]file.ID, error) {
|
||||
return func() ([]models.FileID, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []file.ID
|
||||
var data []models.FileID
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
@@ -113,49 +113,49 @@ func (l *SceneFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *SceneFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
func (l *SceneFileIDsLoader) LoadAll(keys []int) ([][]models.FileID, []error) {
|
||||
results := make([]func() ([]models.FileID, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
fileIDs := make([][]models.FileID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
fileIDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, errors
|
||||
return fileIDs, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a IDs.
|
||||
// LoadAllThunk returns a function that when called will block waiting for a FileIDs.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *SceneFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
|
||||
results := make([]func() ([]file.ID, error), len(keys))
|
||||
func (l *SceneFileIDsLoader) LoadAllThunk(keys []int) func() ([][]models.FileID, []error) {
|
||||
results := make([]func() ([]models.FileID, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]file.ID, []error) {
|
||||
iDs := make([][]file.ID, len(keys))
|
||||
return func() ([][]models.FileID, []error) {
|
||||
fileIDs := make([][]models.FileID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
iDs[i], errors[i] = thunk()
|
||||
fileIDs[i], errors[i] = thunk()
|
||||
}
|
||||
return iDs, errors
|
||||
return fileIDs, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *SceneFileIDsLoader) Prime(key int, value []file.ID) bool {
|
||||
func (l *SceneFileIDsLoader) Prime(key int, value []models.FileID) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]file.ID, len(value))
|
||||
cpy := make([]models.FileID, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
@@ -170,9 +170,9 @@ func (l *SceneFileIDsLoader) Clear(key int) {
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *SceneFileIDsLoader) unsafeSet(key int, value []file.ID) {
|
||||
func (l *SceneFileIDsLoader) unsafeSet(key int, value []models.FileID) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int][]file.ID{}
|
||||
l.cache = map[int][]models.FileID{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
@@ -1,28 +1,16 @@
|
||||
package generator
|
||||
|
||||
import "text/template"
|
||||
|
||||
var tpl = template.Must(template.New("generated").
|
||||
Funcs(template.FuncMap{
|
||||
"lcFirst": lcFirst,
|
||||
}).
|
||||
Parse(`
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package {{.Package}}
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
{{if .KeyType.ImportPath}}"{{.KeyType.ImportPath}}"{{end}}
|
||||
{{if .ValType.ImportPath}}"{{.ValType.ImportPath}}"{{end}}
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// {{.Name}}Config captures the config to create a new {{.Name}}
|
||||
type {{.Name}}Config struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []{{.KeyType.String}}) ([]{{.ValType.String}}, []error)
|
||||
// SceneLastPlayedLoaderConfig captures the config to create a new SceneLastPlayedLoader
|
||||
type SceneLastPlayedLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]*time.Time, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
@@ -31,19 +19,19 @@ type {{.Name}}Config struct {
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// New{{.Name}} creates a new {{.Name}} given a fetch, wait, and maxBatch
|
||||
func New{{.Name}}(config {{.Name}}Config) *{{.Name}} {
|
||||
return &{{.Name}}{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
// NewSceneLastPlayedLoader creates a new SceneLastPlayedLoader given a fetch, wait, and maxBatch
|
||||
func NewSceneLastPlayedLoader(config SceneLastPlayedLoaderConfig) *SceneLastPlayedLoader {
|
||||
return &SceneLastPlayedLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// {{.Name}} batches and caches requests
|
||||
type {{.Name}} struct {
|
||||
// SceneLastPlayedLoader batches and caches requests
|
||||
type SceneLastPlayedLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []{{.KeyType.String}}) ([]{{.ValType.String}}, []error)
|
||||
fetch func(keys []int) ([]*time.Time, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
@@ -54,51 +42,51 @@ type {{.Name}} struct {
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[{{.KeyType.String}}]{{.ValType.String}}
|
||||
cache map[int]*time.Time
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *{{.Name|lcFirst}}Batch
|
||||
batch *sceneLastPlayedLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type {{.Name|lcFirst}}Batch struct {
|
||||
keys []{{.KeyType}}
|
||||
data []{{.ValType.String}}
|
||||
type sceneLastPlayedLoaderBatch struct {
|
||||
keys []int
|
||||
data []*time.Time
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a {{.ValType.Name}} by key, batching and caching will be applied automatically
|
||||
func (l *{{.Name}}) Load(key {{.KeyType.String}}) ({{.ValType.String}}, error) {
|
||||
// Load a Time by key, batching and caching will be applied automatically
|
||||
func (l *SceneLastPlayedLoader) Load(key int) (*time.Time, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a {{.ValType.Name}}.
|
||||
// LoadThunk returns a function that when called will block waiting for a Time.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *{{.Name}}) LoadThunk(key {{.KeyType.String}}) func() ({{.ValType.String}}, error) {
|
||||
func (l *SceneLastPlayedLoader) LoadThunk(key int) func() (*time.Time, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ({{.ValType.String}}, error) {
|
||||
return func() (*time.Time, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &{{.Name|lcFirst}}Batch{done: make(chan struct{})}
|
||||
l.batch = &sceneLastPlayedLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ({{.ValType.String}}, error) {
|
||||
return func() (*time.Time, error) {
|
||||
<-batch.done
|
||||
|
||||
var data {{.ValType.String}}
|
||||
var data *time.Time
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
@@ -123,82 +111,72 @@ func (l *{{.Name}}) LoadThunk(key {{.KeyType.String}}) func() ({{.ValType.String
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *{{.Name}}) LoadAll(keys []{{.KeyType}}) ([]{{.ValType.String}}, []error) {
|
||||
results := make([]func() ({{.ValType.String}}, error), len(keys))
|
||||
func (l *SceneLastPlayedLoader) LoadAll(keys []int) ([]*time.Time, []error) {
|
||||
results := make([]func() (*time.Time, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
{{.ValType.Name|lcFirst}}s := make([]{{.ValType.String}}, len(keys))
|
||||
times := make([]*time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
{{.ValType.Name|lcFirst}}s[i], errors[i] = thunk()
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return {{.ValType.Name|lcFirst}}s, errors
|
||||
return times, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a {{.ValType.Name}}s.
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Times.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *{{.Name}}) LoadAllThunk(keys []{{.KeyType}}) (func() ([]{{.ValType.String}}, []error)) {
|
||||
results := make([]func() ({{.ValType.String}}, error), len(keys))
|
||||
for i, key := range keys {
|
||||
func (l *SceneLastPlayedLoader) LoadAllThunk(keys []int) func() ([]*time.Time, []error) {
|
||||
results := make([]func() (*time.Time, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]{{.ValType.String}}, []error) {
|
||||
{{.ValType.Name|lcFirst}}s := make([]{{.ValType.String}}, len(keys))
|
||||
return func() ([]*time.Time, []error) {
|
||||
times := make([]*time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
{{.ValType.Name|lcFirst}}s[i], errors[i] = thunk()
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return {{.ValType.Name|lcFirst}}s, errors
|
||||
return times, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *{{.Name}}) Prime(key {{.KeyType}}, value {{.ValType.String}}) bool {
|
||||
func (l *SceneLastPlayedLoader) Prime(key int, value *time.Time) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
{{- if .ValType.IsPtr }}
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := *value
|
||||
l.unsafeSet(key, &cpy)
|
||||
{{- else if .ValType.IsSlice }}
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make({{.ValType.String}}, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
{{- else }}
|
||||
l.unsafeSet(key, value)
|
||||
{{- end }}
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := *value
|
||||
l.unsafeSet(key, &cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *{{.Name}}) Clear(key {{.KeyType}}) {
|
||||
func (l *SceneLastPlayedLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *{{.Name}}) unsafeSet(key {{.KeyType}}, value {{.ValType.String}}) {
|
||||
func (l *SceneLastPlayedLoader) unsafeSet(key int, value *time.Time) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[{{.KeyType}}]{{.ValType.String}}{}
|
||||
l.cache = map[int]*time.Time{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *{{.Name|lcFirst}}Batch) keyIndex(l *{{.Name}}, key {{.KeyType}}) int {
|
||||
func (b *sceneLastPlayedLoaderBatch) keyIndex(l *SceneLastPlayedLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
@@ -222,7 +200,7 @@ func (b *{{.Name|lcFirst}}Batch) keyIndex(l *{{.Name}}, key {{.KeyType}}) int {
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *{{.Name|lcFirst}}Batch) startTimer(l *{{.Name}}) {
|
||||
func (b *sceneLastPlayedLoaderBatch) startTimer(l *SceneLastPlayedLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
@@ -238,8 +216,7 @@ func (b *{{.Name|lcFirst}}Batch) startTimer(l *{{.Name}}) {
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *{{.Name|lcFirst}}Batch) end(l *{{.Name}}) {
|
||||
func (b *sceneLastPlayedLoaderBatch) end(l *SceneLastPlayedLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
`))
|
||||
219
internal/api/loaders/sceneocountloader_gen.go
Normal file
219
internal/api/loaders/sceneocountloader_gen.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SceneOCountLoaderConfig captures the config to create a new SceneOCountLoader
|
||||
type SceneOCountLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]int, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewSceneOCountLoader creates a new SceneOCountLoader given a fetch, wait, and maxBatch
|
||||
func NewSceneOCountLoader(config SceneOCountLoaderConfig) *SceneOCountLoader {
|
||||
return &SceneOCountLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// SceneOCountLoader batches and caches requests
|
||||
type SceneOCountLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]int, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]int
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *sceneOCountLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type sceneOCountLoaderBatch struct {
|
||||
keys []int
|
||||
data []int
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a int by key, batching and caching will be applied automatically
|
||||
func (l *SceneOCountLoader) Load(key int) (int, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a int.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *SceneOCountLoader) LoadThunk(key int) func() (int, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (int, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &sceneOCountLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (int, error) {
|
||||
<-batch.done
|
||||
|
||||
var data int
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *SceneOCountLoader) LoadAll(keys []int) ([]int, []error) {
|
||||
results := make([]func() (int, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
ints := make([]int, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
ints[i], errors[i] = thunk()
|
||||
}
|
||||
return ints, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a ints.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *SceneOCountLoader) LoadAllThunk(keys []int) func() ([]int, []error) {
|
||||
results := make([]func() (int, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]int, []error) {
|
||||
ints := make([]int, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
ints[i], errors[i] = thunk()
|
||||
}
|
||||
return ints, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *SceneOCountLoader) Prime(key int, value int) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
l.unsafeSet(key, value)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *SceneOCountLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *SceneOCountLoader) unsafeSet(key int, value int) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]int{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *sceneOCountLoaderBatch) keyIndex(l *SceneOCountLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *sceneOCountLoaderBatch) startTimer(l *SceneOCountLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *sceneOCountLoaderBatch) end(l *SceneOCountLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
223
internal/api/loaders/sceneohistoryloader_gen.go
Normal file
223
internal/api/loaders/sceneohistoryloader_gen.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SceneOHistoryLoaderConfig captures the config to create a new SceneOHistoryLoader
|
||||
type SceneOHistoryLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([][]time.Time, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewSceneOHistoryLoader creates a new SceneOHistoryLoader given a fetch, wait, and maxBatch
|
||||
func NewSceneOHistoryLoader(config SceneOHistoryLoaderConfig) *SceneOHistoryLoader {
|
||||
return &SceneOHistoryLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// SceneOHistoryLoader batches and caches requests
|
||||
type SceneOHistoryLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([][]time.Time, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int][]time.Time
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *sceneOHistoryLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type sceneOHistoryLoaderBatch struct {
|
||||
keys []int
|
||||
data [][]time.Time
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Time by key, batching and caching will be applied automatically
|
||||
func (l *SceneOHistoryLoader) Load(key int) ([]time.Time, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Time.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *SceneOHistoryLoader) LoadThunk(key int) func() ([]time.Time, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]time.Time, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &sceneOHistoryLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]time.Time, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []time.Time
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *SceneOHistoryLoader) LoadAll(keys []int) ([][]time.Time, []error) {
|
||||
results := make([]func() ([]time.Time, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
times := make([][]time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return times, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Times.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *SceneOHistoryLoader) LoadAllThunk(keys []int) func() ([][]time.Time, []error) {
|
||||
results := make([]func() ([]time.Time, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]time.Time, []error) {
|
||||
times := make([][]time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return times, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *SceneOHistoryLoader) Prime(key int, value []time.Time) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]time.Time, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *SceneOHistoryLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *SceneOHistoryLoader) unsafeSet(key int, value []time.Time) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int][]time.Time{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *sceneOHistoryLoaderBatch) keyIndex(l *SceneOHistoryLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *sceneOHistoryLoaderBatch) startTimer(l *SceneOHistoryLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *sceneOHistoryLoaderBatch) end(l *SceneOHistoryLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
219
internal/api/loaders/sceneplaycountloader_gen.go
Normal file
219
internal/api/loaders/sceneplaycountloader_gen.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScenePlayCountLoaderConfig captures the config to create a new ScenePlayCountLoader
|
||||
type ScenePlayCountLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]int, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewScenePlayCountLoader creates a new ScenePlayCountLoader given a fetch, wait, and maxBatch
|
||||
func NewScenePlayCountLoader(config ScenePlayCountLoaderConfig) *ScenePlayCountLoader {
|
||||
return &ScenePlayCountLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// ScenePlayCountLoader batches and caches requests
|
||||
type ScenePlayCountLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]int, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int]int
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *scenePlayCountLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type scenePlayCountLoaderBatch struct {
|
||||
keys []int
|
||||
data []int
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a int by key, batching and caching will be applied automatically
|
||||
func (l *ScenePlayCountLoader) Load(key int) (int, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a int.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *ScenePlayCountLoader) LoadThunk(key int) func() (int, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (int, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &scenePlayCountLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (int, error) {
|
||||
<-batch.done
|
||||
|
||||
var data int
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *ScenePlayCountLoader) LoadAll(keys []int) ([]int, []error) {
|
||||
results := make([]func() (int, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
ints := make([]int, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
ints[i], errors[i] = thunk()
|
||||
}
|
||||
return ints, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a ints.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *ScenePlayCountLoader) LoadAllThunk(keys []int) func() ([]int, []error) {
|
||||
results := make([]func() (int, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]int, []error) {
|
||||
ints := make([]int, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
ints[i], errors[i] = thunk()
|
||||
}
|
||||
return ints, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *ScenePlayCountLoader) Prime(key int, value int) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
l.unsafeSet(key, value)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *ScenePlayCountLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *ScenePlayCountLoader) unsafeSet(key int, value int) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]int{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *scenePlayCountLoaderBatch) keyIndex(l *ScenePlayCountLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *scenePlayCountLoaderBatch) startTimer(l *ScenePlayCountLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *scenePlayCountLoaderBatch) end(l *ScenePlayCountLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
223
internal/api/loaders/sceneplayhistoryloader_gen.go
Normal file
223
internal/api/loaders/sceneplayhistoryloader_gen.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScenePlayHistoryLoaderConfig captures the config to create a new ScenePlayHistoryLoader
|
||||
type ScenePlayHistoryLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([][]time.Time, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewScenePlayHistoryLoader creates a new ScenePlayHistoryLoader given a fetch, wait, and maxBatch
|
||||
func NewScenePlayHistoryLoader(config ScenePlayHistoryLoaderConfig) *ScenePlayHistoryLoader {
|
||||
return &ScenePlayHistoryLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// ScenePlayHistoryLoader batches and caches requests
|
||||
type ScenePlayHistoryLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([][]time.Time, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[int][]time.Time
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *scenePlayHistoryLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type scenePlayHistoryLoaderBatch struct {
|
||||
keys []int
|
||||
data [][]time.Time
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a Time by key, batching and caching will be applied automatically
|
||||
func (l *ScenePlayHistoryLoader) Load(key int) ([]time.Time, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a Time.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *ScenePlayHistoryLoader) LoadThunk(key int) func() ([]time.Time, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]time.Time, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &scenePlayHistoryLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]time.Time, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []time.Time
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *ScenePlayHistoryLoader) LoadAll(keys []int) ([][]time.Time, []error) {
|
||||
results := make([]func() ([]time.Time, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
times := make([][]time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return times, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a Times.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *ScenePlayHistoryLoader) LoadAllThunk(keys []int) func() ([][]time.Time, []error) {
|
||||
results := make([]func() ([]time.Time, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]time.Time, []error) {
|
||||
times := make([][]time.Time, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
times[i], errors[i] = thunk()
|
||||
}
|
||||
return times, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *ScenePlayHistoryLoader) Prime(key int, value []time.Time) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]time.Time, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *ScenePlayHistoryLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *ScenePlayHistoryLoader) unsafeSet(key int, value []time.Time) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int][]time.Time{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *scenePlayHistoryLoaderBatch) keyIndex(l *ScenePlayHistoryLoader, key int) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *scenePlayHistoryLoaderBatch) startTimer(l *ScenePlayHistoryLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *scenePlayHistoryLoaderBatch) end(l *ScenePlayHistoryLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
@@ -1,57 +1,64 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
var ErrTimestamp = errors.New("cannot parse Timestamp")
|
||||
|
||||
func MarshalTimestamp(t time.Time) graphql.Marshaler {
|
||||
if t.IsZero() {
|
||||
return graphql.Null
|
||||
}
|
||||
|
||||
return graphql.WriterFunc(func(w io.Writer) {
|
||||
_, err := io.WriteString(w, strconv.Quote(t.Format(time.RFC3339Nano)))
|
||||
if err != nil {
|
||||
logger.Warnf("could not marshal timestamp: %v", err)
|
||||
}
|
||||
})
|
||||
type BaseFile interface {
|
||||
IsBaseFile()
|
||||
}
|
||||
|
||||
func UnmarshalTimestamp(v interface{}) (time.Time, error) {
|
||||
if tmpStr, ok := v.(string); ok {
|
||||
if len(tmpStr) == 0 {
|
||||
return time.Time{}, fmt.Errorf("%w: empty string", ErrTimestamp)
|
||||
}
|
||||
|
||||
switch tmpStr[0] {
|
||||
case '>', '<':
|
||||
d, err := time.ParseDuration(tmpStr[1:])
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("%w: cannot parse %v-duration: %v", ErrTimestamp, tmpStr[0], err)
|
||||
}
|
||||
t := time.Now()
|
||||
// Compute point in time:
|
||||
if tmpStr[0] == '<' {
|
||||
t = t.Add(-d)
|
||||
} else {
|
||||
t = t.Add(d)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
return utils.ParseDateStringAsTime(tmpStr)
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("%w: not a string", ErrTimestamp)
|
||||
type VisualFile interface {
|
||||
IsVisualFile()
|
||||
}
|
||||
|
||||
func convertVisualFile(f models.File) (VisualFile, error) {
|
||||
switch f := f.(type) {
|
||||
case VisualFile:
|
||||
return f, nil
|
||||
case *models.VideoFile:
|
||||
return &VideoFile{VideoFile: f}, nil
|
||||
case *models.ImageFile:
|
||||
return &ImageFile{ImageFile: f}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("file %s is not a visual file", f.Base().Path)
|
||||
}
|
||||
}
|
||||
|
||||
type GalleryFile struct {
|
||||
*models.BaseFile
|
||||
}
|
||||
|
||||
func (GalleryFile) IsBaseFile() {}
|
||||
|
||||
func (GalleryFile) IsVisualFile() {}
|
||||
|
||||
func (f *GalleryFile) Fingerprints() []models.Fingerprint {
|
||||
return f.BaseFile.Fingerprints
|
||||
}
|
||||
|
||||
type VideoFile struct {
|
||||
*models.VideoFile
|
||||
}
|
||||
|
||||
func (VideoFile) IsBaseFile() {}
|
||||
|
||||
func (VideoFile) IsVisualFile() {}
|
||||
|
||||
func (f *VideoFile) Fingerprints() []models.Fingerprint {
|
||||
return f.VideoFile.Fingerprints
|
||||
}
|
||||
|
||||
type ImageFile struct {
|
||||
*models.ImageFile
|
||||
}
|
||||
|
||||
func (ImageFile) IsBaseFile() {}
|
||||
|
||||
func (ImageFile) IsVisualFile() {}
|
||||
|
||||
func (f *ImageFile) Fingerprints() []models.Fingerprint {
|
||||
return f.ImageFile.Fingerprints
|
||||
}
|
||||
|
||||
37
internal/api/plugin_map.go
Normal file
37
internal/api/plugin_map.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
)
|
||||
|
||||
func MarshalPluginConfigMap(val map[string]map[string]interface{}) graphql.Marshaler {
|
||||
return graphql.WriterFunc(func(w io.Writer) {
|
||||
err := json.NewEncoder(w).Encode(val)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func UnmarshalPluginConfigMap(v interface{}) (map[string]map[string]interface{}, error) {
|
||||
m, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T is not a plugin config map", v)
|
||||
}
|
||||
|
||||
result := make(map[string]map[string]interface{})
|
||||
for k, v := range m {
|
||||
val, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("key %s (%T) is not a map", k, v)
|
||||
}
|
||||
|
||||
result[k] = val
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -29,15 +29,15 @@ var (
|
||||
)
|
||||
|
||||
type hookExecutor interface {
|
||||
ExecutePostHooks(ctx context.Context, id int, hookType plugin.HookTriggerEnum, input interface{}, inputFields []string)
|
||||
ExecutePostHooks(ctx context.Context, id int, hookType hook.TriggerEnum, input interface{}, inputFields []string)
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
txnManager txn.Manager
|
||||
repository manager.Repository
|
||||
repository models.Repository
|
||||
sceneService manager.SceneService
|
||||
imageService manager.ImageService
|
||||
galleryService manager.GalleryService
|
||||
groupService manager.GroupService
|
||||
|
||||
hookExecutor hookExecutor
|
||||
}
|
||||
@@ -73,15 +73,38 @@ func (r *Resolver) SceneMarker() SceneMarkerResolver {
|
||||
func (r *Resolver) Studio() StudioResolver {
|
||||
return &studioResolver{r}
|
||||
}
|
||||
func (r *Resolver) Movie() MovieResolver {
|
||||
return &movieResolver{r}
|
||||
|
||||
func (r *Resolver) Group() GroupResolver {
|
||||
return &groupResolver{r}
|
||||
}
|
||||
func (r *Resolver) Movie() MovieResolver {
|
||||
return &movieResolver{&groupResolver{r}}
|
||||
}
|
||||
|
||||
func (r *Resolver) Subscription() SubscriptionResolver {
|
||||
return &subscriptionResolver{r}
|
||||
}
|
||||
func (r *Resolver) Tag() TagResolver {
|
||||
return &tagResolver{r}
|
||||
}
|
||||
func (r *Resolver) GalleryFile() GalleryFileResolver {
|
||||
return &galleryFileResolver{r}
|
||||
}
|
||||
func (r *Resolver) VideoFile() VideoFileResolver {
|
||||
return &videoFileResolver{r}
|
||||
}
|
||||
func (r *Resolver) ImageFile() ImageFileResolver {
|
||||
return &imageFileResolver{r}
|
||||
}
|
||||
func (r *Resolver) SavedFilter() SavedFilterResolver {
|
||||
return &savedFilterResolver{r}
|
||||
}
|
||||
func (r *Resolver) Plugin() PluginResolver {
|
||||
return &pluginResolver{r}
|
||||
}
|
||||
func (r *Resolver) ConfigResult() ConfigResultResolver {
|
||||
return &configResultResolver{r}
|
||||
}
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
||||
@@ -94,15 +117,29 @@ type sceneResolver struct{ *Resolver }
|
||||
type sceneMarkerResolver struct{ *Resolver }
|
||||
type imageResolver struct{ *Resolver }
|
||||
type studioResolver struct{ *Resolver }
|
||||
type movieResolver struct{ *Resolver }
|
||||
|
||||
// movie is group under the hood
|
||||
type groupResolver struct{ *Resolver }
|
||||
type movieResolver struct{ *groupResolver }
|
||||
|
||||
type tagResolver struct{ *Resolver }
|
||||
type galleryFileResolver struct{ *Resolver }
|
||||
type videoFileResolver struct{ *Resolver }
|
||||
type imageFileResolver struct{ *Resolver }
|
||||
type savedFilterResolver struct{ *Resolver }
|
||||
type pluginResolver struct{ *Resolver }
|
||||
type configResultResolver struct{ *Resolver }
|
||||
|
||||
func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
return txn.WithTxn(ctx, r.txnManager, fn)
|
||||
return r.repository.WithTxn(ctx, fn)
|
||||
}
|
||||
|
||||
func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||
return txn.WithReadTxn(ctx, r.txnManager, fn)
|
||||
return r.repository.WithReadTxn(ctx, fn)
|
||||
}
|
||||
|
||||
func (r *Resolver) stashboxRepository() stashbox.Repository {
|
||||
return stashbox.NewRepository(r.repository)
|
||||
}
|
||||
|
||||
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
|
||||
@@ -141,27 +178,90 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
||||
var ret StatsResultType
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
repo := r.repository
|
||||
scenesQB := repo.Scene
|
||||
sceneQB := repo.Scene
|
||||
imageQB := repo.Image
|
||||
galleryQB := repo.Gallery
|
||||
studiosQB := repo.Studio
|
||||
performersQB := repo.Performer
|
||||
moviesQB := repo.Movie
|
||||
tagsQB := repo.Tag
|
||||
scenesCount, _ := scenesQB.Count(ctx)
|
||||
scenesSize, _ := scenesQB.Size(ctx)
|
||||
scenesDuration, _ := scenesQB.Duration(ctx)
|
||||
imageCount, _ := imageQB.Count(ctx)
|
||||
imageSize, _ := imageQB.Size(ctx)
|
||||
galleryCount, _ := galleryQB.Count(ctx)
|
||||
performersCount, _ := performersQB.Count(ctx)
|
||||
studiosCount, _ := studiosQB.Count(ctx)
|
||||
moviesCount, _ := moviesQB.Count(ctx)
|
||||
tagsCount, _ := tagsQB.Count(ctx)
|
||||
totalOCount, _ := scenesQB.OCount(ctx)
|
||||
totalPlayDuration, _ := scenesQB.PlayDuration(ctx)
|
||||
totalPlayCount, _ := scenesQB.PlayCount(ctx)
|
||||
uniqueScenePlayCount, _ := scenesQB.UniqueScenePlayCount(ctx)
|
||||
studioQB := repo.Studio
|
||||
performerQB := repo.Performer
|
||||
movieQB := repo.Group
|
||||
tagQB := repo.Tag
|
||||
|
||||
// embrace the error
|
||||
|
||||
scenesCount, err := sceneQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenesSize, err := sceneQB.Size(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenesDuration, err := sceneQB.Duration(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageCount, err := imageQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageSize, err := imageQB.Size(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
galleryCount, err := galleryQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
performersCount, err := performerQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
studiosCount, err := studioQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
groupsCount, err := movieQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tagsCount, err := tagQB.Count(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenesTotalOCount, err := sceneQB.GetAllOCount(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imagesTotalOCount, err := imageQB.OCount(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
totalOCount := scenesTotalOCount + imagesTotalOCount
|
||||
|
||||
totalPlayDuration, err := sceneQB.PlayDuration(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalPlayCount, err := sceneQB.CountAllViews(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uniqueScenePlayCount, err := sceneQB.CountUniqueViews(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = StatsResultType{
|
||||
SceneCount: scenesCount,
|
||||
@@ -172,7 +272,8 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
||||
GalleryCount: galleryCount,
|
||||
PerformerCount: performersCount,
|
||||
StudioCount: studiosCount,
|
||||
MovieCount: moviesCount,
|
||||
GroupCount: groupsCount,
|
||||
MovieCount: groupsCount,
|
||||
TagCount: tagsCount,
|
||||
TotalOCount: totalOCount,
|
||||
TotalPlayDuration: totalPlayDuration,
|
||||
|
||||
25
internal/api/resolver_model_config.go
Normal file
25
internal/api/resolver_model_config.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
)
|
||||
|
||||
func (r *configResultResolver) Plugins(ctx context.Context, obj *ConfigResult, include []string) (map[string]map[string]interface{}, error) {
|
||||
if len(include) == 0 {
|
||||
ret := config.GetInstance().GetAllPluginConfiguration()
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
ret := make(map[string]map[string]interface{})
|
||||
|
||||
for _, plugin := range include {
|
||||
c := config.GetInstance().GetPluginConfiguration(plugin)
|
||||
if len(c) > 0 {
|
||||
ret[plugin] = c
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
30
internal/api/resolver_model_file.go
Normal file
30
internal/api/resolver_model_file.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package api
|
||||
|
||||
import "context"
|
||||
|
||||
func (r *galleryFileResolver) Fingerprint(ctx context.Context, obj *GalleryFile, type_ string) (*string, error) {
|
||||
fp := obj.BaseFile.Fingerprints.For(type_)
|
||||
if fp != nil {
|
||||
v := fp.Value()
|
||||
return &v, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageFileResolver) Fingerprint(ctx context.Context, obj *ImageFile, type_ string) (*string, error) {
|
||||
fp := obj.ImageFile.Fingerprints.For(type_)
|
||||
if fp != nil {
|
||||
v := fp.Value()
|
||||
return &v, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *videoFileResolver) Fingerprint(ctx context.Context, obj *VideoFile, type_ string) (*string, error) {
|
||||
fp := obj.VideoFile.Fingerprints.For(type_)
|
||||
if fp != nil {
|
||||
v := fp.Value()
|
||||
return &v, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -2,31 +2,17 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *galleryResolver) getPrimaryFile(ctx context.Context, obj *models.Gallery) (file.File, error) {
|
||||
if obj.PrimaryFileID != nil {
|
||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) getFiles(ctx context.Context, obj *models.Gallery) ([]file.File, error) {
|
||||
func (r *galleryResolver) getFiles(ctx context.Context, obj *models.Gallery) ([]models.File, error) {
|
||||
fileIDs, err := loaders.From(ctx).GalleryFiles.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -45,34 +31,20 @@ func (r *galleryResolver) Files(ctx context.Context, obj *models.Gallery) ([]*Ga
|
||||
ret := make([]*GalleryFile, len(files))
|
||||
|
||||
for i, f := range files {
|
||||
base := f.Base()
|
||||
ret[i] = &GalleryFile{
|
||||
ID: strconv.Itoa(int(base.ID)),
|
||||
Path: base.Path,
|
||||
Basename: base.Basename,
|
||||
ParentFolderID: strconv.Itoa(int(base.ParentFolderID)),
|
||||
ModTime: base.ModTime,
|
||||
Size: base.Size,
|
||||
CreatedAt: base.CreatedAt,
|
||||
UpdatedAt: base.UpdatedAt,
|
||||
Fingerprints: resolveFingerprints(base),
|
||||
}
|
||||
|
||||
if base.ZipFileID != nil {
|
||||
zipFileID := strconv.Itoa(int(*base.ZipFileID))
|
||||
ret[i].ZipFileID = &zipFileID
|
||||
BaseFile: f.Base(),
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*Folder, error) {
|
||||
func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*models.Folder, error) {
|
||||
if obj.FolderID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var ret *file.Folder
|
||||
var ret *models.Folder
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
@@ -91,56 +63,6 @@ func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*Fol
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rr := &Folder{
|
||||
ID: ret.ID.String(),
|
||||
Path: ret.Path,
|
||||
ModTime: ret.ModTime,
|
||||
CreatedAt: ret.CreatedAt,
|
||||
UpdatedAt: ret.UpdatedAt,
|
||||
}
|
||||
|
||||
if ret.ParentFolderID != nil {
|
||||
pfidStr := ret.ParentFolderID.String()
|
||||
rr.ParentFolderID = &pfidStr
|
||||
}
|
||||
|
||||
if ret.ZipFileID != nil {
|
||||
zfidStr := ret.ZipFileID.String()
|
||||
rr.ZipFileID = &zfidStr
|
||||
}
|
||||
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f != nil {
|
||||
return &f.Base().ModTime, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Images is deprecated, slow and shouldn't be used
|
||||
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret []*models.Image, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
// #2376 - sort images by path
|
||||
// doing this via Query is really slow, so stick with FindByGalleryID
|
||||
ret, err = r.repository.Image.FindByGalleryID(ctx, obj.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -164,26 +86,6 @@ func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*strin
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (string, error) {
|
||||
if !obj.Files.PrimaryLoaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadPrimaryFile(ctx, r.repository.File)
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return obj.PrimaryChecksum(), nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Rating100(ctx context.Context, obj *models.Gallery) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
@@ -260,3 +162,57 @@ func (r *galleryResolver) Chapters(ctx context.Context, obj *models.Gallery) (re
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) URL(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Gallery)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
urls := obj.URLs.List()
|
||||
if len(urls) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &urls[0], nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Urls(ctx context.Context, obj *models.Gallery) ([]string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Gallery)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return obj.URLs.List(), nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Paths(ctx context.Context, obj *models.Gallery) (*GalleryPathsType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
builder := urlbuilders.NewGalleryURLBuilder(baseURL, obj)
|
||||
|
||||
return &GalleryPathsType{
|
||||
Cover: builder.GetCoverURL(),
|
||||
Preview: builder.GetPreviewURL(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Image(ctx context.Context, obj *models.Gallery, index int) (ret *models.Image, err error) {
|
||||
if index < 0 {
|
||||
return nil, fmt.Errorf("index must >= 0")
|
||||
}
|
||||
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Image.FindByGalleryIDIndex(ctx, obj.ID, uint(index))
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,58 +2,13 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func convertImageFile(f *file.ImageFile) *ImageFile {
|
||||
ret := &ImageFile{
|
||||
ID: strconv.Itoa(int(f.ID)),
|
||||
Path: f.Path,
|
||||
Basename: f.Basename,
|
||||
ParentFolderID: strconv.Itoa(int(f.ParentFolderID)),
|
||||
ModTime: f.ModTime,
|
||||
Size: f.Size,
|
||||
Width: f.Width,
|
||||
Height: f.Height,
|
||||
CreatedAt: f.CreatedAt,
|
||||
UpdatedAt: f.UpdatedAt,
|
||||
Fingerprints: resolveFingerprints(f.Base()),
|
||||
}
|
||||
|
||||
if f.ZipFileID != nil {
|
||||
zipFileID := strconv.Itoa(int(*f.ZipFileID))
|
||||
ret.ZipFileID = &zipFileID
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *imageResolver) getPrimaryFile(ctx context.Context, obj *models.Image) (file.VisualFile, error) {
|
||||
if obj.PrimaryFileID != nil {
|
||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
asFrame, ok := f.(file.VisualFile)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file %T is not an frame", f)
|
||||
}
|
||||
|
||||
return asFrame, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) getFiles(ctx context.Context, obj *models.Image) ([]file.File, error) {
|
||||
func (r *imageResolver) getFiles(ctx context.Context, obj *models.Image) ([]models.File, error) {
|
||||
fileIDs, err := loaders.From(ctx).ImageFiles.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -68,50 +23,21 @@ func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string,
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFileType, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
width := f.GetWidth()
|
||||
height := f.GetHeight()
|
||||
size := f.Base().Size
|
||||
return &ImageFileType{
|
||||
Size: int(size),
|
||||
Width: width,
|
||||
Height: height,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertVisualFile(f file.File) VisualFile {
|
||||
switch f := f.(type) {
|
||||
case *file.ImageFile:
|
||||
return convertImageFile(f)
|
||||
case *file.VideoFile:
|
||||
return convertVideoFile(f)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown file type %T", f))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *imageResolver) VisualFiles(ctx context.Context, obj *models.Image) ([]VisualFile, error) {
|
||||
fileIDs, err := loaders.From(ctx).ImageFiles.Load(obj.ID)
|
||||
files, err := r.getFiles(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
|
||||
ret := make([]VisualFile, len(files))
|
||||
for i, f := range files {
|
||||
ret[i] = convertVisualFile(f)
|
||||
ret[i], err = convertVisualFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, firstError(errs)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Date(ctx context.Context, obj *models.Image) (*string, error) {
|
||||
@@ -132,31 +58,19 @@ func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageF
|
||||
|
||||
for _, f := range files {
|
||||
// filter out non-image files
|
||||
imageFile, ok := f.(*file.ImageFile)
|
||||
imageFile, ok := f.(*models.ImageFile)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
thisFile := convertImageFile(imageFile)
|
||||
|
||||
ret = append(ret, thisFile)
|
||||
ret = append(ret, &ImageFile{
|
||||
ImageFile: imageFile,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) FileModTime(ctx context.Context, obj *models.Image) (*time.Time, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f != nil {
|
||||
return &f.Base().ModTime, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*ImagePathsType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
builder := urlbuilders.NewImageURLBuilder(baseURL, obj)
|
||||
@@ -184,14 +98,6 @@ func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret [
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Rating100(ctx context.Context, obj *models.Image) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
@@ -231,3 +137,32 @@ func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret
|
||||
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *imageResolver) URL(ctx context.Context, obj *models.Image) (*string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Image)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
urls := obj.URLs.List()
|
||||
if len(urls) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &urls[0], nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Urls(ctx context.Context, obj *models.Image) ([]string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Image)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return obj.URLs.List(), nil
|
||||
}
|
||||
|
||||
@@ -5,16 +5,12 @@ import (
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/hash/md5"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
)
|
||||
|
||||
func (r *movieResolver) Checksum(ctx context.Context, obj *models.Movie) (string, error) {
|
||||
// generate checksum from movie name
|
||||
return md5.FromString(obj.Name), nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
func (r *groupResolver) Date(ctx context.Context, obj *models.Group) (*string, error) {
|
||||
if obj.Date != nil {
|
||||
result := obj.Date.String()
|
||||
return &result, nil
|
||||
@@ -22,19 +18,40 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) {
|
||||
func (r *groupResolver) Rating100(ctx context.Context, obj *models.Group) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *models.Studio, err error) {
|
||||
func (r *groupResolver) URL(ctx context.Context, obj *models.Group) (*string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Group)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
urls := obj.URLs.List()
|
||||
if len(urls) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &urls[0], nil
|
||||
}
|
||||
|
||||
func (r *groupResolver) Urls(ctx context.Context, obj *models.Group) ([]string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Group)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return obj.URLs.List(), nil
|
||||
}
|
||||
|
||||
func (r *groupResolver) Studio(ctx context.Context, obj *models.Group) (ret *models.Studio, err error) {
|
||||
if obj.StudioID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -42,26 +59,102 @@ func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *mod
|
||||
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
|
||||
}
|
||||
|
||||
func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
func (r groupResolver) Tags(ctx context.Context, obj *models.Group) (ret []*models.Tag, err error) {
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadTagIDs(ctx, r.repository.Group)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r groupResolver) relatedGroups(ctx context.Context, rgd models.RelatedGroupDescriptions) (ret []*GroupDescription, err error) {
|
||||
// rgd must be loaded
|
||||
gds := rgd.List()
|
||||
ids := make([]int, len(gds))
|
||||
for i, gd := range gds {
|
||||
ids[i] = gd.GroupID
|
||||
}
|
||||
|
||||
groups, errs := loaders.From(ctx).GroupByID.LoadAll(ids)
|
||||
|
||||
err = firstError(errs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ret = make([]*GroupDescription, len(groups))
|
||||
for i, group := range groups {
|
||||
ret[i] = &GroupDescription{Group: group}
|
||||
d := gds[i].Description
|
||||
if d != "" {
|
||||
ret[i].Description = &d
|
||||
}
|
||||
}
|
||||
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r groupResolver) ContainingGroups(ctx context.Context, obj *models.Group) (ret []*GroupDescription, err error) {
|
||||
if !obj.ContainingGroups.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadContainingGroupIDs(ctx, r.repository.Group)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return r.relatedGroups(ctx, obj.ContainingGroups)
|
||||
}
|
||||
|
||||
func (r groupResolver) SubGroups(ctx context.Context, obj *models.Group) (ret []*GroupDescription, err error) {
|
||||
if !obj.SubGroups.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadSubGroupIDs(ctx, r.repository.Group)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return r.relatedGroups(ctx, obj.SubGroups)
|
||||
}
|
||||
|
||||
func (r *groupResolver) SubGroupCount(ctx context.Context, obj *models.Group, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = group.CountByContainingGroupID(ctx, r.repository.Group, obj.ID, depth)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *groupResolver) FrontImagePath(ctx context.Context, obj *models.Group) (*string, error) {
|
||||
var hasImage bool
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
hasImage, err = r.repository.Movie.HasFrontImage(ctx, obj.ID)
|
||||
hasImage, err = r.repository.Group.HasFrontImage(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
imagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieFrontImageURL(hasImage)
|
||||
imagePath := urlbuilders.NewGroupURLBuilder(baseURL, obj).GetGroupFrontImageURL(hasImage)
|
||||
return &imagePath, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||
func (r *groupResolver) BackImagePath(ctx context.Context, obj *models.Group) (*string, error) {
|
||||
var hasImage bool
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
hasImage, err = r.repository.Movie.HasBackImage(ctx, obj.ID)
|
||||
hasImage, err = r.repository.Group.HasBackImage(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -73,13 +166,13 @@ func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*
|
||||
}
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
imagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieBackImageURL()
|
||||
imagePath := urlbuilders.NewGroupURLBuilder(baseURL, obj).GetGroupBackImageURL()
|
||||
return &imagePath, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret int, err error) {
|
||||
func (r *groupResolver) SceneCount(ctx context.Context, obj *models.Group, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Scene.CountByMovieID(ctx, obj.ID)
|
||||
ret, err = scene.CountByGroupID(ctx, r.repository.Scene, obj.ID, depth)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -88,10 +181,10 @@ func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *movieResolver) Scenes(ctx context.Context, obj *models.Movie) (ret []*models.Scene, err error) {
|
||||
func (r *groupResolver) Scenes(ctx context.Context, obj *models.Group) (ret []*models.Scene, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = r.repository.Scene.FindByMovieID(ctx, obj.ID)
|
||||
ret, err = r.repository.Scene.FindByGroupID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -3,7 +3,6 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
@@ -13,24 +12,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
)
|
||||
|
||||
// Checksum is deprecated
|
||||
func (r *performerResolver) Checksum(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Aliases(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if !obj.Aliases.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadAliases(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ret := strings.Join(obj.Aliases.List(), ", ")
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer) ([]string, error) {
|
||||
if !obj.Aliases.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -43,6 +24,79 @@ func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer
|
||||
return obj.Aliases.List(), nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
urls := obj.URLs.List()
|
||||
if len(urls) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &urls[0], nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
urls := obj.URLs.List()
|
||||
|
||||
// find the first twitter url
|
||||
for _, url := range urls {
|
||||
if performer.IsTwitterURL(url) {
|
||||
u := url
|
||||
return &u, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Instagram(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
urls := obj.URLs.List()
|
||||
|
||||
// find the first instagram url
|
||||
for _, url := range urls {
|
||||
if performer.IsInstagramURL(url) {
|
||||
u := url
|
||||
return &u, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Urls(ctx context.Context, obj *models.Performer) ([]string, error) {
|
||||
if !obj.URLs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadURLs(ctx, r.repository.Performer)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return obj.URLs.List(), nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Height != nil {
|
||||
ret := strconv.Itoa(*obj.Height)
|
||||
@@ -125,9 +179,9 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||
func (r *performerResolver) GroupCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
|
||||
ret, err = r.repository.Group.CountByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -136,6 +190,11 @@ func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performe
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||
return r.GroupCount(ctx, obj)
|
||||
}
|
||||
|
||||
func (r *performerResolver) PerformerCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = performer.CountByAppearsWith(ctx, r.repository.Performer, obj.ID)
|
||||
@@ -186,14 +245,6 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer)
|
||||
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Rating100(ctx context.Context, obj *models.Performer) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
@@ -206,9 +257,9 @@ func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
|
||||
func (r *performerResolver) Groups(ctx context.Context, obj *models.Performer) (ret []*models.Group, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
|
||||
ret, err = r.repository.Group.FindByPerformerID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -216,3 +267,8 @@ func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Group, err error) {
|
||||
return r.Groups(ctx, obj)
|
||||
}
|
||||
|
||||
61
internal/api/resolver_model_plugin.go
Normal file
61
internal/api/resolver_model_plugin.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
)
|
||||
|
||||
type pluginURLBuilder struct {
|
||||
BaseURL string
|
||||
Plugin *plugin.Plugin
|
||||
}
|
||||
|
||||
func (b pluginURLBuilder) javascript() []string {
|
||||
ui := b.Plugin.UI
|
||||
if len(ui.Javascript) == 0 && len(ui.ExternalScript) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ret []string
|
||||
|
||||
ret = append(ret, ui.ExternalScript...)
|
||||
ret = append(ret, b.BaseURL+"/plugin/"+b.Plugin.ID+"/javascript")
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (b pluginURLBuilder) css() []string {
|
||||
ui := b.Plugin.UI
|
||||
if len(ui.CSS) == 0 && len(ui.ExternalCSS) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ret []string
|
||||
|
||||
ret = append(ret, b.Plugin.UI.ExternalCSS...)
|
||||
ret = append(ret, b.BaseURL+"/plugin/"+b.Plugin.ID+"/css")
|
||||
return ret
|
||||
}
|
||||
|
||||
func (b *pluginURLBuilder) paths() *PluginPaths {
|
||||
return &PluginPaths{
|
||||
Javascript: b.javascript(),
|
||||
CSS: b.css(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *pluginResolver) Paths(ctx context.Context, obj *plugin.Plugin) (*PluginPaths, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
b := pluginURLBuilder{
|
||||
BaseURL: baseURL,
|
||||
Plugin: obj,
|
||||
}
|
||||
|
||||
return b.paths(), nil
|
||||
}
|
||||
|
||||
func (r *pluginResolver) Requires(ctx context.Context, obj *plugin.Plugin) ([]string, error) {
|
||||
return obj.UI.Requires, nil
|
||||
}
|
||||
11
internal/api/resolver_model_saved_filter.go
Normal file
11
internal/api/resolver_model_saved_filter.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *savedFilterResolver) Filter(ctx context.Context, obj *models.SavedFilter) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
@@ -3,56 +3,32 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func convertVideoFile(f *file.VideoFile) *VideoFile {
|
||||
ret := &VideoFile{
|
||||
ID: strconv.Itoa(int(f.ID)),
|
||||
Path: f.Path,
|
||||
Basename: f.Basename,
|
||||
ParentFolderID: strconv.Itoa(int(f.ParentFolderID)),
|
||||
ModTime: f.ModTime,
|
||||
Format: f.Format,
|
||||
Size: f.Size,
|
||||
Duration: handleFloat64Value(f.Duration),
|
||||
VideoCodec: f.VideoCodec,
|
||||
AudioCodec: f.AudioCodec,
|
||||
Width: f.Width,
|
||||
Height: f.Height,
|
||||
FrameRate: handleFloat64Value(f.FrameRate),
|
||||
BitRate: int(f.BitRate),
|
||||
CreatedAt: f.CreatedAt,
|
||||
UpdatedAt: f.UpdatedAt,
|
||||
Fingerprints: resolveFingerprints(f.Base()),
|
||||
func convertVideoFile(f models.File) (*models.VideoFile, error) {
|
||||
vf, ok := f.(*models.VideoFile)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file %T is not a video file", f)
|
||||
}
|
||||
|
||||
if f.ZipFileID != nil {
|
||||
zipFileID := strconv.Itoa(int(*f.ZipFileID))
|
||||
ret.ZipFileID = &zipFileID
|
||||
}
|
||||
|
||||
return ret
|
||||
return vf, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (*file.VideoFile, error) {
|
||||
func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (*models.VideoFile, error) {
|
||||
if obj.PrimaryFileID != nil {
|
||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret, ok := f.(*file.VideoFile)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file %T is not an image file", f)
|
||||
ret, err := convertVideoFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj.Files.SetPrimary(ret)
|
||||
@@ -65,38 +41,29 @@ func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) getFiles(ctx context.Context, obj *models.Scene) ([]*file.VideoFile, error) {
|
||||
func (r *sceneResolver) getFiles(ctx context.Context, obj *models.Scene) ([]*models.VideoFile, error) {
|
||||
fileIDs, err := loaders.From(ctx).SceneFiles.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
|
||||
ret := make([]*file.VideoFile, len(files))
|
||||
for i, bf := range files {
|
||||
f, ok := bf.(*file.VideoFile)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file %T is not a video file", f)
|
||||
}
|
||||
|
||||
ret[i] = f
|
||||
}
|
||||
|
||||
obj.Files.Set(ret)
|
||||
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *sceneResolver) FileModTime(ctx context.Context, obj *models.Scene) (*time.Time, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
err = firstError(errs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f != nil {
|
||||
return &f.ModTime, nil
|
||||
ret := make([]*models.VideoFile, len(files))
|
||||
for i, f := range files {
|
||||
ret[i], err = convertVideoFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
obj.Files.Set(ret)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Date(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
@@ -107,31 +74,6 @@ func (r *sceneResolver) Date(ctx context.Context, obj *models.Scene) (*string, e
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// File is deprecated
|
||||
func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.SceneFileType, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
bitrate := int(f.BitRate)
|
||||
size := strconv.FormatInt(f.Size, 10)
|
||||
|
||||
return &models.SceneFileType{
|
||||
Size: &size,
|
||||
Duration: handleFloat64(f.Duration),
|
||||
VideoCodec: &f.VideoCodec,
|
||||
AudioCodec: &f.AudioCodec,
|
||||
Width: &f.Width,
|
||||
Height: &f.Height,
|
||||
Framerate: handleFloat64(f.FrameRate),
|
||||
Bitrate: &bitrate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoFile, error) {
|
||||
files, err := r.getFiles(ctx, obj)
|
||||
if err != nil {
|
||||
@@ -141,7 +83,9 @@ func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoF
|
||||
ret := make([]*VideoFile, len(files))
|
||||
|
||||
for i, f := range files {
|
||||
ret[i] = convertVideoFile(f)
|
||||
ret[i] = &VideoFile{
|
||||
VideoFile: f,
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
@@ -159,28 +103,6 @@ func (r *sceneResolver) Rating100(ctx context.Context, obj *models.Scene) (*int,
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func resolveFingerprints(f *file.BaseFile) []*Fingerprint {
|
||||
ret := make([]*Fingerprint, len(f.Fingerprints))
|
||||
|
||||
for i, fp := range f.Fingerprints {
|
||||
ret[i] = &Fingerprint{
|
||||
Type: fp.Type,
|
||||
Value: formatFingerprint(fp.Fingerprint),
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func formatFingerprint(fp interface{}) string {
|
||||
switch v := fp.(type) {
|
||||
case int64:
|
||||
return strconv.FormatUint(uint64(v), 16)
|
||||
default:
|
||||
return fmt.Sprintf("%v", fp)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePathsType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
config := manager.GetInstance().Config
|
||||
@@ -192,7 +114,6 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
|
||||
objHash := obj.GetHash(config.GetVideoFileNamingAlgorithm())
|
||||
vttPath := builder.GetSpriteVTTURL(objHash)
|
||||
spritePath := builder.GetSpriteURL(objHash)
|
||||
chaptersVttPath := builder.GetChaptersVTTURL()
|
||||
funscriptPath := builder.GetFunscriptURL()
|
||||
captionBasePath := builder.GetCaptionURL()
|
||||
interactiveHeatmap := builder.GetInteractiveHeatmapURL()
|
||||
@@ -203,7 +124,6 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
|
||||
Stream: &streamPath,
|
||||
Webp: &webpPath,
|
||||
Vtt: &vttPath,
|
||||
ChaptersVtt: &chaptersVttPath,
|
||||
Sprite: &spritePath,
|
||||
Funscript: &funscriptPath,
|
||||
InteractiveHeatmap: &interactiveHeatmap,
|
||||
@@ -264,20 +184,20 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) {
|
||||
if !obj.Movies.Loaded() {
|
||||
if !obj.Groups.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
return obj.LoadMovies(ctx, qb)
|
||||
return obj.LoadGroups(ctx, qb)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
loader := loaders.From(ctx).MovieByID
|
||||
loader := loaders.From(ctx).GroupByID
|
||||
|
||||
for _, sm := range obj.Movies.List() {
|
||||
movie, err := loader.Load(sm.MovieID)
|
||||
for _, sm := range obj.Groups.List() {
|
||||
movie, err := loader.Load(sm.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -294,6 +214,37 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Groups(ctx context.Context, obj *models.Scene) (ret []*SceneGroup, err error) {
|
||||
if !obj.Groups.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Scene
|
||||
|
||||
return obj.LoadGroups(ctx, qb)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
loader := loaders.From(ctx).GroupByID
|
||||
|
||||
for _, sm := range obj.Groups.List() {
|
||||
group, err := loader.Load(sm.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sceneIdx := sm.SceneIndex
|
||||
sceneGroup := &SceneGroup{
|
||||
Group: group,
|
||||
SceneIndex: sceneIdx,
|
||||
}
|
||||
|
||||
ret = append(ret, sceneGroup)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -322,16 +273,6 @@ func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID {
|
||||
ret := make([]*models.StashID, len(v))
|
||||
for i, vv := range v {
|
||||
c := vv
|
||||
ret[i] = &c
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadStashIDs(ctx, r.repository.Scene)
|
||||
@@ -342,30 +283,6 @@ func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []
|
||||
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) Phash(ctx context.Context, obj *models.Scene) (*string, error) {
|
||||
f, err := r.getPrimaryFile(ctx, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
val := f.Fingerprints.Get(file.FingerprintTypePhash)
|
||||
if val == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
phash, _ := val.(int64)
|
||||
|
||||
if phash != 0 {
|
||||
hexval := utils.PhashToString(phash)
|
||||
return &hexval, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) SceneStreams(ctx context.Context, obj *models.Scene) ([]*manager.SceneStreamEndpoint, error) {
|
||||
// load the primary file into the scene
|
||||
_, err := r.getPrimaryFile(ctx, obj)
|
||||
@@ -434,3 +351,62 @@ func (r *sceneResolver) Urls(ctx context.Context, obj *models.Scene) ([]string,
|
||||
|
||||
return obj.URLs.List(), nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) OCounter(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
ret, err := loaders.From(ctx).SceneOCount.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) LastPlayedAt(ctx context.Context, obj *models.Scene) (*time.Time, error) {
|
||||
ret, err := loaders.From(ctx).SceneLastPlayed.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) PlayCount(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||
ret, err := loaders.From(ctx).ScenePlayCount.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) PlayHistory(ctx context.Context, obj *models.Scene) ([]*time.Time, error) {
|
||||
ret, err := loaders.From(ctx).ScenePlayHistory.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert to pointer slice
|
||||
ptrRet := make([]*time.Time, len(ret))
|
||||
for i, t := range ret {
|
||||
tt := t
|
||||
ptrRet[i] = &tt
|
||||
}
|
||||
|
||||
return ptrRet, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) OHistory(ctx context.Context, obj *models.Scene) ([]*time.Time, error) {
|
||||
ret, err := loaders.From(ctx).SceneOHistory.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert to pointer slice
|
||||
ptrRet := make([]*time.Time, len(ret))
|
||||
for i, t := range ret {
|
||||
tt := t
|
||||
ptrRet[i] = &tt
|
||||
}
|
||||
|
||||
return ptrRet, nil
|
||||
}
|
||||
|
||||
@@ -6,19 +6,13 @@ import (
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/hash/md5"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/movie"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
)
|
||||
|
||||
func (r *studioResolver) Checksum(ctx context.Context, obj *models.Studio) (string, error) {
|
||||
// generate checksum from studio name
|
||||
return md5.FromString(obj.Name), nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*string, error) {
|
||||
var hasImage bool
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -46,6 +40,20 @@ func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) ([]str
|
||||
return obj.Aliases.List(), nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Tags(ctx context.Context, obj *models.Studio) (ret []*models.Tag, err error) {
|
||||
if !obj.TagIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadTagIDs(ctx, r.repository.Studio)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = scene.CountByStudioID(ctx, r.repository.Scene, obj.ID, depth)
|
||||
@@ -90,9 +98,9 @@ func (r *studioResolver) PerformerCount(ctx context.Context, obj *models.Studio,
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
||||
func (r *studioResolver) GroupCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = movie.CountByStudioID(ctx, r.repository.Movie, obj.ID, depth)
|
||||
ret, err = group.CountByStudioID(ctx, r.repository.Group, obj.ID, depth)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
@@ -101,6 +109,11 @@ func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, dep
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
||||
return r.GroupCount(ctx, obj, depth)
|
||||
}
|
||||
|
||||
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
|
||||
if obj.ParentID == nil {
|
||||
return nil, nil
|
||||
@@ -132,21 +145,13 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*m
|
||||
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
if obj.Rating != nil {
|
||||
rating := models.Rating100To5(*obj.Rating)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*int, error) {
|
||||
return obj.Rating, nil
|
||||
}
|
||||
|
||||
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
|
||||
func (r *studioResolver) Groups(ctx context.Context, obj *models.Studio) (ret []*models.Group, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
|
||||
ret, err = r.repository.Group.FindByStudioID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -154,3 +159,8 @@ func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Group, err error) {
|
||||
return r.Groups(ctx, obj)
|
||||
}
|
||||
|
||||
@@ -3,45 +3,55 @@ package api
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/studio"
|
||||
)
|
||||
|
||||
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.FindByChildTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.ParentIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadParentIDs(ctx, r.repository.Tag)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.ParentIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.FindByParentTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.ChildIDs.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadChildIDs(ctx, r.repository.Tag)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
var errs []error
|
||||
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.ChildIDs.List())
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []string, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.GetAliases(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
if !obj.Aliases.Loaded() {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
return obj.LoadAliases(ctx, r.repository.Tag)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, err
|
||||
return obj.Aliases.List(), nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||
@@ -99,6 +109,32 @@ func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag, depth
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) StudioCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = studio.CountByTagID(ctx, r.repository.Studio, obj.ID, depth)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) GroupCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = group.CountByTagID(ctx, r.repository.Group, obj.ID, depth)
|
||||
return err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) MovieCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||
return r.GroupCount(ctx, obj, depth)
|
||||
}
|
||||
|
||||
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
|
||||
var hasImage bool
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -113,3 +149,25 @@ func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string,
|
||||
imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj).GetTagImageURL(hasImage)
|
||||
return &imagePath, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) ParentCount(ctx context.Context, obj *models.Tag) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.CountByParentTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) ChildCount(ctx context.Context, obj *models.Tag) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.CountByChildTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -6,12 +6,16 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/internal/manager/task"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
var ErrOverriddenConfig = errors.New("cannot set overridden value")
|
||||
@@ -21,9 +25,60 @@ func (r *mutationResolver) Setup(ctx context.Context, input manager.SetupInput)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) Migrate(ctx context.Context, input manager.MigrateInput) (bool, error) {
|
||||
err := manager.GetInstance().Migrate(ctx, input)
|
||||
return err == nil, err
|
||||
func (r *mutationResolver) DownloadFFMpeg(ctx context.Context) (string, error) {
|
||||
mgr := manager.GetInstance()
|
||||
configDir := mgr.Config.GetConfigPathAbs()
|
||||
|
||||
// don't run if ffmpeg is already installed
|
||||
ffmpegPath := ffmpeg.FindFFMpeg(configDir)
|
||||
ffprobePath := ffmpeg.FindFFProbe(configDir)
|
||||
if ffmpegPath != "" && ffprobePath != "" {
|
||||
return "", fmt.Errorf("ffmpeg and ffprobe already installed at %s and %s", ffmpegPath, ffprobePath)
|
||||
}
|
||||
|
||||
t := &task.DownloadFFmpegJob{
|
||||
ConfigDirectory: configDir,
|
||||
OnComplete: func(ctx context.Context) {
|
||||
// clear the ffmpeg and ffprobe paths
|
||||
logger.Infof("Clearing ffmpeg and ffprobe config paths so they are resolved from the config directory")
|
||||
mgr.Config.SetString(config.FFMpegPath, "")
|
||||
mgr.Config.SetString(config.FFProbePath, "")
|
||||
mgr.RefreshFFMpeg(ctx)
|
||||
mgr.RefreshStreamManager()
|
||||
},
|
||||
}
|
||||
|
||||
jobID := mgr.JobManager.Add(ctx, "Downloading ffmpeg...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) setConfigString(key string, value *string) {
|
||||
c := config.GetInstance()
|
||||
if value != nil {
|
||||
c.SetString(key, *value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) setConfigBool(key string, value *bool) {
|
||||
c := config.GetInstance()
|
||||
if value != nil {
|
||||
c.SetBool(key, *value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) setConfigInt(key string, value *int) {
|
||||
c := config.GetInstance()
|
||||
if value != nil {
|
||||
c.SetInt(key, *value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) setConfigFloat(key string, value *float64) {
|
||||
c := config.GetInstance()
|
||||
if value != nil {
|
||||
c.SetFloat(key, *value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGeneralInput) (*ConfigGeneralResult, error) {
|
||||
@@ -47,7 +102,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Set(config.Stash, input.Stashes)
|
||||
c.SetInterface(config.Stash, input.Stashes)
|
||||
}
|
||||
|
||||
checkConfigOverride := func(key string) error {
|
||||
@@ -82,7 +137,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
if ext != ".db" && ext != ".sqlite" && ext != ".sqlite3" {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("invalid database path, use extension db, sqlite, or sqlite3")
|
||||
}
|
||||
c.Set(config.Database, input.DatabasePath)
|
||||
c.SetString(config.Database, *input.DatabasePath)
|
||||
}
|
||||
|
||||
existingBackupDirectoryPath := c.GetBackupDirectoryPath()
|
||||
@@ -91,7 +146,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.BackupDirectoryPath, input.BackupDirectoryPath)
|
||||
c.SetString(config.BackupDirectoryPath, *input.BackupDirectoryPath)
|
||||
}
|
||||
|
||||
existingGeneratedPath := c.GetGeneratedPath()
|
||||
@@ -100,10 +155,11 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.Generated, input.GeneratedPath)
|
||||
c.SetString(config.Generated, *input.GeneratedPath)
|
||||
}
|
||||
|
||||
refreshScraperCache := false
|
||||
refreshScraperSource := false
|
||||
existingScrapersPath := c.GetScrapersPath()
|
||||
if input.ScrapersPath != nil && existingScrapersPath != *input.ScrapersPath {
|
||||
if err := validateDir(config.ScrapersPath, *input.ScrapersPath, false); err != nil {
|
||||
@@ -111,7 +167,21 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
}
|
||||
|
||||
refreshScraperCache = true
|
||||
c.Set(config.ScrapersPath, input.ScrapersPath)
|
||||
refreshScraperSource = true
|
||||
c.SetString(config.ScrapersPath, *input.ScrapersPath)
|
||||
}
|
||||
|
||||
refreshPluginCache := false
|
||||
refreshPluginSource := false
|
||||
existingPluginsPath := c.GetPluginsPath()
|
||||
if input.PluginsPath != nil && existingPluginsPath != *input.PluginsPath {
|
||||
if err := validateDir(config.PluginsPath, *input.PluginsPath, false); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
refreshPluginCache = true
|
||||
refreshPluginSource = true
|
||||
c.SetString(config.PluginsPath, *input.PluginsPath)
|
||||
}
|
||||
|
||||
existingMetadataPath := c.GetMetadataPath()
|
||||
@@ -120,7 +190,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.Metadata, input.MetadataPath)
|
||||
c.SetString(config.Metadata, *input.MetadataPath)
|
||||
}
|
||||
|
||||
refreshStreamManager := false
|
||||
@@ -130,7 +200,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.Cache, input.CachePath)
|
||||
c.SetString(config.Cache, *input.CachePath)
|
||||
refreshStreamManager = true
|
||||
}
|
||||
|
||||
@@ -141,7 +211,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.BlobsPath, input.BlobsPath)
|
||||
c.SetString(config.BlobsPath, *input.BlobsPath)
|
||||
refreshBlobStorage = true
|
||||
}
|
||||
|
||||
@@ -150,12 +220,34 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), fmt.Errorf("blobs path must be set when using filesystem storage")
|
||||
}
|
||||
|
||||
// TODO - migrate between systems
|
||||
c.Set(config.BlobsStorage, input.BlobsStorage)
|
||||
c.SetInterface(config.BlobsStorage, *input.BlobsStorage)
|
||||
|
||||
refreshBlobStorage = true
|
||||
}
|
||||
|
||||
refreshFfmpeg := false
|
||||
if input.FfmpegPath != nil && *input.FfmpegPath != c.GetFFMpegPath() {
|
||||
if *input.FfmpegPath != "" {
|
||||
if err := ffmpeg.ValidateFFMpeg(*input.FfmpegPath); err != nil {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("invalid ffmpeg path: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.SetString(config.FFMpegPath, *input.FfmpegPath)
|
||||
refreshFfmpeg = true
|
||||
}
|
||||
|
||||
if input.FfprobePath != nil && *input.FfprobePath != c.GetFFProbePath() {
|
||||
if *input.FfprobePath != "" {
|
||||
if err := ffmpeg.ValidateFFProbe(*input.FfprobePath); err != nil {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("invalid ffprobe path: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.SetString(config.FFProbePath, *input.FfprobePath)
|
||||
refreshFfmpeg = true
|
||||
}
|
||||
|
||||
if input.VideoFileNamingAlgorithm != nil && *input.VideoFileNamingAlgorithm != c.GetVideoFileNamingAlgorithm() {
|
||||
calculateMD5 := c.IsCalculateMD5()
|
||||
if input.CalculateMd5 != nil {
|
||||
@@ -172,68 +264,42 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
c.Set(config.VideoFileNamingAlgorithm, *input.VideoFileNamingAlgorithm)
|
||||
c.SetInterface(config.VideoFileNamingAlgorithm, *input.VideoFileNamingAlgorithm)
|
||||
}
|
||||
|
||||
if input.CalculateMd5 != nil {
|
||||
c.Set(config.CalculateMD5, *input.CalculateMd5)
|
||||
}
|
||||
|
||||
if input.ParallelTasks != nil {
|
||||
c.Set(config.ParallelTasks, *input.ParallelTasks)
|
||||
}
|
||||
|
||||
if input.PreviewAudio != nil {
|
||||
c.Set(config.PreviewAudio, *input.PreviewAudio)
|
||||
}
|
||||
|
||||
if input.PreviewSegments != nil {
|
||||
c.Set(config.PreviewSegments, *input.PreviewSegments)
|
||||
}
|
||||
if input.PreviewSegmentDuration != nil {
|
||||
c.Set(config.PreviewSegmentDuration, *input.PreviewSegmentDuration)
|
||||
}
|
||||
if input.PreviewExcludeStart != nil {
|
||||
c.Set(config.PreviewExcludeStart, *input.PreviewExcludeStart)
|
||||
}
|
||||
if input.PreviewExcludeEnd != nil {
|
||||
c.Set(config.PreviewExcludeEnd, *input.PreviewExcludeEnd)
|
||||
}
|
||||
r.setConfigBool(config.CalculateMD5, input.CalculateMd5)
|
||||
r.setConfigInt(config.ParallelTasks, input.ParallelTasks)
|
||||
r.setConfigBool(config.PreviewAudio, input.PreviewAudio)
|
||||
r.setConfigInt(config.PreviewSegments, input.PreviewSegments)
|
||||
r.setConfigFloat(config.PreviewSegmentDuration, input.PreviewSegmentDuration)
|
||||
r.setConfigString(config.PreviewExcludeStart, input.PreviewExcludeStart)
|
||||
r.setConfigString(config.PreviewExcludeEnd, input.PreviewExcludeEnd)
|
||||
if input.PreviewPreset != nil {
|
||||
c.Set(config.PreviewPreset, input.PreviewPreset.String())
|
||||
c.SetString(config.PreviewPreset, input.PreviewPreset.String())
|
||||
}
|
||||
|
||||
if input.TranscodeHardwareAcceleration != nil {
|
||||
c.Set(config.TranscodeHardwareAcceleration, *input.TranscodeHardwareAcceleration)
|
||||
}
|
||||
r.setConfigBool(config.TranscodeHardwareAcceleration, input.TranscodeHardwareAcceleration)
|
||||
if input.MaxTranscodeSize != nil {
|
||||
c.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||
c.SetString(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||
}
|
||||
|
||||
if input.MaxStreamingTranscodeSize != nil {
|
||||
c.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
|
||||
}
|
||||
|
||||
if input.WriteImageThumbnails != nil {
|
||||
c.Set(config.WriteImageThumbnails, *input.WriteImageThumbnails)
|
||||
}
|
||||
|
||||
if input.CreateImageClipsFromVideos != nil {
|
||||
c.Set(config.CreateImageClipsFromVideos, *input.CreateImageClipsFromVideos)
|
||||
c.SetString(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
|
||||
}
|
||||
r.setConfigBool(config.WriteImageThumbnails, input.WriteImageThumbnails)
|
||||
r.setConfigBool(config.CreateImageClipsFromVideos, input.CreateImageClipsFromVideos)
|
||||
|
||||
if input.GalleryCoverRegex != nil {
|
||||
|
||||
_, err := regexp.Compile(*input.GalleryCoverRegex)
|
||||
if err != nil {
|
||||
return makeConfigGeneralResult(), fmt.Errorf("Gallery cover regex '%v' invalid, '%v'", *input.GalleryCoverRegex, err.Error())
|
||||
}
|
||||
|
||||
c.Set(config.GalleryCoverRegex, *input.GalleryCoverRegex)
|
||||
c.SetString(config.GalleryCoverRegex, *input.GalleryCoverRegex)
|
||||
}
|
||||
|
||||
if input.Username != nil && *input.Username != c.GetUsername() {
|
||||
c.Set(config.Username, input.Username)
|
||||
c.SetString(config.Username, *input.Username)
|
||||
if *input.Password == "" {
|
||||
logger.Info("Username cleared")
|
||||
} else {
|
||||
@@ -256,24 +322,13 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
}
|
||||
}
|
||||
|
||||
if input.MaxSessionAge != nil {
|
||||
c.Set(config.MaxSessionAge, *input.MaxSessionAge)
|
||||
}
|
||||
|
||||
if input.LogFile != nil {
|
||||
c.Set(config.LogFile, input.LogFile)
|
||||
}
|
||||
|
||||
if input.LogOut != nil {
|
||||
c.Set(config.LogOut, *input.LogOut)
|
||||
}
|
||||
|
||||
if input.LogAccess != nil {
|
||||
c.Set(config.LogAccess, *input.LogAccess)
|
||||
}
|
||||
r.setConfigInt(config.MaxSessionAge, input.MaxSessionAge)
|
||||
r.setConfigString(config.LogFile, input.LogFile)
|
||||
r.setConfigBool(config.LogOut, input.LogOut)
|
||||
r.setConfigBool(config.LogAccess, input.LogAccess)
|
||||
|
||||
if input.LogLevel != nil && *input.LogLevel != c.GetLogLevel() {
|
||||
c.Set(config.LogLevel, input.LogLevel)
|
||||
c.SetString(config.LogLevel, *input.LogLevel)
|
||||
logger := manager.GetInstance().Logger
|
||||
logger.SetLogLevel(*input.LogLevel)
|
||||
}
|
||||
@@ -285,7 +340,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), fmt.Errorf("video exclusion pattern '%v' invalid: %w", r, err)
|
||||
}
|
||||
}
|
||||
c.Set(config.Exclude, input.Excludes)
|
||||
c.SetInterface(config.Exclude, input.Excludes)
|
||||
}
|
||||
|
||||
if input.ImageExcludes != nil {
|
||||
@@ -295,70 +350,62 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
return makeConfigGeneralResult(), fmt.Errorf("image/gallery exclusion pattern '%v' invalid: %w", r, err)
|
||||
}
|
||||
}
|
||||
c.Set(config.ImageExclude, input.ImageExcludes)
|
||||
c.SetInterface(config.ImageExclude, input.ImageExcludes)
|
||||
}
|
||||
|
||||
if input.VideoExtensions != nil {
|
||||
c.Set(config.VideoExtensions, input.VideoExtensions)
|
||||
c.SetInterface(config.VideoExtensions, input.VideoExtensions)
|
||||
}
|
||||
|
||||
if input.ImageExtensions != nil {
|
||||
c.Set(config.ImageExtensions, input.ImageExtensions)
|
||||
c.SetInterface(config.ImageExtensions, input.ImageExtensions)
|
||||
}
|
||||
|
||||
if input.GalleryExtensions != nil {
|
||||
c.Set(config.GalleryExtensions, input.GalleryExtensions)
|
||||
c.SetInterface(config.GalleryExtensions, input.GalleryExtensions)
|
||||
}
|
||||
|
||||
if input.CreateGalleriesFromFolders != nil {
|
||||
c.Set(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
|
||||
}
|
||||
r.setConfigBool(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
|
||||
|
||||
if input.CustomPerformerImageLocation != nil {
|
||||
c.Set(config.CustomPerformerImageLocation, *input.CustomPerformerImageLocation)
|
||||
initialiseCustomImages()
|
||||
}
|
||||
|
||||
if input.ScraperUserAgent != nil {
|
||||
c.Set(config.ScraperUserAgent, input.ScraperUserAgent)
|
||||
refreshScraperCache = true
|
||||
}
|
||||
|
||||
if input.ScraperCDPPath != nil {
|
||||
c.Set(config.ScraperCDPPath, input.ScraperCDPPath)
|
||||
refreshScraperCache = true
|
||||
}
|
||||
|
||||
if input.ScraperCertCheck != nil {
|
||||
c.Set(config.ScraperCertCheck, input.ScraperCertCheck)
|
||||
c.SetString(config.CustomPerformerImageLocation, *input.CustomPerformerImageLocation)
|
||||
initCustomPerformerImages(*input.CustomPerformerImageLocation)
|
||||
}
|
||||
|
||||
if input.StashBoxes != nil {
|
||||
if err := c.ValidateStashBoxes(input.StashBoxes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Set(config.StashBoxes, input.StashBoxes)
|
||||
c.SetInterface(config.StashBoxes, input.StashBoxes)
|
||||
}
|
||||
|
||||
if input.PythonPath != nil {
|
||||
c.Set(config.PythonPath, input.PythonPath)
|
||||
r.setConfigString(config.PythonPath, input.PythonPath)
|
||||
}
|
||||
|
||||
if input.TranscodeInputArgs != nil {
|
||||
c.Set(config.TranscodeInputArgs, input.TranscodeInputArgs)
|
||||
c.SetInterface(config.TranscodeInputArgs, input.TranscodeInputArgs)
|
||||
}
|
||||
if input.TranscodeOutputArgs != nil {
|
||||
c.Set(config.TranscodeOutputArgs, input.TranscodeOutputArgs)
|
||||
c.SetInterface(config.TranscodeOutputArgs, input.TranscodeOutputArgs)
|
||||
}
|
||||
if input.LiveTranscodeInputArgs != nil {
|
||||
c.Set(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs)
|
||||
c.SetInterface(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs)
|
||||
}
|
||||
if input.LiveTranscodeOutputArgs != nil {
|
||||
c.Set(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs)
|
||||
c.SetInterface(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs)
|
||||
}
|
||||
|
||||
if input.DrawFunscriptHeatmapRange != nil {
|
||||
c.Set(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
|
||||
r.setConfigBool(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
|
||||
|
||||
if input.ScraperPackageSources != nil {
|
||||
c.SetInterface(config.ScraperPackageSources, input.ScraperPackageSources)
|
||||
refreshScraperSource = true
|
||||
}
|
||||
|
||||
if input.PluginPackageSources != nil {
|
||||
c.SetInterface(config.PluginPackageSources, input.PluginPackageSources)
|
||||
refreshPluginSource = true
|
||||
}
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
@@ -369,12 +416,27 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
if refreshScraperCache {
|
||||
manager.GetInstance().RefreshScraperCache()
|
||||
}
|
||||
if refreshPluginCache {
|
||||
manager.GetInstance().RefreshPluginCache()
|
||||
}
|
||||
if refreshFfmpeg {
|
||||
manager.GetInstance().RefreshFFMpeg(ctx)
|
||||
|
||||
// refresh stream manager is required since ffmpeg changed
|
||||
refreshStreamManager = true
|
||||
}
|
||||
if refreshStreamManager {
|
||||
manager.GetInstance().RefreshStreamManager()
|
||||
}
|
||||
if refreshBlobStorage {
|
||||
manager.GetInstance().SetBlobStoreOptions()
|
||||
}
|
||||
if refreshScraperSource {
|
||||
manager.GetInstance().RefreshScraperSourceManager()
|
||||
}
|
||||
if refreshPluginSource {
|
||||
manager.GetInstance().RefreshPluginSourceManager()
|
||||
}
|
||||
|
||||
return makeConfigGeneralResult(), nil
|
||||
}
|
||||
@@ -382,107 +444,70 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
setBool := func(key string, v *bool) {
|
||||
if v != nil {
|
||||
c.Set(key, *v)
|
||||
}
|
||||
}
|
||||
|
||||
setString := func(key string, v *string) {
|
||||
if v != nil {
|
||||
c.Set(key, *v)
|
||||
}
|
||||
}
|
||||
|
||||
if input.MenuItems != nil {
|
||||
c.Set(config.MenuItems, input.MenuItems)
|
||||
c.SetInterface(config.MenuItems, input.MenuItems)
|
||||
}
|
||||
|
||||
setBool(config.SoundOnPreview, input.SoundOnPreview)
|
||||
setBool(config.WallShowTitle, input.WallShowTitle)
|
||||
r.setConfigBool(config.SoundOnPreview, input.SoundOnPreview)
|
||||
r.setConfigBool(config.WallShowTitle, input.WallShowTitle)
|
||||
|
||||
setBool(config.NoBrowser, input.NoBrowser)
|
||||
r.setConfigBool(config.NoBrowser, input.NoBrowser)
|
||||
|
||||
setBool(config.NotificationsEnabled, input.NotificationsEnabled)
|
||||
r.setConfigBool(config.NotificationsEnabled, input.NotificationsEnabled)
|
||||
|
||||
setBool(config.ShowScrubber, input.ShowScrubber)
|
||||
r.setConfigBool(config.ShowScrubber, input.ShowScrubber)
|
||||
|
||||
if input.WallPlayback != nil {
|
||||
c.Set(config.WallPlayback, *input.WallPlayback)
|
||||
}
|
||||
r.setConfigString(config.WallPlayback, input.WallPlayback)
|
||||
r.setConfigInt(config.MaximumLoopDuration, input.MaximumLoopDuration)
|
||||
r.setConfigBool(config.AutostartVideo, input.AutostartVideo)
|
||||
r.setConfigBool(config.ShowStudioAsText, input.ShowStudioAsText)
|
||||
r.setConfigBool(config.AutostartVideoOnPlaySelected, input.AutostartVideoOnPlaySelected)
|
||||
r.setConfigBool(config.ContinuePlaylistDefault, input.ContinuePlaylistDefault)
|
||||
|
||||
if input.MaximumLoopDuration != nil {
|
||||
c.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
|
||||
}
|
||||
|
||||
setBool(config.AutostartVideo, input.AutostartVideo)
|
||||
setBool(config.ShowStudioAsText, input.ShowStudioAsText)
|
||||
setBool(config.AutostartVideoOnPlaySelected, input.AutostartVideoOnPlaySelected)
|
||||
setBool(config.ContinuePlaylistDefault, input.ContinuePlaylistDefault)
|
||||
|
||||
if input.Language != nil {
|
||||
c.Set(config.Language, *input.Language)
|
||||
}
|
||||
|
||||
// deprecated field
|
||||
if input.SlideshowDelay != nil {
|
||||
c.Set(config.ImageLightboxSlideshowDelay, *input.SlideshowDelay)
|
||||
}
|
||||
r.setConfigString(config.Language, input.Language)
|
||||
|
||||
if input.ImageLightbox != nil {
|
||||
options := input.ImageLightbox
|
||||
|
||||
if options.SlideshowDelay != nil {
|
||||
c.Set(config.ImageLightboxSlideshowDelay, *options.SlideshowDelay)
|
||||
}
|
||||
r.setConfigInt(config.ImageLightboxSlideshowDelay, options.SlideshowDelay)
|
||||
|
||||
setString(config.ImageLightboxDisplayModeKey, (*string)(options.DisplayMode))
|
||||
setBool(config.ImageLightboxScaleUp, options.ScaleUp)
|
||||
setBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
|
||||
setString(config.ImageLightboxScrollModeKey, (*string)(options.ScrollMode))
|
||||
r.setConfigString(config.ImageLightboxDisplayModeKey, (*string)(options.DisplayMode))
|
||||
r.setConfigBool(config.ImageLightboxScaleUp, options.ScaleUp)
|
||||
r.setConfigBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
|
||||
r.setConfigString(config.ImageLightboxScrollModeKey, (*string)(options.ScrollMode))
|
||||
|
||||
if options.ScrollAttemptsBeforeChange != nil {
|
||||
c.Set(config.ImageLightboxScrollAttemptsBeforeChange, *options.ScrollAttemptsBeforeChange)
|
||||
}
|
||||
r.setConfigInt(config.ImageLightboxScrollAttemptsBeforeChange, options.ScrollAttemptsBeforeChange)
|
||||
}
|
||||
|
||||
if input.CSS != nil {
|
||||
c.SetCSS(*input.CSS)
|
||||
}
|
||||
|
||||
setBool(config.CSSEnabled, input.CSSEnabled)
|
||||
r.setConfigBool(config.CSSEnabled, input.CSSEnabled)
|
||||
|
||||
if input.Javascript != nil {
|
||||
c.SetJavascript(*input.Javascript)
|
||||
}
|
||||
|
||||
setBool(config.JavascriptEnabled, input.JavascriptEnabled)
|
||||
r.setConfigBool(config.JavascriptEnabled, input.JavascriptEnabled)
|
||||
|
||||
if input.CustomLocales != nil {
|
||||
c.SetCustomLocales(*input.CustomLocales)
|
||||
}
|
||||
|
||||
setBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
|
||||
r.setConfigBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
|
||||
|
||||
if input.DisableDropdownCreate != nil {
|
||||
ddc := input.DisableDropdownCreate
|
||||
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
|
||||
setBool(config.DisableDropdownCreateStudio, ddc.Studio)
|
||||
setBool(config.DisableDropdownCreateTag, ddc.Tag)
|
||||
setBool(config.DisableDropdownCreateMovie, ddc.Movie)
|
||||
r.setConfigBool(config.DisableDropdownCreatePerformer, ddc.Performer)
|
||||
r.setConfigBool(config.DisableDropdownCreateStudio, ddc.Studio)
|
||||
r.setConfigBool(config.DisableDropdownCreateTag, ddc.Tag)
|
||||
r.setConfigBool(config.DisableDropdownCreateMovie, ddc.Movie)
|
||||
}
|
||||
|
||||
if input.HandyKey != nil {
|
||||
c.Set(config.HandyKey, *input.HandyKey)
|
||||
}
|
||||
|
||||
if input.FunscriptOffset != nil {
|
||||
c.Set(config.FunscriptOffset, *input.FunscriptOffset)
|
||||
}
|
||||
|
||||
if input.UseStashHostedFunscript != nil {
|
||||
c.Set(config.UseStashHostedFunscript, *input.UseStashHostedFunscript)
|
||||
}
|
||||
r.setConfigString(config.HandyKey, input.HandyKey)
|
||||
r.setConfigInt(config.FunscriptOffset, input.FunscriptOffset)
|
||||
r.setConfigBool(config.UseStashHostedFunscript, input.UseStashHostedFunscript)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigInterfaceResult(), err
|
||||
@@ -494,41 +519,33 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
|
||||
func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAInput) (*ConfigDLNAResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
if input.ServerName != nil {
|
||||
c.Set(config.DLNAServerName, *input.ServerName)
|
||||
}
|
||||
r.setConfigString(config.DLNAServerName, input.ServerName)
|
||||
|
||||
if input.WhitelistedIPs != nil {
|
||||
c.Set(config.DLNADefaultIPWhitelist, input.WhitelistedIPs)
|
||||
c.SetInterface(config.DLNADefaultIPWhitelist, input.WhitelistedIPs)
|
||||
}
|
||||
|
||||
if input.VideoSortOrder != nil {
|
||||
c.Set(config.DLNAVideoSortOrder, input.VideoSortOrder)
|
||||
}
|
||||
r.setConfigString(config.DLNAVideoSortOrder, input.VideoSortOrder)
|
||||
r.setConfigInt(config.DLNAPort, input.Port)
|
||||
|
||||
currentDLNAEnabled := c.GetDLNADefaultEnabled()
|
||||
if input.Enabled != nil && *input.Enabled != currentDLNAEnabled {
|
||||
c.Set(config.DLNADefaultEnabled, *input.Enabled)
|
||||
|
||||
// start/stop the DLNA service as needed
|
||||
dlnaService := manager.GetInstance().DLNAService
|
||||
if !*input.Enabled && dlnaService.IsRunning() {
|
||||
dlnaService.Stop(nil)
|
||||
} else if *input.Enabled && !dlnaService.IsRunning() {
|
||||
if err := dlnaService.Start(nil); err != nil {
|
||||
logger.Warnf("error starting DLNA service: %v", err)
|
||||
}
|
||||
}
|
||||
refresh := false
|
||||
if input.Enabled != nil {
|
||||
c.SetBool(config.DLNADefaultEnabled, *input.Enabled)
|
||||
refresh = true
|
||||
}
|
||||
|
||||
if input.Interfaces != nil {
|
||||
c.Set(config.DLNAInterfaces, input.Interfaces)
|
||||
c.SetInterface(config.DLNAInterfaces, input.Interfaces)
|
||||
}
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigDLNAResult(), err
|
||||
}
|
||||
|
||||
if refresh {
|
||||
manager.GetInstance().RefreshDLNA()
|
||||
}
|
||||
|
||||
return makeConfigDLNAResult(), nil
|
||||
}
|
||||
|
||||
@@ -537,12 +554,12 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigSc
|
||||
|
||||
refreshScraperCache := false
|
||||
if input.ScraperUserAgent != nil {
|
||||
c.Set(config.ScraperUserAgent, input.ScraperUserAgent)
|
||||
c.SetString(config.ScraperUserAgent, *input.ScraperUserAgent)
|
||||
refreshScraperCache = true
|
||||
}
|
||||
|
||||
if input.ScraperCDPPath != nil {
|
||||
c.Set(config.ScraperCDPPath, input.ScraperCDPPath)
|
||||
c.SetString(config.ScraperCDPPath, *input.ScraperCDPPath)
|
||||
refreshScraperCache = true
|
||||
}
|
||||
|
||||
@@ -553,12 +570,10 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigSc
|
||||
return makeConfigScrapingResult(), fmt.Errorf("tag exclusion pattern '%v' invalid: %w", r, err)
|
||||
}
|
||||
}
|
||||
c.Set(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns)
|
||||
c.SetInterface(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns)
|
||||
}
|
||||
|
||||
if input.ScraperCertCheck != nil {
|
||||
c.Set(config.ScraperCertCheck, input.ScraperCertCheck)
|
||||
}
|
||||
r.setConfigBool(config.ScraperCertCheck, input.ScraperCertCheck)
|
||||
|
||||
if refreshScraperCache {
|
||||
manager.GetInstance().RefreshScraperCache()
|
||||
@@ -574,30 +589,25 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input ConfigDe
|
||||
c := config.GetInstance()
|
||||
|
||||
if input.Identify != nil {
|
||||
c.Set(config.DefaultIdentifySettings, input.Identify)
|
||||
c.SetInterface(config.DefaultIdentifySettings, input.Identify)
|
||||
}
|
||||
|
||||
if input.Scan != nil {
|
||||
// if input.Scan is used then ScanMetadataOptions is included in the config file
|
||||
// this causes the values to not be read correctly
|
||||
c.Set(config.DefaultScanSettings, input.Scan.ScanMetadataOptions)
|
||||
c.SetInterface(config.DefaultScanSettings, input.Scan.ScanMetadataOptions)
|
||||
}
|
||||
|
||||
if input.AutoTag != nil {
|
||||
c.Set(config.DefaultAutoTagSettings, input.AutoTag)
|
||||
c.SetInterface(config.DefaultAutoTagSettings, input.AutoTag)
|
||||
}
|
||||
|
||||
if input.Generate != nil {
|
||||
c.Set(config.DefaultGenerateSettings, input.Generate)
|
||||
c.SetInterface(config.DefaultGenerateSettings, input.Generate)
|
||||
}
|
||||
|
||||
if input.DeleteFile != nil {
|
||||
c.Set(config.DeleteFileDefault, *input.DeleteFile)
|
||||
}
|
||||
|
||||
if input.DeleteGenerated != nil {
|
||||
c.Set(config.DeleteGeneratedDefault, *input.DeleteGenerated)
|
||||
}
|
||||
r.setConfigBool(config.DeleteFileDefault, input.DeleteFile)
|
||||
r.setConfigBool(config.DeleteGeneratedDefault, input.DeleteGenerated)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigDefaultsResult(), err
|
||||
@@ -621,7 +631,7 @@ func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input GenerateAPI
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(config.ApiKey, newAPIKey)
|
||||
c.SetString(config.ApiKey, newAPIKey)
|
||||
if err := c.Write(); err != nil {
|
||||
return newAPIKey, err
|
||||
}
|
||||
@@ -629,9 +639,19 @@ func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input GenerateAPI
|
||||
return newAPIKey, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
|
||||
func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]interface{}, partial map[string]interface{}) (map[string]interface{}, error) {
|
||||
c := config.GetInstance()
|
||||
c.SetUIConfiguration(input)
|
||||
|
||||
if input != nil {
|
||||
c.SetUIConfiguration(input)
|
||||
}
|
||||
|
||||
if partial != nil {
|
||||
// merge partial into existing config
|
||||
existing := c.GetUIConfiguration()
|
||||
utils.MergeMaps(existing, partial)
|
||||
c.SetUIConfiguration(existing)
|
||||
}
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return c.GetUIConfiguration(), err
|
||||
@@ -643,8 +663,19 @@ func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]int
|
||||
func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, value interface{}) (map[string]interface{}, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
cfg := c.GetUIConfiguration()
|
||||
cfg[key] = value
|
||||
cfg := utils.NestedMap(c.GetUIConfiguration())
|
||||
cfg.Set(key, value)
|
||||
|
||||
return r.ConfigureUI(ctx, cfg)
|
||||
return r.ConfigureUI(ctx, cfg, nil)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigurePlugin(ctx context.Context, pluginID string, input map[string]interface{}) (map[string]interface{}, error) {
|
||||
c := config.GetInstance()
|
||||
c.SetPluginConfiguration(pluginID, input)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return c.GetPluginConfiguration(pluginID), err
|
||||
}
|
||||
|
||||
return c.GetPluginConfiguration(pluginID), nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
@@ -16,16 +17,16 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
|
||||
fileStore := r.repository.File
|
||||
folderStore := r.repository.Folder
|
||||
mover := file.NewMover(fileStore, folderStore)
|
||||
mover.RegisterHooks(ctx, r.txnManager)
|
||||
mover.RegisterHooks(ctx)
|
||||
|
||||
var (
|
||||
folder *file.Folder
|
||||
folder *models.Folder
|
||||
basename string
|
||||
)
|
||||
|
||||
fileIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return fmt.Errorf("converting file ids: %w", err)
|
||||
return fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
@@ -34,10 +35,10 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
|
||||
|
||||
folderID, err := strconv.Atoi(*input.DestinationFolderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid folder id %s: %w", *input.DestinationFolderID, err)
|
||||
return fmt.Errorf("converting destination folder id: %w", err)
|
||||
}
|
||||
|
||||
folder, err = folderStore.Find(ctx, file.FolderID(folderID))
|
||||
folder, err = folderStore.Find(ctx, models.FolderID(folderID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding destination folder: %w", err)
|
||||
}
|
||||
@@ -82,7 +83,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
|
||||
}
|
||||
|
||||
for _, fileIDInt := range fileIDs {
|
||||
fileID := file.ID(fileIDInt)
|
||||
fileID := models.FileID(fileIDInt)
|
||||
f, err := fileStore.Find(ctx, fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding file %d: %w", fileID, err)
|
||||
@@ -145,7 +146,7 @@ func (r *mutationResolver) validateFileExtensionList(exts []string, oldBasename,
|
||||
func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret bool, err error) {
|
||||
fileIDs, err := stringslice.StringSliceToIntSlice(ids)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
fileDeleter := file.NewDeleter()
|
||||
@@ -158,7 +159,7 @@ func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret b
|
||||
qb := r.repository.File
|
||||
|
||||
for _, fileIDInt := range fileIDs {
|
||||
fileID := file.ID(fileIDInt)
|
||||
fileID := models.FileID(fileIDInt)
|
||||
f, err := qb.Find(ctx, fileID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -206,3 +207,68 @@ func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret b
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) FileSetFingerprints(ctx context.Context, input FileSetFingerprintsInput) (bool, error) {
|
||||
fileIDInt, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
fileID := models.FileID(fileIDInt)
|
||||
|
||||
// determine what we're doing
|
||||
var (
|
||||
fingerprints []models.Fingerprint
|
||||
toDelete []string
|
||||
)
|
||||
|
||||
for _, i := range input.Fingerprints {
|
||||
if i.Type == models.FingerprintTypeMD5 || i.Type == models.FingerprintTypeOshash {
|
||||
return false, fmt.Errorf("cannot modify %s fingerprint", i.Type)
|
||||
}
|
||||
|
||||
if i.Value == nil {
|
||||
toDelete = append(toDelete, i.Type)
|
||||
} else {
|
||||
// phashes need to be converted from string into uint64
|
||||
var v interface{}
|
||||
v = *i.Value
|
||||
|
||||
if i.Type == models.FingerprintTypePhash {
|
||||
vInt, err := strconv.ParseUint(*i.Value, 16, 64)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting phash %s: %w", *i.Value, err)
|
||||
}
|
||||
|
||||
v = vInt
|
||||
}
|
||||
|
||||
fingerprints = append(fingerprints, models.Fingerprint{
|
||||
Type: i.Type,
|
||||
Fingerprint: v,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.File
|
||||
|
||||
if len(fingerprints) > 0 {
|
||||
if err := qb.ModifyFingerprints(ctx, fileID, fingerprints); err != nil {
|
||||
return fmt.Errorf("modifying fingerprints: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(toDelete) > 0 {
|
||||
if err := qb.DestroyFingerprints(ctx, fileID, toDelete); err != nil {
|
||||
return fmt.Errorf("destroying fingerprints: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
@@ -14,10 +13,12 @@ import (
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
// used to refetch gallery after hooks run
|
||||
func (r *mutationResolver) getGallery(ctx context.Context, id int) (ret *models.Gallery, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Gallery.Find(ctx, id)
|
||||
@@ -39,40 +40,43 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
performerIDs, err := stringslice.StringSliceToIntSlice(input.PerformerIds)
|
||||
// Populate a new gallery from the input
|
||||
newGallery := models.NewGallery()
|
||||
|
||||
newGallery.Title = input.Title
|
||||
newGallery.Code = translator.string(input.Code)
|
||||
newGallery.Details = translator.string(input.Details)
|
||||
newGallery.Photographer = translator.string(input.Photographer)
|
||||
newGallery.Rating = input.Rating100
|
||||
|
||||
var err error
|
||||
|
||||
newGallery.Date, err = translator.datePtr(input.Date)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting date: %w", err)
|
||||
}
|
||||
newGallery.StudioID, err = translator.intPtrFromString(input.StudioID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
newGallery.PerformerIDs, err = translator.relatedIds(input.PerformerIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
|
||||
newGallery.TagIDs, err = translator.relatedIds(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
sceneIDs, err := stringslice.StringSliceToIntSlice(input.SceneIds)
|
||||
newGallery.SceneIDs, err = translator.relatedIds(input.SceneIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
|
||||
// Populate a new gallery from the input
|
||||
currentTime := time.Now()
|
||||
newGallery := models.Gallery{
|
||||
Title: input.Title,
|
||||
URL: translator.string(input.URL, "url"),
|
||||
Details: translator.string(input.Details, "details"),
|
||||
Rating: translator.ratingConversionInt(input.Rating, input.Rating100),
|
||||
PerformerIDs: models.NewRelatedIDs(performerIDs),
|
||||
TagIDs: models.NewRelatedIDs(tagIDs),
|
||||
SceneIDs: models.NewRelatedIDs(sceneIDs),
|
||||
CreatedAt: currentTime,
|
||||
UpdatedAt: currentTime,
|
||||
}
|
||||
|
||||
newGallery.Date, err = translator.datePtr(input.Date, "date")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting date: %w", err)
|
||||
}
|
||||
newGallery.StudioID, err = translator.intPtrFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
if input.Urls != nil {
|
||||
newGallery.URLs = models.NewRelatedStrings(input.Urls)
|
||||
} else if input.URL != nil {
|
||||
newGallery.URLs = models.NewRelatedStrings([]string{*input.URL})
|
||||
}
|
||||
|
||||
// Start the transaction and save the gallery
|
||||
@@ -87,7 +91,7 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGallery.ID, plugin.GalleryCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGallery.ID, hook.GalleryCreatePost, input, nil)
|
||||
return r.getGallery(ctx, newGallery.ID)
|
||||
}
|
||||
|
||||
@@ -105,7 +109,7 @@ func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.Galle
|
||||
}
|
||||
|
||||
// execute post hooks outside txn
|
||||
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.GalleryUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, hook.GalleryUpdatePost, input, translator.getFields())
|
||||
return r.getGallery(ctx, ret.ID)
|
||||
}
|
||||
|
||||
@@ -139,7 +143,8 @@ func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, hook.GalleryUpdatePost, input, translator.getFields())
|
||||
|
||||
gallery, err = r.getGallery(ctx, gallery.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -154,7 +159,7 @@ func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.
|
||||
func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.GalleryUpdateInput, translator changesetTranslator) (*models.Gallery, error) {
|
||||
galleryID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
qb := r.repository.Gallery
|
||||
@@ -180,64 +185,58 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||
updatedGallery.Title = models.NewOptionalString(*input.Title)
|
||||
}
|
||||
|
||||
updatedGallery.Code = translator.optionalString(input.Code, "code")
|
||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
||||
updatedGallery.Photographer = translator.optionalString(input.Photographer, "photographer")
|
||||
updatedGallery.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
updatedGallery.Date, err = translator.optionalDate(input.Date, "date")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting date: %w", err)
|
||||
}
|
||||
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
if input.PrimaryFileID != nil {
|
||||
primaryFileID, err := strconv.Atoi(*input.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||
}
|
||||
updatedGallery.URLs = translator.optionalURLs(input.Urls, input.URL)
|
||||
|
||||
converted := file.ID(primaryFileID)
|
||||
updatedGallery.PrimaryFileID = &converted
|
||||
updatedGallery.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||
}
|
||||
if updatedGallery.PrimaryFileID != nil {
|
||||
primaryFileID := *updatedGallery.PrimaryFileID
|
||||
|
||||
if err := originalGallery.LoadFiles(ctx, r.repository.Gallery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that new primary file is associated with gallery
|
||||
var f file.File
|
||||
var f models.File
|
||||
for _, ff := range originalGallery.Files.List() {
|
||||
if ff.Base().ID == converted {
|
||||
if ff.Base().ID == primaryFileID {
|
||||
f = ff
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, fmt.Errorf("file with id %d not associated with gallery", converted)
|
||||
return nil, fmt.Errorf("file with id %d not associated with gallery", primaryFileID)
|
||||
}
|
||||
}
|
||||
|
||||
if translator.hasField("performer_ids") {
|
||||
updatedGallery.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
updatedGallery.PerformerIDs, err = translator.updateIds(input.PerformerIds, "performer_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedGallery.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
updatedGallery.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
if translator.hasField("scene_ids") {
|
||||
updatedGallery.SceneIDs, err = translateUpdateIDs(input.SceneIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
updatedGallery.SceneIDs, err = translator.updateIds(input.SceneIds, "scene_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
|
||||
// gallery scene is set from the scene only
|
||||
@@ -253,7 +252,7 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||
func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGalleryUpdateInput) ([]*models.Gallery, error) {
|
||||
galleryIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
@@ -263,38 +262,33 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
|
||||
// Populate gallery from the input
|
||||
updatedGallery := models.NewGalleryPartial()
|
||||
|
||||
updatedGallery.Code = translator.optionalString(input.Code, "code")
|
||||
updatedGallery.Details = translator.optionalString(input.Details, "details")
|
||||
updatedGallery.URL = translator.optionalString(input.URL, "url")
|
||||
updatedGallery.Photographer = translator.optionalString(input.Photographer, "photographer")
|
||||
updatedGallery.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
updatedGallery.URLs = translator.optionalURLsBulk(input.Urls, input.URL)
|
||||
|
||||
updatedGallery.Date, err = translator.optionalDate(input.Date, "date")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting date: %w", err)
|
||||
}
|
||||
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
if translator.hasField("performer_ids") {
|
||||
updatedGallery.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
updatedGallery.PerformerIDs, err = translator.updateIdsBulk(input.PerformerIds, "performer_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedGallery.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
updatedGallery.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
if translator.hasField("scene_ids") {
|
||||
updatedGallery.SceneIDs, err = translateUpdateIDs(input.SceneIds.Ids, input.SceneIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
updatedGallery.SceneIDs, err = translator.updateIdsBulk(input.SceneIds, "scene_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
|
||||
ret := []*models.Gallery{}
|
||||
@@ -320,7 +314,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
|
||||
// execute post hooks outside of txn
|
||||
var newRet []*models.Gallery
|
||||
for _, gallery := range ret {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, hook.GalleryUpdatePost, input, translator.getFields())
|
||||
|
||||
gallery, err := r.getGallery(ctx, gallery.ID)
|
||||
if err != nil {
|
||||
@@ -336,7 +330,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
|
||||
func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.GalleryDestroyInput) (bool, error) {
|
||||
galleryIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
var galleries []*models.Gallery
|
||||
@@ -393,9 +387,9 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
}
|
||||
}
|
||||
|
||||
// call post hook after performing the other actions
|
||||
// call post hook after performing the other actionsa
|
||||
for _, gallery := range galleries {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, hook.GalleryDestroyPost, plugin.GalleryDestroyInput{
|
||||
GalleryDestroyInput: input,
|
||||
Checksum: gallery.PrimaryChecksum(),
|
||||
Path: gallery.Path,
|
||||
@@ -404,7 +398,7 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
|
||||
|
||||
// call image destroy post hook as well
|
||||
for _, img := range imgsDestroyed {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, img.ID, plugin.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
r.hookExecutor.ExecutePostHooks(ctx, img.ID, hook.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
Checksum: img.Checksum,
|
||||
Path: img.Path,
|
||||
}, nil)
|
||||
@@ -427,12 +421,12 @@ func isStashPath(path string) bool {
|
||||
func (r *mutationResolver) AddGalleryImages(ctx context.Context, input GalleryAddInput) (bool, error) {
|
||||
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("converting gallery id: %w", err)
|
||||
}
|
||||
|
||||
imageIDs, err := stringslice.StringSliceToIntSlice(input.ImageIds)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("converting image ids: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -457,12 +451,12 @@ func (r *mutationResolver) AddGalleryImages(ctx context.Context, input GalleryAd
|
||||
func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input GalleryRemoveInput) (bool, error) {
|
||||
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("converting gallery id: %w", err)
|
||||
}
|
||||
|
||||
imageIDs, err := stringslice.StringSliceToIntSlice(input.ImageIds)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("converting image ids: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -484,6 +478,61 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input Galler
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SetGalleryCover(ctx context.Context, input GallerySetCoverInput) (bool, error) {
|
||||
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting gallery id: %w", err)
|
||||
}
|
||||
|
||||
coverImageID, err := strconv.Atoi(input.CoverImageID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting cover image id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Gallery
|
||||
gallery, err := qb.Find(ctx, galleryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
return fmt.Errorf("gallery with id %d not found", galleryID)
|
||||
}
|
||||
|
||||
return r.galleryService.SetCover(ctx, gallery, coverImageID)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ResetGalleryCover(ctx context.Context, input GalleryResetCoverInput) (bool, error) {
|
||||
galleryID, err := strconv.Atoi(input.GalleryID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting gallery id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Gallery
|
||||
gallery, err := qb.Find(ctx, galleryID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
return fmt.Errorf("gallery with id %d not found", galleryID)
|
||||
}
|
||||
|
||||
return r.galleryService.ResetCover(ctx, gallery)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret *models.GalleryChapter, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.GalleryChapter.Find(ctx, id)
|
||||
@@ -501,14 +550,12 @@ func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input Galle
|
||||
return nil, fmt.Errorf("converting gallery id: %w", err)
|
||||
}
|
||||
|
||||
currentTime := time.Now()
|
||||
newChapter := models.GalleryChapter{
|
||||
Title: input.Title,
|
||||
ImageIndex: input.ImageIndex,
|
||||
GalleryID: galleryID,
|
||||
CreatedAt: currentTime,
|
||||
UpdatedAt: currentTime,
|
||||
}
|
||||
// Populate a new gallery chapter from the input
|
||||
newChapter := models.NewGalleryChapter()
|
||||
|
||||
newChapter.Title = input.Title
|
||||
newChapter.ImageIndex = input.ImageIndex
|
||||
newChapter.GalleryID = galleryID
|
||||
|
||||
// Start the transaction and save the gallery chapter
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -527,14 +574,14 @@ func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input Galle
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newChapter.ID, plugin.GalleryChapterCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newChapter.ID, hook.GalleryChapterCreatePost, input, nil)
|
||||
return r.getGalleryChapter(ctx, newChapter.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input GalleryChapterUpdateInput) (*models.GalleryChapter, error) {
|
||||
chapterID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
@@ -593,14 +640,14 @@ func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input Galle
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, chapterID, plugin.GalleryChapterUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, chapterID, hook.GalleryChapterUpdatePost, input, translator.getFields())
|
||||
return r.getGalleryChapter(ctx, chapterID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleryChapterDestroy(ctx context.Context, id string) (bool, error) {
|
||||
chapterID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -621,7 +668,7 @@ func (r *mutationResolver) GalleryChapterDestroy(ctx context.Context, id string)
|
||||
return false, err
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, chapterID, plugin.GalleryChapterDestroyPost, id, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, chapterID, hook.GalleryChapterDestroyPost, id, nil)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
413
internal/api/resolver_mutation_group.go
Normal file
413
internal/api/resolver_mutation_group.go
Normal file
@@ -0,0 +1,413 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/static"
|
||||
"github.com/stashapp/stash/pkg/group"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate a new group from the input
|
||||
newGroup := models.NewGroup()
|
||||
|
||||
newGroup.Name = input.Name
|
||||
newGroup.Aliases = translator.string(input.Aliases)
|
||||
newGroup.Duration = input.Duration
|
||||
newGroup.Rating = input.Rating100
|
||||
newGroup.Director = translator.string(input.Director)
|
||||
newGroup.Synopsis = translator.string(input.Synopsis)
|
||||
|
||||
var err error
|
||||
|
||||
newGroup.Date, err = translator.datePtr(input.Date)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting date: %w", err)
|
||||
}
|
||||
newGroup.StudioID, err = translator.intPtrFromString(input.StudioID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
|
||||
newGroup.TagIDs, err = translator.relatedIds(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
newGroup.ContainingGroups, err = translator.groupIDDescriptions(input.ContainingGroups)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting containing group ids: %w", err)
|
||||
}
|
||||
|
||||
newGroup.SubGroups, err = translator.groupIDDescriptions(input.SubGroups)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting containing group ids: %w", err)
|
||||
}
|
||||
|
||||
if input.Urls != nil {
|
||||
newGroup.URLs = models.NewRelatedStrings(input.Urls)
|
||||
}
|
||||
|
||||
return &newGroup, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
|
||||
newGroup, err := groupFromGroupCreateInput(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var frontimageData []byte
|
||||
if input.FrontImage != nil {
|
||||
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing front image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var backimageData []byte
|
||||
if input.BackImage != nil {
|
||||
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing back image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: if back image is being set, set the front image to the default.
|
||||
// This is because we can't have a null front image with a non-null back image.
|
||||
if len(frontimageData) == 0 && len(backimageData) != 0 {
|
||||
frontimageData = static.ReadAll(static.DefaultGroupImage)
|
||||
}
|
||||
|
||||
// Start the transaction and save the group
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err = r.groupService.Create(ctx, newGroup, frontimageData, backimageData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.GroupCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.MovieCreatePost, input, nil)
|
||||
return r.getGroup(ctx, newGroup.ID)
|
||||
}
|
||||
|
||||
func groupPartialFromGroupUpdateInput(translator changesetTranslator, input GroupUpdateInput) (ret models.GroupPartial, err error) {
|
||||
// Populate group from the input
|
||||
updatedGroup := models.NewGroupPartial()
|
||||
|
||||
updatedGroup.Name = translator.optionalString(input.Name, "name")
|
||||
updatedGroup.Aliases = translator.optionalString(input.Aliases, "aliases")
|
||||
updatedGroup.Duration = translator.optionalInt(input.Duration, "duration")
|
||||
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGroup.Director = translator.optionalString(input.Director, "director")
|
||||
updatedGroup.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
|
||||
|
||||
updatedGroup.Date, err = translator.optionalDate(input.Date, "date")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting date: %w", err)
|
||||
return
|
||||
}
|
||||
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting studio id: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting tag ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.ContainingGroups, err = translator.updateGroupIDDescriptions(input.ContainingGroups, "containing_groups")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting containing group ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.SubGroups, err = translator.updateGroupIDDescriptions(input.SubGroups, "sub_groups")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting containing group ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.URLs = translator.updateStrings(input.Urls, "urls")
|
||||
|
||||
return updatedGroup, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupUpdate(ctx context.Context, input GroupUpdateInput) (*models.Group, error) {
|
||||
groupID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
updatedGroup, err := groupPartialFromGroupUpdateInput(translator, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var frontimageData []byte
|
||||
frontImageIncluded := translator.hasField("front_image")
|
||||
if input.FrontImage != nil {
|
||||
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing front image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var backimageData []byte
|
||||
backImageIncluded := translator.hasField("back_image")
|
||||
if input.BackImage != nil {
|
||||
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing back image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
frontImage := group.ImageInput{
|
||||
Image: frontimageData,
|
||||
Set: frontImageIncluded,
|
||||
}
|
||||
|
||||
backImage := group.ImageInput{
|
||||
Image: backimageData,
|
||||
Set: backImageIncluded,
|
||||
}
|
||||
|
||||
_, err = r.groupService.UpdatePartial(ctx, groupID, updatedGroup, frontImage, backImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, groupID, hook.GroupUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, groupID, hook.MovieUpdatePost, input, translator.getFields())
|
||||
return r.getGroup(ctx, groupID)
|
||||
}
|
||||
|
||||
func groupPartialFromBulkGroupUpdateInput(translator changesetTranslator, input BulkGroupUpdateInput) (ret models.GroupPartial, err error) {
|
||||
updatedGroup := models.NewGroupPartial()
|
||||
|
||||
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedGroup.Director = translator.optionalString(input.Director, "director")
|
||||
|
||||
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting studio id: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting tag ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.ContainingGroups, err = translator.updateGroupIDDescriptionsBulk(input.ContainingGroups, "containing_groups")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting containing group ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.SubGroups, err = translator.updateGroupIDDescriptionsBulk(input.SubGroups, "sub_groups")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("converting containing group ids: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedGroup.URLs = translator.optionalURLsBulk(input.Urls, nil)
|
||||
|
||||
return updatedGroup, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkGroupUpdate(ctx context.Context, input BulkGroupUpdateInput) ([]*models.Group, error) {
|
||||
groupIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate group from the input
|
||||
updatedGroup, err := groupPartialFromBulkGroupUpdateInput(translator, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := []*models.Group{}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
for _, groupID := range groupIDs {
|
||||
group, err := r.groupService.UpdatePartial(ctx, groupID, updatedGroup, group.ImageInput{}, group.ImageInput{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, group)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var newRet []*models.Group
|
||||
for _, group := range ret {
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.GroupUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||
|
||||
group, err = r.getGroup(ctx, group.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newRet = append(newRet, group)
|
||||
}
|
||||
|
||||
return newRet, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupDestroy(ctx context.Context, input GroupDestroyInput) (bool, error) {
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.repository.Group.Destroy(ctx, id)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupsDestroy(ctx context.Context, groupIDs []string) (bool, error) {
|
||||
ids, err := stringslice.StringSliceToIntSlice(groupIDs)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Group
|
||||
for _, id := range ids {
|
||||
if err := qb.Destroy(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, groupIDs, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, groupIDs, nil)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddGroupSubGroups(ctx context.Context, input GroupSubGroupAddInput) (bool, error) {
|
||||
groupID, err := strconv.Atoi(input.ContainingGroupID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting group id: %w", err)
|
||||
}
|
||||
|
||||
subGroups, err := groupsDescriptionsFromGroupInput(input.SubGroups)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting sub group ids: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.groupService.AddSubGroups(ctx, groupID, subGroups, input.InsertIndex)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveGroupSubGroups(ctx context.Context, input GroupSubGroupRemoveInput) (bool, error) {
|
||||
groupID, err := strconv.Atoi(input.ContainingGroupID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting group id: %w", err)
|
||||
}
|
||||
|
||||
subGroupIDs, err := stringslice.StringSliceToIntSlice(input.SubGroupIds)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting sub group ids: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.groupService.RemoveSubGroups(ctx, groupID, subGroupIDs)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ReorderSubGroups(ctx context.Context, input ReorderSubGroupsInput) (bool, error) {
|
||||
groupID, err := strconv.Atoi(input.GroupID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting group id: %w", err)
|
||||
}
|
||||
|
||||
subGroupIDs, err := stringslice.StringSliceToIntSlice(input.SubGroupIds)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting sub group ids: %w", err)
|
||||
}
|
||||
|
||||
insertPointID, err := strconv.Atoi(input.InsertAtID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting insert at id: %w", err)
|
||||
}
|
||||
|
||||
insertAfter := utils.IsTrue(input.InsertAfter)
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
return r.groupService.ReorderSubGroups(ctx, groupID, subGroupIDs, insertPointID, insertAfter)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -10,11 +10,13 @@ import (
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
// used to refetch image after hooks run
|
||||
func (r *mutationResolver) getImage(ctx context.Context, id int) (ret *models.Image, err error) {
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Image.Find(ctx, id)
|
||||
@@ -40,7 +42,7 @@ func (r *mutationResolver) ImageUpdate(ctx context.Context, input ImageUpdateInp
|
||||
}
|
||||
|
||||
// execute post hooks outside txn
|
||||
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.ImageUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, hook.ImageUpdatePost, input, translator.getFields())
|
||||
return r.getImage(ctx, ret.ID)
|
||||
}
|
||||
|
||||
@@ -74,7 +76,8 @@ func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdat
|
||||
inputMap: inputMaps[i],
|
||||
}
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, plugin.ImageUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, hook.ImageUpdatePost, input, translator.getFields())
|
||||
|
||||
image, err = r.getImage(ctx, image.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -89,7 +92,7 @@ func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdat
|
||||
func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInput, translator changesetTranslator) (*models.Image, error) {
|
||||
imageID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
i, err := r.repository.Image.Find(ctx, imageID)
|
||||
@@ -105,8 +108,12 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
||||
updatedImage := models.NewImagePartial()
|
||||
|
||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedImage.URL = translator.optionalString(input.URL, "url")
|
||||
updatedImage.Code = translator.optionalString(input.Code, "code")
|
||||
updatedImage.Details = translator.optionalString(input.Details, "details")
|
||||
updatedImage.Photographer = translator.optionalString(input.Photographer, "photographer")
|
||||
updatedImage.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
updatedImage.Date, err = translator.optionalDate(input.Date, "date")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting date: %w", err)
|
||||
@@ -115,42 +122,40 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
if input.PrimaryFileID != nil {
|
||||
primaryFileID, err := strconv.Atoi(*input.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||
}
|
||||
updatedImage.URLs = translator.optionalURLs(input.Urls, input.URL)
|
||||
|
||||
converted := file.ID(primaryFileID)
|
||||
updatedImage.PrimaryFileID = &converted
|
||||
updatedImage.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||
}
|
||||
if updatedImage.PrimaryFileID != nil {
|
||||
primaryFileID := *updatedImage.PrimaryFileID
|
||||
|
||||
if err := i.LoadFiles(ctx, r.repository.Image); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure that new primary file is associated with image
|
||||
var f file.File
|
||||
var f models.File
|
||||
for _, ff := range i.Files.List() {
|
||||
if ff.Base().ID == converted {
|
||||
if ff.Base().ID == primaryFileID {
|
||||
f = ff
|
||||
}
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, fmt.Errorf("file with id %d not associated with image", converted)
|
||||
return nil, fmt.Errorf("file with id %d not associated with image", primaryFileID)
|
||||
}
|
||||
}
|
||||
|
||||
var updatedGalleryIDs []int
|
||||
|
||||
if translator.hasField("gallery_ids") {
|
||||
updatedImage.GalleryIDs, err = translateUpdateIDs(input.GalleryIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
|
||||
updatedImage.GalleryIDs, err = translator.updateIds(input.GalleryIds, "gallery_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
if updatedImage.GalleryIDs != nil {
|
||||
// ensure gallery IDs are loaded
|
||||
if err := i.LoadGalleryIDs(ctx, r.repository.Image); err != nil {
|
||||
return nil, err
|
||||
@@ -163,18 +168,13 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
||||
updatedGalleryIDs = updatedImage.GalleryIDs.ImpactedIDs(i.GalleryIDs.List())
|
||||
}
|
||||
|
||||
if translator.hasField("performer_ids") {
|
||||
updatedImage.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
updatedImage.PerformerIDs, err = translator.updateIds(input.PerformerIds, "performer_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedImage.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
updatedImage.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
qb := r.repository.Image
|
||||
@@ -196,7 +196,7 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
||||
func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageUpdateInput) (ret []*models.Image, err error) {
|
||||
imageIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
@@ -207,8 +207,12 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
updatedImage := models.NewImagePartial()
|
||||
|
||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||
updatedImage.URL = translator.optionalString(input.URL, "url")
|
||||
updatedImage.Code = translator.optionalString(input.Code, "code")
|
||||
updatedImage.Details = translator.optionalString(input.Details, "details")
|
||||
updatedImage.Photographer = translator.optionalString(input.Photographer, "photographer")
|
||||
updatedImage.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
updatedImage.Date, err = translator.optionalDate(input.Date, "date")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting date: %w", err)
|
||||
@@ -217,27 +221,20 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||
}
|
||||
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
if translator.hasField("gallery_ids") {
|
||||
updatedImage.GalleryIDs, err = translateUpdateIDs(input.GalleryIds.Ids, input.GalleryIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
updatedImage.URLs = translator.optionalURLsBulk(input.Urls, input.URL)
|
||||
|
||||
updatedImage.GalleryIDs, err = translator.updateIdsBulk(input.GalleryIds, "gallery_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||
}
|
||||
|
||||
if translator.hasField("performer_ids") {
|
||||
updatedImage.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
updatedImage.PerformerIDs, err = translator.updateIdsBulk(input.PerformerIds, "performer_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting performer ids: %w", err)
|
||||
}
|
||||
|
||||
if translator.hasField("tag_ids") {
|
||||
updatedImage.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
updatedImage.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
// Start the transaction and save the images
|
||||
@@ -266,7 +263,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
}
|
||||
|
||||
thisUpdatedGalleryIDs := updatedImage.GalleryIDs.ImpactedIDs(i.GalleryIDs.List())
|
||||
updatedGalleryIDs = intslice.IntAppendUniques(updatedGalleryIDs, thisUpdatedGalleryIDs)
|
||||
updatedGalleryIDs = sliceutil.AppendUniques(updatedGalleryIDs, thisUpdatedGalleryIDs)
|
||||
}
|
||||
|
||||
image, err := qb.UpdatePartial(ctx, imageID, updatedImage)
|
||||
@@ -292,7 +289,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
// execute post hooks outside of txn
|
||||
var newRet []*models.Image
|
||||
for _, image := range ret {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, plugin.ImageUpdatePost, input, translator.getFields())
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, hook.ImageUpdatePost, input, translator.getFields())
|
||||
|
||||
image, err = r.getImage(ctx, image.ID)
|
||||
if err != nil {
|
||||
@@ -308,7 +305,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageDestroyInput) (ret bool, err error) {
|
||||
imageID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
var i *models.Image
|
||||
@@ -336,7 +333,7 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD
|
||||
fileDeleter.Commit()
|
||||
|
||||
// call post hook after performing the other actions
|
||||
r.hookExecutor.ExecutePostHooks(ctx, i.ID, plugin.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
r.hookExecutor.ExecutePostHooks(ctx, i.ID, hook.ImageDestroyPost, plugin.ImageDestroyInput{
|
||||
ImageDestroyInput: input,
|
||||
Checksum: i.Checksum,
|
||||
Path: i.Path,
|
||||
@@ -348,7 +345,7 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD
|
||||
func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.ImagesDestroyInput) (ret bool, err error) {
|
||||
imageIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
var images []*models.Image
|
||||
@@ -387,7 +384,7 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image
|
||||
|
||||
for _, image := range images {
|
||||
// call post hook after performing the other actions
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, plugin.ImageDestroyPost, plugin.ImagesDestroyInput{
|
||||
r.hookExecutor.ExecutePostHooks(ctx, image.ID, hook.ImageDestroyPost, plugin.ImagesDestroyInput{
|
||||
ImagesDestroyInput: input,
|
||||
Checksum: image.Checksum,
|
||||
Path: image.Path,
|
||||
@@ -400,7 +397,7 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image
|
||||
func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (ret int, err error) {
|
||||
imageID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -418,7 +415,7 @@ func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (ret
|
||||
func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (ret int, err error) {
|
||||
imageID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
@@ -436,7 +433,7 @@ func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (ret
|
||||
func (r *mutationResolver) ImageResetO(ctx context.Context, id string) (ret int, err error) {
|
||||
imageID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
|
||||
@@ -2,17 +2,18 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) StopJob(ctx context.Context, jobID string) (bool, error) {
|
||||
idInt, err := strconv.Atoi(jobID)
|
||||
id, err := strconv.Atoi(jobID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
manager.GetInstance().JobManager.CancelJob(idInt)
|
||||
manager.GetInstance().JobManager.CancelJob(id)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -12,7 +10,7 @@ import (
|
||||
"github.com/stashapp/stash/internal/identify"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/internal/manager/task"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -101,6 +99,21 @@ func (r *mutationResolver) MetadataClean(ctx context.Context, input manager.Clea
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MetadataCleanGenerated(ctx context.Context, input task.CleanGeneratedOptions) (string, error) {
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.CleanGeneratedJob{
|
||||
Options: input,
|
||||
Paths: mgr.Paths,
|
||||
BlobsStorageType: mgr.Config.GetBlobsStorage(),
|
||||
VideoFileNamingAlgorithm: mgr.Config.GetVideoFileNamingAlgorithm(),
|
||||
Repository: mgr.Repository,
|
||||
BlobCleaner: mgr.Repository.Blob,
|
||||
}
|
||||
jobID := mgr.JobManager.Add(ctx, "Cleaning generated files...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MigrateHashNaming(ctx context.Context) (string, error) {
|
||||
jobID := manager.GetInstance().MigrateHash(ctx)
|
||||
return strconv.Itoa(jobID), nil
|
||||
@@ -110,31 +123,10 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
||||
// if download is true, then backup to temporary file and return a link
|
||||
download := input.Download != nil && *input.Download
|
||||
mgr := manager.GetInstance()
|
||||
database := mgr.Database
|
||||
var backupPath string
|
||||
if download {
|
||||
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
|
||||
}
|
||||
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "backup*.sqlite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backupPath = f.Name()
|
||||
f.Close()
|
||||
} else {
|
||||
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
|
||||
if backupDirectoryPath != "" {
|
||||
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
|
||||
}
|
||||
}
|
||||
backupPath = database.DatabaseBackupPath(backupDirectoryPath)
|
||||
}
|
||||
|
||||
err := database.Backup(backupPath)
|
||||
backupPath, backupName, err := mgr.BackupDatabase(download)
|
||||
if err != nil {
|
||||
logger.Errorf("Error backing up database: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -147,8 +139,7 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
fn := filepath.Base(database.DatabaseBackupPath(""))
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + backupName
|
||||
return &ret, nil
|
||||
} else {
|
||||
logger.Infof("Successfully backed up database to: %s", backupPath)
|
||||
@@ -158,33 +149,11 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input AnonymiseDatabaseInput) (*string, error) {
|
||||
// if download is true, then backup to temporary file and return a link
|
||||
// if download is true, then save to temporary file and return a link
|
||||
download := input.Download != nil && *input.Download
|
||||
mgr := manager.GetInstance()
|
||||
database := mgr.Database
|
||||
var outPath string
|
||||
if download {
|
||||
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
|
||||
}
|
||||
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "anonymous*.sqlite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outPath = f.Name()
|
||||
f.Close()
|
||||
} else {
|
||||
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
|
||||
if backupDirectoryPath != "" {
|
||||
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
|
||||
}
|
||||
}
|
||||
outPath = database.AnonymousDatabasePath(backupDirectoryPath)
|
||||
}
|
||||
|
||||
err := database.Anonymise(outPath)
|
||||
outPath, outName, err := mgr.AnonymiseDatabase(download)
|
||||
if err != nil {
|
||||
logger.Errorf("Error anonymising database: %v", err)
|
||||
return nil, err
|
||||
@@ -199,8 +168,7 @@ func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input Anonymis
|
||||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
fn := filepath.Base(database.DatabaseBackupPath(""))
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + outName
|
||||
return &ret, nil
|
||||
} else {
|
||||
logger.Infof("Successfully anonymised database to: %s", outPath)
|
||||
|
||||
@@ -11,30 +11,43 @@ import (
|
||||
)
|
||||
|
||||
func (r *mutationResolver) MigrateSceneScreenshots(ctx context.Context, input MigrateSceneScreenshotsInput) (string, error) {
|
||||
db := manager.GetInstance().Database
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.MigrateSceneScreenshotsJob{
|
||||
ScreenshotsPath: manager.GetInstance().Paths.Generated.Screenshots,
|
||||
Input: scene.MigrateSceneScreenshotsInput{
|
||||
DeleteFiles: utils.IsTrue(input.DeleteFiles),
|
||||
OverwriteExisting: utils.IsTrue(input.OverwriteExisting),
|
||||
},
|
||||
SceneRepo: db.Scene,
|
||||
TxnManager: db,
|
||||
SceneRepo: mgr.Repository.Scene,
|
||||
TxnManager: mgr.Repository.TxnManager,
|
||||
}
|
||||
jobID := manager.GetInstance().JobManager.Add(ctx, "Migrating scene screenshots to blobs...", t)
|
||||
jobID := mgr.JobManager.Add(ctx, "Migrating scene screenshots to blobs...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) MigrateBlobs(ctx context.Context, input MigrateBlobsInput) (string, error) {
|
||||
db := manager.GetInstance().Database
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.MigrateBlobsJob{
|
||||
TxnManager: db,
|
||||
BlobStore: db.Blobs,
|
||||
Vacuumer: db,
|
||||
TxnManager: mgr.Database,
|
||||
BlobStore: mgr.Database.Blobs,
|
||||
Vacuumer: mgr.Database,
|
||||
DeleteOld: utils.IsTrue(input.DeleteOld),
|
||||
}
|
||||
jobID := manager.GetInstance().JobManager.Add(ctx, "Migrating blobs...", t)
|
||||
jobID := mgr.JobManager.Add(ctx, "Migrating blobs...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) Migrate(ctx context.Context, input manager.MigrateInput) (string, error) {
|
||||
mgr := manager.GetInstance()
|
||||
t := &task.MigrateJob{
|
||||
BackupPath: input.BackupPath,
|
||||
Config: mgr.Config,
|
||||
Database: mgr.Database,
|
||||
}
|
||||
|
||||
jobID := mgr.JobManager.Add(ctx, "Migrating database...", t)
|
||||
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user