Compare commits

..

137 Commits

Author SHA1 Message Date
WithoutPants
e9fa7d071e Add changelog entries 2023-02-21 12:06:17 +11:00
DingDongSoLong4
dd5cff2aec Minor gallery-related fixes (#3448)
* Fix gallery titles
* Fix SceneListTable
2023-02-21 12:00:33 +11:00
DingDongSoLong4
c7c4d5b126 Optimize allData queries (#3452)
* Add specific fields to allData queries
* Add additional allData endpoints
2023-02-21 11:53:08 +11:00
DingDongSoLong4
ccbe3c4e92 Fix batch performer panic (#3456) 2023-02-21 11:45:58 +11:00
DingDongSoLong4
96ce260a40 Fix allTags cache reset (#3444) 2023-02-21 11:45:13 +11:00
WithoutPants
8bd5f91e58 Disable tag popover for create tag option (#3429) 2023-02-13 12:15:06 +11:00
DingDongSoLong4
83cb51ec47 Error reliably on invalid filter (#3428) 2023-02-13 12:14:41 +11:00
DingDongSoLong4
ca38a355d2 Add regex validation (#3424) 2023-02-13 10:48:10 +11:00
WithoutPants
d2865b0796 Use absolute paths when creating config file (#3417) 2023-02-11 09:22:17 +11:00
WithoutPants
3cf97f6e27 Handle folder symlinks correctly during clean (#3415) 2023-02-11 09:21:18 +11:00
WithoutPants
f0988817c8 Fix multi scrape bug with fileless scenes (#3414) 2023-02-11 09:19:15 +11:00
WithoutPants
692c1e55ac Fix save disabled when creating new object with name (#3409)
* Dirty when creating with name
* Fix prompt on tag save
2023-02-11 08:59:08 +11:00
WithoutPants
18b44e9381 Set performer disambiguation for names with parentheses (#3406) 2023-02-10 09:30:23 +11:00
CJ
7761ac19de Add count attribute to badges (#3405) 2023-02-08 15:08:38 +11:00
dependabot[bot]
deb5110623 Bump http-cache-semantics from 4.1.0 to 4.1.1 in /ui/v2.5 (#3393)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-08 15:03:42 +11:00
dependabot[bot]
9875a21674 Bump ua-parser-js from 0.7.24 to 0.7.33 in /ui/v2.5 (#3385)
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.24 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.24...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-08 15:03:03 +11:00
DingDongSoLong4
901a7e59ec Add additional latest version info (#3357) 2023-02-08 15:02:23 +11:00
WithoutPants
cb808c7be2 Remove is (not) null modifier for resume and play times (#3407) 2023-02-08 13:26:12 +11:00
WithoutPants
cb3545a303 Clear search term when clicking popover pill (#3408) 2023-02-08 11:07:15 +11:00
WithoutPants
6f2057a51e Handle unset rating system (#3410) 2023-02-07 16:44:00 +11:00
apache202119
53f9530524 Add shortcuts for decimal rating (#3226)
* Add shortcuts for decimal rating
* Add shortcut to reset decimal rating
* Generalise rating keybind code

Use r x x for decimal ratings.
* Update manual page
---------

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-02-07 12:23:18 +11:00
JackDawson94
65d1353f2c Allow use of Proxy (#3284)
* Proxy config
* Disable proxy for localhost & local LAN
* No_proxy is now configurable
2023-02-07 09:46:18 +11:00
DogmaDragon
1cba910435 [Documentation] Typo (#3390) 2023-02-06 16:34:33 +11:00
DogmaDragon
ef2af977d3 Update the links after minor file restructuring (#3396) 2023-02-06 16:34:06 +11:00
bnkai
d80ec1d7a1 Fix scene studio results when doing a search scrape (#3246) 2023-01-30 09:40:53 +11:00
JoeSmithStarkers
7ccfa07843 re-enable preview fallback generation mode (#3377) 2023-01-30 09:23:25 +11:00
DingDongSoLong4
32e8496314 Add studio performer count (#3362)
* Add studio performer count

* Add mocks
2023-01-28 19:12:47 -05:00
bnkai
c52d8c9314 Tweak IS/IS NOT NULL date filter (#3326) 2023-01-28 18:33:49 -05:00
DogmaDragon
71a751d997 Typo (#3360) 2023-01-28 18:23:04 -05:00
stash-translation-bot
f5ff1139b0 Translations update from Stash (#3209)
* Translated using Weblate (German)

Currently translated at 98.0% (868 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/de/

* Translated using Weblate (French)

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/sv/

* Translated using Weblate (Japanese)

Currently translated at 97.8% (866 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ja/

* Translated using Weblate (French)

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hant/

* Translated using Weblate (Italian)

Currently translated at 100.0% (886 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/it/

* Translated using Weblate (Korean)

Currently translated at 98.9% (877 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ko/

* Translated using Weblate (French)

Currently translated at 100.0% (886 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (German)

Currently translated at 98.9% (877 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/de/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (886 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (886 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hans/

* Translated using Weblate (French)

Currently translated at 100.0% (886 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Russian)

Currently translated at 88.9% (788 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (Korean)

Currently translated at 99.0% (878 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ko/

* Translated using Weblate (Polish)

Currently translated at 100.0% (886 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pl/

* Translated using Weblate (German)

Currently translated at 99.6% (883 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/de/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (886 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ja/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (886 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hans/

* Translated using Weblate (Croatian)

Currently translated at 10.7% (95 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/hr/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (886 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/sv/

* Translated using Weblate (Danish)

Currently translated at 93.7% (831 of 886 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/da/

* Translated using Weblate (French)

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Italian)

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/it/

* Translated using Weblate (Russian)

Currently translated at 88.4% (788 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (French)

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hans/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/sv/

* Translated using Weblate (Spanish)

Currently translated at 91.0% (811 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/es/

* Translated using Weblate (Finnish)

Currently translated at 91.5% (816 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fi/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ja/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hant/

* Translated using Weblate (French)

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (Polish)

Currently translated at 99.7% (889 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pl/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (891 of 891 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/sv/

---------

Co-authored-by: Niko <nikolai.boeck@gmx.de>
Co-authored-by: MrOV3RDOSE <mrov3rdose@gmail.com>
Co-authored-by: Alpaca Serious <srhsgsef@gmail.com>
Co-authored-by: suzuhiroruri <suzuhiroruri@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Ben Mullin-Lamarch <ben.mullinlamarche@gmail.com>
Co-authored-by: Still <dev@stillu.cc>
Co-authored-by: BViking78 <bviking78@gmail.com>
Co-authored-by: yc <yechan24680@gmail.com>
Co-authored-by: Gerd Wittmann <spamkill2006-08@yahoo.de>
Co-authored-by: Lauri <stashapp.pot3l@8shield.net>
Co-authored-by: Philip Wang <philpw99@gmail.com>
Co-authored-by: ApxuBbI <nick1232@inbox.lv>
Co-authored-by: alan <wnd1@daum.net>
Co-authored-by: Wasylq <Wasylq@protonmail.com>
Co-authored-by: Frank Hamm <hammfrank83@gmail.com>
Co-authored-by: 風林火山 <nezoko@digdig.org>
Co-authored-by: Ivy JP <ivi104.wiki@gmail.com>
Co-authored-by: John Doe <yc6w3zir@duck.com>
Co-authored-by: deepserket <deepserket@gmail.com>
Co-authored-by: Mistle Yurika <tehmistle@gmx.com>
Co-authored-by: Hugo Vidal <hugo@hugovidafe.dev>
Co-authored-by: Aa <jarruraita@outlook.com>
Co-authored-by: Coscosname <coscosname@gmail.com>
2023-01-28 18:03:21 -05:00
puc9
cf0ce6cb08 Fix and improve captions detection (#3276) 2023-01-27 11:52:56 +11:00
CJ
08560923d2 CSS update to unify popover location (#3355) 2023-01-27 11:49:21 +11:00
HappyAxolotl
0e2bd125a8 add fields director and code to stash-box SceneDraftInput (#3335) 2023-01-27 11:38:32 +11:00
WithoutPants
b67abb89ff Allow configuration of ffmpeg args (#3216)
* Allow configuration of ffmpeg args
* Add UI settings for ffmpeg config
* Add changelog entry
* Add documentation in manual
2023-01-27 11:31:11 +11:00
dependabot[bot]
a36b895e4b Bump json5 from 1.0.1 to 1.0.2 in /ui/v2.5 (#3349)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-13 09:47:41 +11:00
DogmaDragon
7df26e2f56 Update description for Custom CSS/JS (#3354) 2023-01-13 09:46:55 +11:00
DogmaDragon
515202d28e [Documentation] Replaces wiki links with new documentation site (#3352) 2023-01-13 09:44:53 +11:00
DingDongSoLong4
639a9da65b Include URL query in login redirects (#3305) 2023-01-09 11:13:42 +11:00
CJ
01d40c1b9e Performer age calculation on images (#3301) 2023-01-03 09:00:16 +11:00
CJ
ef622659ff added US message for customize (#3331) 2023-01-03 08:54:19 +11:00
CJ
8bac413d74 Added Label for Russian Subtitle (#3300) 2023-01-03 08:53:39 +11:00
WithoutPants
9351a0b2a4 Add anonymise database task (#3186) 2022-12-23 09:15:27 +11:00
HijackHornet
0b4b100ecc [Feature] Images new fields : URL & Date (#3015)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-12-20 11:13:41 +11:00
WithoutPants
cc4b0f7b11 Fix performer migration (#3285)
* Recreate performers table instead of drop column
* Use performer_aliases to store original value
2022-12-19 11:24:42 +11:00
WithoutPants
150c496949 Fix dockerfile 2022-12-19 09:09:15 +11:00
WithoutPants
74506bc5e8 Fix UI crash when scraping new scene (#3283) 2022-12-16 12:14:00 +11:00
WithoutPants
05b0fb23f4 Restore old stash id filtering code (#3282) 2022-12-16 11:31:38 +11:00
WithoutPants
8629a0713d Add incorrectly removed dependencies 2022-12-16 10:14:28 +11:00
SnZ
0aab2c382f Fix failing ARM builds (#3278) 2022-12-15 11:28:18 +11:00
WithoutPants
74b585a05f Revert "Update Dockerfile (#3273)" (#3279)
This reverts commit 7c58305b05.
2022-12-15 09:40:48 +11:00
SnZ
7c58305b05 Update Dockerfile (#3273)
Add 'bencoder.pyx' python package for new torrent.py parser:
- bencoder.pyx is fast bencode implementation in Cython (https://github.com/whtsky/bencoder.pyx/ | https://pypi.org/project/bencoder.pyx/)
2022-12-14 13:16:30 -05:00
WithoutPants
ce080c1b07 Fix generate preview overwrite behaviour (#3256) 2022-12-09 12:46:25 +11:00
WithoutPants
a67eee8f4c Fix placeholder in string list input (#3257) 2022-12-09 12:05:05 +11:00
dependabot[bot]
2715dcb72a Bump decode-uri-component from 0.2.0 to 0.2.2 in /ui/v2.5 (#3249)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-09 08:30:21 +11:00
WithoutPants
eb7956a05a Correct title ordering for objects without titles (#3244)
* Correct scene title ordering
* Correct ordering for other objects
* Add basename function, add to gallery title sort
2022-12-09 08:29:34 +11:00
WithoutPants
78bb2d8425 Fix formatting issue in performer dialog (#3252) 2022-12-09 08:01:28 +11:00
WithoutPants
2064ea27b0 Fix incorrect performer removed after creation (#3251) 2022-12-08 19:08:29 +11:00
WithoutPants
782bed9df8 Fix date fields not being nulled correctly when cleared (#3243)
* Interpret empty string date as null
* Fix setSQLiteDate for null
2022-12-06 13:33:58 +11:00
WithoutPants
c9b0841184 Fix wall items not showing scene titles where title not set (#3242) 2022-12-06 13:08:09 +11:00
WithoutPants
38d6af8b66 Fix image exclusion pattern applying to all files (#3241)
* Add debug logging for scanning ignored files
* Fix image exclusion pattern handling
2022-12-06 12:28:16 +11:00
WithoutPants
dc875ed5d7 Clean missing captions during scan (#3240) 2022-12-06 12:04:40 +11:00
WithoutPants
b5b9023b3e Add injected css/javascript to plugins (#3195)
* Add injected css/javascript to plugins
* Manual documentation
2022-12-05 15:08:22 +11:00
alexandra-3
87cea80e7b Fix a bunch of scanning / tagging bugs (#3154)
* Fix possible infinite loop/stack overflow with weird/broken zip files
* Fix path length calculation using bytes instead of characters (runes)
* Fix bug where oshash gets buffers with size not actually multiple of 8
* Add oshash tests

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-12-01 16:48:04 +11:00
JackDawson94
e614ca8d26 Performer Active Years is submitted to Stashbox (#3146) 2022-12-01 14:06:29 +11:00
WithoutPants
4daf0a14a2 Performer disambiguation and aliases (#3113)
* Refactor performer relationships
* Remove checksum from performer
* Add disambiguation, overhaul aliases
* Add disambiguation filter criterion
* Improve name matching during import
* Add disambiguation filtering in UI
* Include aliases in performer select
2022-12-01 13:54:08 +11:00
CJ
d2395e579c Use popover property consistently across cards (#3207) 2022-12-01 10:58:54 +11:00
WithoutPants
8a649f0268 Rearrange changelog 2022-11-30 10:31:17 +11:00
WithoutPants
0b19a00ba8 Fix incorrect key being checked for override (#3212) 2022-11-30 09:34:05 +11:00
WithoutPants
5e332514fa Fix concurrent test timeout 2022-11-30 09:25:12 +11:00
stash-translation-bot
646f8bc02e Translations update from Stash (#3178)
* Translated using Weblate (Italian)

Currently translated at 100.0% (884 of 884 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/it/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 97.8% (865 of 884 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hans/

* Translated using Weblate (Polish)

Currently translated at 98.5% (871 of 884 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pl/

* Translated using Weblate (Italian)

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/it/

* Translated using Weblate (Finnish)

Currently translated at 91.2% (808 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fi/

* Translated using Weblate (French)

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hans/

* Translated using Weblate (Russian)

Currently translated at 89.0% (788 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (Korean)

Currently translated at 97.5% (863 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ko/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (French)

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Japanese)

Currently translated at 95.4% (845 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ja/

* Translated using Weblate (Japanese)

Currently translated at 97.1% (860 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ja/

* Translated using Weblate (Polish)

Currently translated at 98.9% (876 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pl/

* Translated using Weblate (Korean)

Currently translated at 97.7% (865 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ko/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (French)

Currently translated at 100.0% (885 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Czech)

Currently translated at 59.0% (523 of 885 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/cs/

Co-authored-by: BViking78 <bviking78@gmail.com>
Co-authored-by: kkk <850345193@qq.com>
Co-authored-by: Coscosname <coscosname@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Aa <jarruraita@outlook.com>
Co-authored-by: MrOV3RDOSE <mrov3rdose@gmail.com>
Co-authored-by: Philip Wang <philpw99@gmail.com>
Co-authored-by: ApxuBbI <nick1232@inbox.lv>
Co-authored-by: alan <wnd1@daum.net>
Co-authored-by: Lauri <stashapp.pot3l@8shield.net>
Co-authored-by: Ikko Ashimine <eltociear@gmail.com>
Co-authored-by: suzuhiroruri <suzuhiroruri@gmail.com>
Co-authored-by: Jin YunHo <justin4758@naver.com>
Co-authored-by: Foucoubou <foucoubou26@email.cz>
2022-11-29 02:14:58 -05:00
WithoutPants
9350be12d9 Fix sortdir not being set in saved filter (#3206) 2022-11-29 12:56:03 +11:00
CJ
02ec98b302 fix card clipping on recomendation row (#3205) 2022-11-29 12:02:52 +11:00
CJ
a8f4c2c29c Reorder new scene fields (#3191) 2022-11-27 07:45:26 +11:00
WithoutPants
57ad12e43b Improve gallery performance (#3183)
* Improve cover resolver performance
* Deprecate and remove usage of slow gallery images
2022-11-25 11:14:50 +11:00
WithoutPants
f0a3a3dd44 Handle bad funscript at values (#3182) 2022-11-25 08:20:23 +11:00
WithoutPants
27998c35a1 Fix buttons not working correct in number filter (#3177) 2022-11-24 08:11:40 +11:00
WithoutPants
03cd9529bd Fix NaN weight in performer scrape dialog 2022-11-23 17:35:44 +11:00
puc9
3072333118 Adding info about duplicate files in various places in the UI (#3054)
* Add counter to File Info where file count > 1
* Add file modification time to File Info panel
* Remove duplicate intl keys
* Add file count to duplicate checker
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-11-23 17:30:51 +11:00
DingDongSoLong4
821587b166 Player mobile improvements (#3120)
* Add videojs-mobile-ui
* Prevent marker wrapping and fix alignment
* Fix marker update on delete
* Change hotkey modifier behaviour
* Update KeyboardShortcuts.md
2022-11-23 15:55:24 +11:00
WithoutPants
b175f1865f Remove python3/python link
Looks like this is already present in the latest alpine image
2022-11-23 14:52:23 +11:00
kermieisinthehouse
5a2242e78d I18N: Add Estonian, Russian first-level langs, add many preview langs (#3176)
* Add Estonian, Russian first-level langs, add many preview langs

* Changelog
2022-11-22 18:27:47 -05:00
WithoutPants
54c495d867 Remove netgo build tag from Windows builds (#3170) 2022-11-23 09:10:39 +11:00
stash-translation-bot
e84221ccbe Translations update from Stash (#3024)
* Translated using Weblate (Japanese)

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ja/

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Korean)

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ko/

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Czech)

Currently translated at 58.4% (494 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/cs/

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.8% (844 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hant/

* Translated using Weblate (German)

Currently translated at 99.8% (844 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/de/

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Added translation using Weblate (Estonian)

* Translated using Weblate (Estonian)

Currently translated at 0.5% (5 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (Estonian)

Currently translated at 37.2% (315 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (French)

Currently translated at 97.8% (827 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (French)

Currently translated at 97.8% (827 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (French)

Currently translated at 97.8% (827 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Danish)

Currently translated at 97.5% (824 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/da/

* Translated using Weblate (French)

Currently translated at 99.2% (839 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (French)

Currently translated at 99.2% (839 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (French)

Currently translated at 99.2% (839 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/et/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.8% (844 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hant/

* Translated using Weblate (Russian)

Currently translated at 75.9% (642 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/en_GB/

* Translated using Weblate (Russian)

Currently translated at 85.6% (724 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (Russian)

Currently translated at 85.6% (724 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (Polish)

Currently translated at 99.8% (844 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pl/

* Translated using Weblate (Ukrainian)

Currently translated at 19.5% (165 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/uk/

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Added translation using Weblate (Bengali)

* Translated using Weblate (Russian)

Currently translated at 91.0% (769 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (Russian)

Currently translated at 91.0% (769 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.7% (843 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hant/

* Translated using Weblate (Russian)

Currently translated at 90.8% (768 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (Bengali (Bangladesh))

Currently translated at 17.1% (145 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/bn_BD/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (845 of 845 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hant/

* Translated using Weblate (Italian)

Currently translated at 100.0% (846 of 846 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/it/

* Translated using Weblate (Polish)

Currently translated at 100.0% (846 of 846 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pl/

* Translated using Weblate (Russian)

Currently translated at 90.7% (768 of 846 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (Italian)

Currently translated at 100.0% (848 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/it/

* Translated using Weblate (Russian)

Currently translated at 90.5% (768 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (Polish)

Currently translated at 100.0% (848 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pl/

* Translated using Weblate (French)

Currently translated at 100.0% (848 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (847 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hans/

* Added translation using Weblate (Persian)

* Translated using Weblate (Persian)

Currently translated at 0.7% (6 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fa/

* Translated using Weblate (Russian)

Currently translated at 90.5% (768 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

* Translated using Weblate (Bengali (Bangladesh))

Currently translated at 26.6% (226 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/bn_BD/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (848 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/sv/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.6% (845 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hant/

* Translated using Weblate (Russian)

Currently translated at 91.1% (773 of 848 strings)

Translation: Stash/Stash Desktop Client
Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ru/

Co-authored-by: 風林火山 <nezoko@digdig.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: MrOV3RDOSE <mrov3rdose@gmail.com>
Co-authored-by: yc <yechan24680@gmail.com>
Co-authored-by: Foucoubou <foucoubou26@email.cz>
Co-authored-by: Still <dev@stillu.cc>
Co-authored-by: HappyAxolotl <qfozc3kmkzc@temp.mailbox.org>
Co-authored-by: Lauri Lepik <stashapp.pot3l@8shield.net>
Co-authored-by: An der <oebro@duck.com>
Co-authored-by: jimao <zhuzibintc@163.com>
Co-authored-by: brestu <brestu@protonmail.com>
Co-authored-by: ApxuBbI <nick1232@inbox.lv>
Co-authored-by: vohibi harcity <vohibi9657@harcity.com>
Co-authored-by: floordiv <real.floordiv@gmail.com>
Co-authored-by: Nayan Hossen <yarbutt2005@gmail.com>
Co-authored-by: Klim Waeknowing <wae@waeknowing.ml>
Co-authored-by: BViking78 <bviking78@gmail.com>
Co-authored-by: Coscosname <coscosname@gmail.com>
Co-authored-by: lhDream <1107053351@qq.com>
Co-authored-by: guoard <afsharzadeh8@gmail.com>
Co-authored-by: Alpaca Serious <srhsgsef@gmail.com>
Co-authored-by: kermieisinthehouse <kermie@isinthe.house>
2022-11-22 16:31:46 -05:00
WithoutPants
8b59a3b014 Fix hang in concurrency tests 2022-11-22 12:24:18 +11:00
WithoutPants
f76a440e54 Update changelog 2022-11-22 10:23:32 +11:00
WithoutPants
aafbba7d77 Prevent hang when deleting while streaming (#3169) 2022-11-22 10:21:27 +11:00
WithoutPants
7bb35b2b09 Handle index.html correctly in custom served folders (#3168)
* getStringMapString return nil if key not found
* Refactor custom routes. Handle /index.html
2022-11-22 10:21:15 +11:00
WithoutPants
af28fd0f3b Fix duplicate downloaded from field 2022-11-21 17:28:49 +11:00
alexandra-3
abc9ec648a Fix a few cases where ffmpeg produces no output (#3161)
* Treat no output from ffmpeg as an error condition
* Distinguish file vs. video duration, and use later where appropriate
* Check for empty file in generateFile

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-11-21 17:21:27 +11:00
WithoutPants
045ba55def Fix startPosition not honoured 2022-11-21 14:57:57 +11:00
CJ
0664c5b974 Track watch activity for scenes. (#3055)
* track watchtime and view time
* add view count sorting, added continue position filter
* display metrics in file info
* add toggle for tracking activity
* save activity every 10 seconds
* reset resume when video is nearly complete
* start from beginning when playing scene in queue

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-11-21 12:55:15 +11:00
WithoutPants
f39fa416a9 Fix database locked errors (#3153)
* Make read-only operations use WithReadTxn
* Allow one database write thread
* Add unit test for concurrent transactions
* Perform some actions after commit to release txn
* Suppress some errors from cancelled context
2022-11-21 06:49:10 +11:00
DingDongSoLong4
420c6fa9d7 Fix duplicate log messages (#3116) 2022-11-18 14:39:30 +11:00
WithoutPants
b1c00a64fc Remove performer age image filter criterion (#3143) 2022-11-17 14:56:17 +11:00
WithoutPants
4ff163d375 Add ResizeObserver polyfill for safari (#3142) 2022-11-17 13:50:41 +11:00
WithoutPants
c1f271fc52 Trim database path in migrate page (#3140) 2022-11-17 12:26:20 +11:00
WithoutPants
2cce547986 Fix regression in marker title generation (#3141) 2022-11-17 12:11:10 +11:00
WithoutPants
f0bf780c2e Use RatingSystem control in RatingFilter (#3133)
* Use RatingSystem control in RatingFilter
* Improve styling for rating on performer page
2022-11-17 10:10:40 +11:00
stg-annon
3660bf2d1a Support filtering by StashID endpoint (#3005)
* Add endpoint to stash_id filter in UI

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-11-17 10:08:15 +11:00
CJ
ca9c8e0a34 Support Javascript injection (#3132) 2022-11-17 09:37:06 +11:00
WithoutPants
3a63f1f9b7 Fire hook when gallery created from image (#3134) 2022-11-17 08:23:08 +11:00
bnkai
ffca8f0c0f Fix rating in SceneCreateInput graphql schema (#3137)
* Fix rating in SceneCreateInput graphql schema
* Fix rating not set at creation

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-11-17 08:22:11 +11:00
WithoutPants
0443439fae Show error if savedFilterId not present (#3129) 2022-11-16 10:09:50 +11:00
WithoutPants
dc820e29af Revert removal of chapters_vtt from ScenePathsType (#3128) 2022-11-16 09:45:45 +11:00
skier233
7eae751d1c Stash rating system (#2830)
* add rating100 fields to represent rating range 1-100
* deprecate existing (1-5) rating fields
* add half- and quarter-star options for rating system
* add decimal rating system option

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-11-16 09:31:44 +11:00
gitgiggety
f66333bac9 Add date filters (#2834)
* graphql: support date and timestamp filter types
* sql: add support for date & timestamp criterions
* ui: add support for date and timestamp criterions
* scenes: add support for filtering by date, created at and updated at
* image: support filtering by created at and updated at
* gallery: support filtering by date, created at and updated at
* movie: support filtering by date, created at and updated at
* studio: support filtering by date, created at and updated at
* tag: support filtering by date, created at and updated at
* performer: support filtering by bitrh & death date and created & updated at
* marker: support filtering by created & updated at and scene date, created & updated at
2022-11-15 11:52:05 +11:00
WithoutPants
ce17230c13 Refactor autotag to use individual transactions (#3106)
* Add id filtering to scenes, images, and galleries
* Perform tagging in batches
* One transaction per object tagged
2022-11-14 17:07:24 +11:00
WithoutPants
4a054ab081 Support file-less scenes. Add scene split, merge and reassign file (#3006)
* Reassign scene file functionality
* Implement scene create
* Add scene create UI
* Add sceneMerge backend support
* Add merge scene to UI
* Populate split create with scene details
* Add merge button to duplicate checker
* Handle file-less scenes in marker preview generate
* Make unique file name for file-less scene exports
* Add o-counter to scene update input
* Hide rescan for file-less scenes
* Generate heatmap if no speed set on file
* Fix count in scene/image queries
2022-11-14 16:35:09 +11:00
WithoutPants
d0b0be4dd4 Fix test database generator (#3112) [skip ci] 2022-11-10 15:01:47 +11:00
WithoutPants
9df66024d1 Make title optional for non-user created galleries (#3110) 2022-11-10 14:19:13 +11:00
WithoutPants
c83ebf7c1c Order performers by name in All (#3111) 2022-11-10 14:18:46 +11:00
WithoutPants
eb795ff9ab Add new fields to scene tagger (#3094)
* Add new fields to scene tagger
* Update scraper docs with new fields
* Set code and director in identify
* Add new fields to identify dialog
2022-11-10 12:51:49 +11:00
WithoutPants
a2ca266cb3 Upgrade to go 1.19 only (#3087)
* Update to go 1.19
* Update cross-compile script
* Add missing targets to cross-compile-all
* Update cache action to remove warning
2022-11-09 14:41:23 +11:00
WithoutPants
3bc0de3f3a Show imperial units for height and weight (#3097)
* Show imperial units for height and weight
* Fix migration note index
2022-11-09 11:10:57 +11:00
WithoutPants
30a7482ddf Order saved filters by name (#3101) 2022-11-09 11:06:11 +11:00
WithoutPants
04514fbc88 Fix changelog 2022-11-08 14:43:31 +11:00
WithoutPants
f1d57c3d62 Update changelog 2022-11-08 14:42:41 +11:00
wildsolutionbroadcast
404a68c994 Limiting how many options are shown in select dropdowns (#3062)
Introducing a limit to how many options are shown in select dropdowns. Fixes an issue I was experiencing where large numbers of options (5000 tags) was causing dropdown to be unresponsive. Does not effect filtering, always shows 'Create "..."' option if it exists, and shows a notice at the bottom of the dropdown of how many options were hidden from the list if any were.
2022-11-08 14:41:24 +11:00
WithoutPants
d2743cf5fb Change performer height to be numeric (#3060)
* Make height an int. Add height_cm field
* Change UI to use height_cm
* Use number fields for height/weight
* Add migration note
2022-11-08 14:09:03 +11:00
WithoutPants
b9e07ade92 Fix seeking (#3096)
* Update apikey when generating/clearing
* Fix seeking on systems with api key
2022-11-08 13:45:54 +11:00
WithoutPants
962bc7df4e Fix panic when fileSize is negative (#3089) 2022-11-08 10:01:32 +11:00
DogmaDragon
f1c454eb09 Typo (#3090) 2022-11-07 18:41:47 +11:00
HappyAxolotl
eff86bf2f8 [Feature] Add fields director and (studio) code to scenes (#3051)
* added schema migration and updated data models
* added code and director to UI
* new fields are exported and imported
* added filters
* Add changelog entry
2022-11-07 18:16:52 +11:00
DingDongSoLong4
7540d3b477 Fix react hook order errors (#3081)
* Fix react hook order error on tag page
* Make all filter hooks actual hooks
2022-11-07 15:49:37 +11:00
DingDongSoLong4
653db3cc1d Scene player improvements (#3020)
* Add types to player plugins
* Use videojs-vtt.js to parse sprite VTT files
* Overhaul scene player
* Replace vtt-thumbnails-freetube
* Remove chapters_vtt
* Force remove shadow from player progress bar
* Cleanup player css
* Rewrite live.ts as middleware
* Don't force play when changing source
2022-11-07 14:53:12 +11:00
WithoutPants
2609095c7a Revert "Upgrade to go 1.19 and update dependencies (#3069)" (#3085)
This reverts commit bba7c23957.
2022-11-07 12:33:15 +11:00
WithoutPants
bba7c23957 Upgrade to go 1.19 and update dependencies (#3069)
* Update to go 1.19
* Update dependencies
* Update cross-compile script
* Add missing targets to cross-compile-all
* Update cache action to remove warning
2022-11-04 13:41:26 +11:00
WithoutPants
f25881a3bf Use alpha2 for country label (#3067) 2022-11-03 09:04:48 +11:00
WithoutPants
270bc317cb Performer refactor (#3057)
* Separate performer model from sqlite model
* Use GenderEnum for gender
2022-10-31 14:58:01 +11:00
DingDongSoLong4
b1fa933868 Fix URL encoding again (#3044)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-10-28 17:46:39 +11:00
DingDongSoLong4
2cd9ef6302 Fix vite circular dependency warning 2022-10-28 06:16:13 +00:00
DingDongSoLong4
db29246883 Fix subtitle error typo (#3056) 2022-10-28 16:38:50 +11:00
InfiniteTF
7b7d6758ef Change performer country value to be ISO code (#1922)
* Change performer country value to be ISO code
* Localize country names
* Use country select for filter

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-10-28 16:37:57 +11:00
DingDongSoLong4
1c0042c4c2 Fix path filters (#3041)
* Fix path filters
* Replace getPathSearchClause
* Remove incorrect tests
2022-10-26 18:48:13 +11:00
stg-annon
a60afc162f Add Tag description filter (#3011)
* init tag description filter

issue with rendering UI filter dialog

* move description to string block
* Add 0.18.0 changelog page
2022-10-26 18:06:52 +11:00
puc9
2b80b6d8d5 Minor UI fix for StashId display in FileInfoPanel (#3050) 2022-10-26 13:57:29 +11:00
A Ghoul Coder
3ac3fe09b8 add pre-ui to default make target and update documentation (#3030)
Without `pre-ui`, `make` fails. `pre-ui` is already used in the GitHub
workflows but omitted from the Makefile and docs.
2022-10-26 09:18:02 +11:00
488 changed files with 23082 additions and 7077 deletions

View File

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

View File

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

8
.gitignore vendored
View File

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

View File

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

View File

@@ -26,7 +26,7 @@ For further information you can [read the in-app manual](ui/v2.5/src/docs/en).
## 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.
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.
@@ -39,32 +39,31 @@ On first run, Stash will prompt you for some configuration options and media dir
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 at the [Community Scrapers Collection](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.
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.
<sub>StashDB is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box).</sub>
<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
[![Translate](https://translate.stashapp.cc/widgets/stash/-/stash-desktop-client/svg-badge.svg)](https://translate.stashapp.cc/engage/stash/)
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇪🇸 🇸🇪 🇹🇼 🇹🇷
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇪🇪 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇷🇺 🇪🇸 🇸🇪 🇹🇼 🇹🇷
Stash is available in 16 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks!
Stash is available in 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!
# Support (FAQ)
Answers to other Frequently Asked Questions can be found [on our Wiki](https://github.com/stashapp/stash/wiki/FAQ)
Check out our documentation on [Stash-Docs](https://docs.stashapp.cc) for information about the software, questions, guides, add-ons and more.
For issues not addressed there, there are a few options.
* Read the [Wiki](https://github.com/stashapp/stash/wiki)
* Check the in-app documentation, in the top right corner of the app (also available [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en)
For more help you can:
* Check the in-app documentation, in the top right corner of the app (it's also mirrored on [Stash-Docs](https://docs.stashapp.cc/in-app-manual))
* Join the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support.
* Start a [discussion on GitHub](https://github.com/stashapp/stash/discussions)
# Customization
## Themes and CSS Customization
There is a [directory of community-created themes](https://github.com/stashapp/stash/wiki/Themes) on our Wiki, along with instructions on how to install them.
There is a [directory of community-created themes](https://docs.stashapp.cc/user-interface-ui/themes) on Stash-Docs, along with instructions on how to install them.
You can also make Stash interface fit your desired style with [Custom CSS snippets](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets).
You can also change the Stash interface to fit your desired style with various snippets from [Custom CSS snippets](https://docs.stashapp.cc/user-interface-ui/custom-css-snippets).
# For Developers

View File

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

View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

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

6
go.sum
View File

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

View File

@@ -46,6 +46,10 @@ fragment ConfigGeneralData on ConfigGeneralResult {
api_key
}
pythonPath
transcodeInputArgs
transcodeOutputArgs
liveTranscodeInputArgs
liveTranscodeOutputArgs
}
fragment ConfigInterfaceData on ConfigInterfaceResult {
@@ -63,6 +67,8 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
showStudioAsText
css
cssEnabled
javascript
javascriptEnabled
customLocales
customLocalesEnabled
language

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
fragment SlimPerformerData on Performer {
id
name
disambiguation
gender
url
twitter
@@ -13,11 +14,12 @@ fragment SlimPerformerData on Performer {
ethnicity
hair_color
eye_color
height
height_cm
fake_tits
career_length
tattoos
piercings
alias_list
tags {
id
name
@@ -26,7 +28,7 @@ fragment SlimPerformerData on Performer {
endpoint
stash_id
}
rating
rating100
death_date
weight
}

View File

@@ -2,6 +2,7 @@ fragment PerformerData on Performer {
id
checksum
name
disambiguation
url
gender
twitter
@@ -10,13 +11,13 @@ fragment PerformerData on Performer {
ethnicity
country
eye_color
height
height_cm
measurements
fake_tits
career_length
tattoos
piercings
aliases
alias_list
favorite
ignore_auto_tag
image_path
@@ -33,7 +34,7 @@ fragment PerformerData on Performer {
stash_id
endpoint
}
rating
rating100
details
death_date
hair_color

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
fragment ScrapedPerformerData on ScrapedPerformer {
stored_id
name
disambiguation
gender
url
twitter
@@ -30,6 +31,7 @@ fragment ScrapedPerformerData on ScrapedPerformer {
fragment ScrapedScenePerformerData on ScrapedPerformer {
stored_id
name
disambiguation
gender
url
twitter
@@ -105,7 +107,9 @@ fragment ScrapedSceneTagData on ScrapedTag {
fragment ScrapedSceneData on ScrapedScene {
title
code
details
director
url
date
image
@@ -166,7 +170,9 @@ fragment ScrapedGalleryData on ScrapedGallery {
fragment ScrapedStashBoxSceneData on ScrapedScene {
title
code
details
director
url
date
image

View File

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

View File

@@ -19,12 +19,13 @@ fragment StudioData on Studio {
scene_count
image_count
gallery_count
performer_count
movie_count
stash_ids {
stash_id
endpoint
}
details
rating
rating100
aliases
}

View File

@@ -41,3 +41,7 @@ mutation MigrateHashNaming {
mutation BackupDatabase($input: BackupDatabaseInput!) {
backupDatabase(input: $input)
}
mutation AnonymiseDatabase($input: AnonymiseDatabaseInput!) {
anonymiseDatabase(input: $input)
}

View File

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

View File

@@ -6,26 +6,27 @@ query MarkerStrings($q: String, $sort: String) {
}
}
query AllTags {
allTags {
...TagData
}
}
query AllPerformersForFilter {
allPerformers {
...SlimPerformerData
id
name
disambiguation
alias_list
}
}
query AllStudiosForFilter {
allStudios {
...SlimStudioData
id
name
aliases
}
}
query AllMoviesForFilter {
allMovies {
...SlimMovieData
id
name
}
}
@@ -67,7 +68,9 @@ query Version {
query LatestVersion {
latestversion {
version
shorthash
release_date
url
}
}

View File

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

View File

@@ -96,7 +96,7 @@ type Query {
"""Scrapes a complete performer record based on a URL"""
scrapePerformerURL(url: String!): ScrapedPerformer
"""Scrapes a complete performer record based on a URL"""
"""Scrapes a complete scene record based on a URL"""
scrapeSceneURL(url: String!): ScrapedScene
"""Scrapes a complete gallery record based on a URL"""
scrapeGalleryURL(url: String!): ScrapedGallery
@@ -144,6 +144,10 @@ type Query {
# Get everything
allScenes: [Scene!]!
allSceneMarkers: [SceneMarker!]!
allImages: [Image!]!
allGalleries: [Gallery!]!
allPerformers: [Performer!]!
allStudios: [Studio!]!
allMovies: [Movie!]!
@@ -155,14 +159,16 @@ type Query {
version: Version!
# LatestVersion
latestversion: ShortVersion!
latestversion: LatestVersion!
}
type Mutation {
setup(input: SetupInput!): Boolean!
migrate(input: MigrateInput!): Boolean!
sceneCreate(input: SceneCreateInput!): Scene
sceneUpdate(input: SceneUpdateInput!): Scene
sceneMerge(input: SceneMergeInput!): Scene
bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!]
sceneDestroy(input: SceneDestroyInput!): Boolean!
scenesDestroy(input: ScenesDestroyInput!): Boolean!
@@ -175,6 +181,12 @@ type Mutation {
"""Resets the o-counter for a scene to 0. Returns the new value"""
sceneResetO(id: ID!): Int!
"""Sets the resume time point (if provided) and adds the provided duration to the scene's play duration"""
sceneSaveActivity(id: ID!, resume_time: Float, playDuration: Float): Boolean!
"""Increments the play count for the scene. Returns the new play count value."""
sceneIncrementPlayCount(id: ID!): Int!
"""Generates screenshot at specified time in seconds. Leave empty to generate default screenshot"""
sceneGenerateScreenshot(id: ID!, at: Float): String!
@@ -182,6 +194,8 @@ type Mutation {
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean!
sceneAssignFile(input: AssignSceneFileInput!): Boolean!
imageUpdate(input: ImageUpdateInput!): Image
bulkImageUpdate(input: BulkImageUpdateInput!): [Image!]
imageDestroy(input: ImageDestroyInput!): Boolean!
@@ -271,6 +285,9 @@ type Mutation {
metadataIdentify(input: IdentifyMetadataInput!): ID!
"""Migrate generated files for the current hash naming"""
migrateHashNaming: ID!
"""Anonymise the database in a separate file. Optionally returns a link to download the database file"""
anonymiseDatabase(input: AnonymiseDatabaseInput!): String
"""Reload scrapers"""
reloadScrapers: Boolean!

View File

@@ -69,6 +69,21 @@ input ConfigGeneralInput {
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""ffmpeg transcode input args - injected before input file
These are applied to generated transcodes (previews and transcodes)"""
transcodeInputArgs: [String!]
"""ffmpeg transcode output args - injected before output file
These are applied to generated transcodes (previews and transcodes)"""
transcodeOutputArgs: [String!]
"""ffmpeg stream input args - injected before input file
These are applied when live transcoding"""
liveTranscodeInputArgs: [String!]
"""ffmpeg stream output args - injected before output file
These are applied when live transcoding"""
liveTranscodeOutputArgs: [String!]
"""Write image thumbnails to disk when generating on the fly"""
writeImageThumbnails: Boolean
"""Username"""
@@ -152,6 +167,21 @@ type ConfigGeneralResult {
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""ffmpeg transcode input args - injected before input file
These are applied to generated transcodes (previews and transcodes)"""
transcodeInputArgs: [String!]!
"""ffmpeg transcode output args - injected before output file
These are applied to generated transcodes (previews and transcodes)"""
transcodeOutputArgs: [String!]!
"""ffmpeg stream input args - injected before input file
These are applied when live transcoding"""
liveTranscodeInputArgs: [String!]!
"""ffmpeg stream output args - injected before output file
These are applied when live transcoding"""
liveTranscodeOutputArgs: [String!]!
"""Write image thumbnails to disk when generating on the fly"""
writeImageThumbnails: Boolean!
"""API Key"""
@@ -264,6 +294,10 @@ input ConfigInterfaceInput {
css: String
cssEnabled: Boolean
"""Custom Javascript"""
javascript: String
javascriptEnabled: Boolean
"""Custom Locales"""
customLocales: String
customLocalesEnabled: Boolean
@@ -330,6 +364,10 @@ type ConfigInterfaceResult {
css: String
cssEnabled: Boolean
"""Custom Javascript"""
javascript: String
javascriptEnabled: Boolean
"""Custom Locales"""
customLocales: String
customLocalesEnabled: Boolean

View File

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

View File

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

View File

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

View File

@@ -263,6 +263,10 @@ input BackupDatabaseInput {
download: Boolean
}
input AnonymiseDatabaseInput {
download: Boolean
}
enum SystemStatusEnum {
SETUP
NEEDS_MIGRATION

View File

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

View File

@@ -9,8 +9,9 @@ enum GenderEnum {
type Performer {
id: ID!
checksum: String!
name: String
checksum: String @deprecated(reason: "Not used")
name: String!
disambiguation: String
url: String
gender: GenderEnum
twitter: String
@@ -19,13 +20,15 @@ type Performer {
ethnicity: String
country: String
eye_color: String
height: String
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
aliases: String @deprecated(reason: "Use alias_list")
alias_list: [String!]!
favorite: Boolean!
tags: [Tag!]!
ignore_auto_tag: Boolean!
@@ -36,7 +39,10 @@ type Performer {
gallery_count: Int # Resolver
scenes: [Scene!]!
stash_ids: [StashID!]!
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
death_date: String
hair_color: String
@@ -49,19 +55,23 @@ type Performer {
input PerformerCreateInput {
name: String!
disambiguation: String
url: String
gender: GenderEnum
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
# height must be parsable into an integer
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
aliases: String @deprecated(reason: "Use alias_list")
alias_list: [String!]
twitter: String
instagram: String
favorite: Boolean
@@ -69,7 +79,10 @@ input PerformerCreateInput {
"""This should be a URL or a base64 encoded data URL"""
image: String
stash_ids: [StashIDInput!]
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
death_date: String
hair_color: String
@@ -80,19 +93,23 @@ input PerformerCreateInput {
input PerformerUpdateInput {
id: ID!
name: String
disambiguation: String
url: String
gender: GenderEnum
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
# height must be parsable into an integer
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
aliases: String @deprecated(reason: "Use alias_list")
alias_list: [String!]
twitter: String
instagram: String
favorite: Boolean
@@ -100,7 +117,10 @@ input PerformerUpdateInput {
"""This should be a URL or a base64 encoded data URL"""
image: String
stash_ids: [StashIDInput!]
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
death_date: String
hair_color: String
@@ -108,27 +128,39 @@ input PerformerUpdateInput {
ignore_auto_tag: Boolean
}
input BulkUpdateStrings {
values: [String!]
mode: BulkUpdateIdMode!
}
input BulkPerformerUpdateInput {
clientMutationId: String
ids: [ID!]
disambiguation: String
url: String
gender: GenderEnum
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
# height must be parsable into an integer
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
aliases: String @deprecated(reason: "Use alias_list")
alias_list: BulkUpdateStrings
twitter: String
instagram: String
favorite: Boolean
tag_ids: BulkUpdateIds
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
death_date: String
hair_color: String

View File

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

View File

@@ -3,6 +3,7 @@ type ScrapedPerformer {
"""Set if performer matched"""
stored_id: ID
name: String
disambiguation: String
gender: String
url: String
twitter: String
@@ -17,6 +18,7 @@ type ScrapedPerformer {
career_length: String
tattoos: String
piercings: String
# aliases must be comma-delimited to be parsed correctly
aliases: String
tags: [ScrapedTag!]
@@ -34,6 +36,7 @@ input ScrapedPerformerInput {
"""Set if performer matched"""
stored_id: ID
name: String
disambiguation: String
gender: String
url: String
twitter: String

View File

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

View File

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

View File

@@ -4,7 +4,9 @@ type Version {
build_time: String!
}
type ShortVersion {
type LatestVersion {
version: String!
shorthash: String!
release_date: String!
url: String!
}

View File

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

View File

@@ -17,12 +17,12 @@ const loginEndPoint = "/login"
const (
tripwireActivatedErrMsg = "Stash is exposed to the public internet without authentication, and is not serving any more content to protect your privacy. " +
"More information and fixes are available at https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet"
"More information and fixes are available at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
externalAccessErrMsg = "You have attempted to access Stash over the internet, and authentication is not enabled. " +
"This is extremely dangerous! The whole world can see your your stash page and browse your files! " +
"Stash is not answering any other requests to protect your privacy. " +
"Please read the log entry or visit https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet"
"Please read the log entry or visit https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
)
func allowUnauthenticated(r *http.Request) bool {
@@ -85,12 +85,16 @@ func authenticateHandler() func(http.Handler) http.Handler {
prefix := getProxyPrefix(r.Header)
// otherwise redirect to the login page
u := url.URL{
Path: prefix + "/login",
returnURL := url.URL{
Path: prefix + r.URL.Path,
RawQuery: r.URL.RawQuery,
}
q := make(url.Values)
q.Set(returnURLParam, returnURL.String())
u := url.URL{
Path: prefix + "/login",
RawQuery: q.Encode(),
}
q := u.Query()
q.Set(returnURLParam, prefix+r.URL.Path)
u.RawQuery = q.Encode()
http.Redirect(w, r, u.String(), http.StatusFound)
return
}

View File

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

View File

@@ -20,12 +20,7 @@ import (
const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases"
const apiTags string = "https://api.github.com/repos/stashapp/stash/tags"
const apiAcceptHeader string = "application/vnd.github.v3+json"
const developmentTag string = "latest_develop"
const defaultSHLength int = 7 // default length of SHA short hash returned by <git rev-parse --short HEAD>
// ErrNoVersion indicates that no version information has been embedded in the
// stash binary
var ErrNoVersion = errors.New("no stash version")
const defaultSHLength int = 8 // default length of SHA short hash returned by <git rev-parse --short HEAD>
var stashReleases = func() map[string]string {
return map[string]string{
@@ -108,9 +103,21 @@ type githubTagResponse struct {
Node_id string
}
type LatestRelease struct {
Version string
Hash string
ShortHash string
Date string
Url string
}
func makeGithubRequest(ctx context.Context, url string, output interface{}) error {
transport := &http.Transport{Proxy: http.ProxyFromEnvironment}
client := &http.Client{
Timeout: 3 * time.Second,
Timeout: 3 * time.Second,
Transport: transport,
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
@@ -144,14 +151,16 @@ func makeGithubRequest(ctx context.Context, url string, output interface{}) erro
return nil
}
// GetLatestVersion gets latest version (git commit hash) from github API
// GetLatestRelease gets latest release information from github API
// If running a build from the "master" branch, then the latest full release
// is used, otherwise it uses the release that is tagged with "latest_develop"
// which is the latest pre-release build.
func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string, latestRelease string, err error) {
func GetLatestRelease(ctx context.Context) (*LatestRelease, error) {
arch := runtime.GOARCH
arch := runtime.GOARCH // https://en.wikipedia.org/wiki/Comparison_of_ARM_cores
isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4 // armv6 doesn't support any of these features
// https://en.wikipedia.org/wiki/Comparison_of_ARM_cores
// armv6 doesn't support any of these features
isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4
if arch == "arm" && isARMv7 {
arch = "armv7"
}
@@ -159,125 +168,100 @@ func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string
platform := fmt.Sprintf("%s/%s", runtime.GOOS, arch)
wantedRelease := stashReleases()[platform]
version, _, _ := GetVersion()
if version == "" {
return "", "", ErrNoVersion
}
// if the version is suffixed with -x-xxxx, then we are running a development build
usePreRelease := false
re := regexp.MustCompile(`-\d+-g\w+$`)
if re.MatchString(version) {
usePreRelease = true
}
url := apiReleases
if !usePreRelease {
// just get the latest full release
url += "/latest"
var release githubReleasesResponse
if IsDevelop() {
// get the latest release, prerelease or not
releases := []githubReleasesResponse{}
err := makeGithubRequest(ctx, apiReleases+"?per_page=1", &releases)
if err != nil {
return nil, err
}
release = releases[0]
} else {
// get the release tagged with the development tag
url += "/tags/" + developmentTag
// just get the latest full release
err := makeGithubRequest(ctx, apiReleases+"/latest", &release)
if err != nil {
return nil, err
}
}
release := githubReleasesResponse{}
err = makeGithubRequest(ctx, url, &release)
version := release.Name
if release.Prerelease {
// find version in prerelease name
re := regexp.MustCompile(`v[\w-\.]+-\d+-g[0-9a-f]+`)
if match := re.FindString(version); match != "" {
version = match
}
}
latestHash, err := getReleaseHash(ctx, release.Tag_name)
if err != nil {
return "", "", err
return nil, err
}
if release.Prerelease == usePreRelease {
latestVersion = getReleaseHash(ctx, release, shortHash, usePreRelease)
var releaseDate string
if publishedAt, err := time.Parse(time.RFC3339, release.Published_at); err == nil {
releaseDate = publishedAt.Format("2006-01-02")
}
if wantedRelease != "" {
for _, asset := range release.Assets {
if asset.Name == wantedRelease {
latestRelease = asset.Browser_download_url
break
}
var releaseUrl string
if wantedRelease != "" {
for _, asset := range release.Assets {
if asset.Name == wantedRelease {
releaseUrl = asset.Browser_download_url
break
}
}
}
if latestVersion == "" {
return "", "", fmt.Errorf("no version found for \"%s\"", version)
_, githash, _ := GetVersion()
shLength := len(githash)
if shLength == 0 {
shLength = defaultSHLength
}
return latestVersion, latestRelease, nil
return &LatestRelease{
Version: version,
Hash: latestHash,
ShortHash: latestHash[:shLength],
Date: releaseDate,
Url: releaseUrl,
}, nil
}
func getReleaseHash(ctx context.Context, release githubReleasesResponse, shortHash bool, usePreRelease bool) string {
shaLength := len(release.Target_commitish)
// the /latest API call doesn't return the hash in target_commitish
// also add sanity check in case Target_commitish is not 40 characters
if !usePreRelease || shaLength != 40 {
return getShaFromTags(ctx, shortHash, release.Tag_name)
}
if shortHash {
last := defaultSHLength // default length of git short hash
_, gitShort, _ := GetVersion() // retrieve it to check actual length
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
last = len(gitShort)
}
return release.Target_commitish[0:last]
}
return release.Target_commitish
}
func printLatestVersion(ctx context.Context) {
_, githash, _ = GetVersion()
latest, _, err := GetLatestVersion(ctx, true)
if err != nil {
logger.Errorf("Couldn't find latest version: %s", err)
} else {
if githash == latest {
logger.Infof("Version: (%s) is already the latest released.", latest)
} else {
logger.Infof("New version: (%s) available.", latest)
}
}
}
// get sha from the github api tags endpoint
// returns the sha1 hash/shorthash or "" if something's wrong
func getShaFromTags(ctx context.Context, shortHash bool, name string) string {
func getReleaseHash(ctx context.Context, tagName string) (string, error) {
url := apiTags
tags := []githubTagResponse{}
err := makeGithubRequest(ctx, url, &tags)
if err != nil {
// If the context is canceled, we don't want to log this as an error
// in the path. The function here just gives up and returns "" if
// something goes wrong. Hence, log the error at the info-level so
// it's still present, but don't treat this as an error.
if errors.Is(err, context.Canceled) {
logger.Infof("aborting sha request due to context cancellation")
} else {
logger.Errorf("Github Tags Api: %v", err)
}
return ""
return "", err
}
_, gitShort, _ := GetVersion() // retrieve short hash to check actual length
for _, tag := range tags {
if tag.Name == name {
shaLength := len(tag.Commit.Sha)
if shaLength != 40 {
return ""
if tag.Name == tagName {
if len(tag.Commit.Sha) != 40 {
return "", errors.New("invalid Github API response")
}
if shortHash {
last := defaultSHLength // default length of git short hash
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
last = len(gitShort)
}
return tag.Commit.Sha[0:last]
}
return tag.Commit.Sha
return tag.Commit.Sha, nil
}
}
return ""
return "", errors.New("invalid Github API response")
}
func printLatestVersion(ctx context.Context) {
latestRelease, err := GetLatestRelease(ctx)
if err != nil {
logger.Errorf("Couldn't retrieve latest version: %v", err)
} else {
_, githash, _ = GetVersion()
switch {
case githash == "":
logger.Infof("Latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
case githash == latestRelease.ShortHash:
logger.Infof("Version %s (%s) is already the latest released", latestRelease.Version, latestRelease.ShortHash)
default:
logger.Infof("New version available: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
}
}
}

View File

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

View File

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

View File

@@ -95,8 +95,12 @@ func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) err
return txn.WithTxn(ctx, r.txnManager, fn)
}
func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithReadTxn(ctx, r.txnManager, fn)
}
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.Wall(ctx, q)
return err
}); err != nil {
@@ -106,7 +110,7 @@ func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*model
}
func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models.Scene, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.Wall(ctx, q)
return err
}); err != nil {
@@ -117,7 +121,7 @@ func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models
}
func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *string) (ret []*models.MarkerStringsResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.GetMarkerStrings(ctx, q, sort)
return err
}); err != nil {
@@ -129,7 +133,7 @@ func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *stri
func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
var ret StatsResultType
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
repo := r.repository
scenesQB := repo.Scene
imageQB := repo.Image
@@ -180,19 +184,22 @@ func (r *queryResolver) Version(ctx context.Context) (*Version, error) {
}, nil
}
// Latestversion returns the latest git shorthash commit.
func (r *queryResolver) Latestversion(ctx context.Context) (*ShortVersion, error) {
ver, url, err := GetLatestVersion(ctx, true)
if err == nil {
logger.Infof("Retrieved latest hash: %s", ver)
} else {
logger.Errorf("Error while retrieving latest hash: %s", err)
func (r *queryResolver) Latestversion(ctx context.Context) (*LatestVersion, error) {
latestRelease, err := GetLatestRelease(ctx)
if err != nil {
if !errors.Is(err, context.Canceled) {
logger.Errorf("Error while retrieving latest version: %v", err)
}
return nil, err
}
logger.Infof("Retrieved latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
return &ShortVersion{
Shorthash: ver,
URL: url,
}, err
return &LatestVersion{
Version: latestRelease.Version,
Shorthash: latestRelease.ShortHash,
ReleaseDate: latestRelease.Date,
URL: latestRelease.Url,
}, nil
}
// Get scene marker tags which show up under the video.
@@ -205,7 +212,7 @@ func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([
var keys []int
tags := make(map[int]*SceneMarkerTag)
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneMarkers, err := r.repository.SceneMarker.FindBySceneID(ctx, sceneID)
if err != nil {
return err

View File

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

View File

@@ -75,6 +75,14 @@ func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFile
}, nil
}
func (r *imageResolver) Date(ctx context.Context, obj *models.Image) (*string, error) {
if obj.Date != nil {
result := obj.Date.String()
return &result, nil
}
return nil, nil
}
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) {
files, err := r.getFiles(ctx, obj)
if err != nil {
@@ -132,7 +140,7 @@ func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*ImagePat
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret []*models.Gallery, err error) {
if !obj.GalleryIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadGalleryIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err
@@ -144,6 +152,18 @@ func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret [
return ret, firstError(errs)
}
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *imageResolver) Rating100(ctx context.Context, obj *models.Image) (*int, error) {
return obj.Rating, nil
}
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *models.Studio, err error) {
if obj.StudioID == nil {
return nil, nil
@@ -154,7 +174,7 @@ func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *mod
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err
@@ -168,7 +188,7 @@ func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*mod
func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret []*models.Performer, err error) {
if !obj.PerformerIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPerformerIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err

View File

@@ -48,6 +48,14 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e
}
func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, error) {
if obj.Rating.Valid {
rating := models.Rating100To5(int(obj.Rating.Int64))
return &rating, nil
}
return nil, nil
}
func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
@@ -86,7 +94,7 @@ func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (
func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
// don't return any thing if there is no back image
var img []byte
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
img, err = r.repository.Movie.GetBackImage(ctx, obj.ID)
if err != nil {
@@ -109,7 +117,7 @@ func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Scene.CountByMovieID(ctx, obj.ID)
return err
}); err != nil {
@@ -120,7 +128,7 @@ func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret
}
func (r *movieResolver) Scenes(ctx context.Context, obj *models.Movie) (ret []*models.Scene, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Scene.FindByMovieID(ctx, obj.ID)
return err

View File

@@ -2,137 +2,64 @@ package api
import (
"context"
"time"
"strconv"
"strings"
"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/image"
"github.com/stashapp/stash/pkg/models"
)
func (r *performerResolver) Name(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Name.Valid {
return &obj.Name.String, nil
}
return nil, nil
}
func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.URL.Valid {
return &obj.URL.String, nil
}
return nil, nil
}
func (r *performerResolver) Gender(ctx context.Context, obj *models.Performer) (*models.GenderEnum, error) {
var ret models.GenderEnum
if obj.Gender.Valid {
ret = models.GenderEnum(obj.Gender.String)
if ret.IsValid() {
return &ret, nil
}
}
return nil, nil
}
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Twitter.Valid {
return &obj.Twitter.String, nil
}
return nil, nil
}
func (r *performerResolver) Instagram(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Instagram.Valid {
return &obj.Instagram.String, nil
}
return nil, nil
}
func (r *performerResolver) Birthdate(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Birthdate.Valid {
return &obj.Birthdate.String, nil
}
return nil, nil
}
func (r *performerResolver) Ethnicity(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Ethnicity.Valid {
return &obj.Ethnicity.String, nil
}
return nil, nil
}
func (r *performerResolver) Country(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Country.Valid {
return &obj.Country.String, nil
}
return nil, nil
}
func (r *performerResolver) EyeColor(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.EyeColor.Valid {
return &obj.EyeColor.String, nil
}
return nil, nil
}
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Height.Valid {
return &obj.Height.String, nil
}
return nil, nil
}
func (r *performerResolver) Measurements(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Measurements.Valid {
return &obj.Measurements.String, nil
}
return nil, nil
}
func (r *performerResolver) FakeTits(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.FakeTits.Valid {
return &obj.FakeTits.String, nil
}
return nil, nil
}
func (r *performerResolver) CareerLength(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.CareerLength.Valid {
return &obj.CareerLength.String, nil
}
return nil, nil
}
func (r *performerResolver) Tattoos(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Tattoos.Valid {
return &obj.Tattoos.String, nil
}
return nil, nil
}
func (r *performerResolver) Piercings(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Piercings.Valid {
return &obj.Piercings.String, nil
}
// 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.Valid {
return &obj.Aliases.String, nil
if !obj.Aliases.Loaded() {
if err := r.withTxn(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.withTxn(ctx, func(ctx context.Context) error {
return obj.LoadAliases(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
return obj.Aliases.List(), nil
}
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Height != nil {
ret := strconv.Itoa(*obj.Height)
return &ret, nil
}
return nil, nil
}
func (r *performerResolver) Favorite(ctx context.Context, obj *models.Performer) (bool, error) {
if obj.Favorite.Valid {
return obj.Favorite.Bool, nil
func (r *performerResolver) HeightCm(ctx context.Context, obj *models.Performer) (*int, error) {
return obj.Height, nil
}
func (r *performerResolver) Birthdate(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Birthdate != nil {
ret := obj.Birthdate.String()
return &ret, nil
}
return false, nil
return nil, nil
}
func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer) (*string, error) {
@@ -142,19 +69,22 @@ func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer
}
func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
return ret, firstError(errs)
}
func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Scene.CountByPerformerID(ctx, obj.ID)
return err
}); err != nil {
@@ -166,7 +96,7 @@ func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performe
func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = image.CountByPerformerID(ctx, r.repository.Image, obj.ID)
return err
}); err != nil {
@@ -178,7 +108,7 @@ func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performe
func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = gallery.CountByPerformerID(ctx, r.repository.Gallery, obj.ID)
return err
}); err != nil {
@@ -189,7 +119,7 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
}
func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (ret []*models.Scene, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
@@ -200,65 +130,37 @@ func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (
}
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) {
var ret []models.StashID
if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Performer.GetStashIDs(ctx, obj.ID)
return err
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadStashIDs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
return stashIDsSliceToPtrSlice(ret), nil
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
}
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *performerResolver) Details(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Details.Valid {
return &obj.Details.String, nil
}
return nil, nil
func (r *performerResolver) Rating100(ctx context.Context, obj *models.Performer) (*int, error) {
return obj.Rating, nil
}
func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.DeathDate.Valid {
return &obj.DeathDate.String, nil
if obj.DeathDate != nil {
ret := obj.DeathDate.String()
return &ret, nil
}
return nil, nil
}
func (r *performerResolver) HairColor(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.HairColor.Valid {
return &obj.HairColor.String, nil
}
return nil, nil
}
func (r *performerResolver) Weight(ctx context.Context, obj *models.Performer) (*int, error) {
if obj.Weight.Valid {
weight := int(obj.Weight.Int64)
return &weight, nil
}
return nil, nil
}
func (r *performerResolver) CreatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *performerResolver) UpdatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
@@ -270,7 +172,7 @@ func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
return err
}); err != nil {

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import (
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/performer"
)
func (r *studioResolver) Name(ctx context.Context, obj *models.Studio) (string, error) {
@@ -30,7 +31,7 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj).GetStudioImageURL()
var hasImage bool
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
hasImage, err = r.repository.Studio.HasImage(ctx, obj.ID)
return err
@@ -47,7 +48,7 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
}
func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) (ret []string, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.GetAliases(ctx, obj.ID)
return err
}); err != nil {
@@ -59,7 +60,7 @@ func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) (ret [
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Scene.CountByStudioID(ctx, obj.ID)
return err
}); err != nil {
@@ -71,7 +72,7 @@ func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (re
func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = image.CountByStudioID(ctx, r.repository.Image, obj.ID)
return err
}); err != nil {
@@ -83,7 +84,7 @@ func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (re
func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = gallery.CountByStudioID(ctx, r.repository.Gallery, obj.ID)
return err
}); err != nil {
@@ -93,6 +94,18 @@ func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) (
return &res, nil
}
func (r *studioResolver) PerformerCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = performer.CountByStudioID(ctx, r.repository.Performer, obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
if !obj.ParentID.Valid {
return nil, nil
@@ -102,7 +115,7 @@ func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (
}
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (ret []*models.Studio, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.FindChildren(ctx, obj.ID)
return err
}); err != nil {
@@ -114,7 +127,7 @@ func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*models.StashID, error) {
var ret []models.StashID
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Studio.GetStashIDs(ctx, obj.ID)
return err
@@ -126,6 +139,14 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*m
}
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
if obj.Rating.Valid {
rating := models.Rating100To5(int(obj.Rating.Int64))
return &rating, nil
}
return nil, nil
}
func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
@@ -149,7 +170,7 @@ func (r *studioResolver) UpdatedAt(ctx context.Context, obj *models.Studio) (*ti
}
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
return err
}); err != nil {
@@ -161,7 +182,7 @@ func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Movie.CountByStudioID(ctx, obj.ID)
return err
}); err != nil {

View File

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

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"path/filepath"
"regexp"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
@@ -58,7 +59,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
validateDir := func(key string, value string, optional bool) error {
if err := checkConfigOverride(config.Metadata); err != nil {
if err := checkConfigOverride(key); err != nil {
return err
}
@@ -227,10 +228,22 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
if input.Excludes != nil {
for _, r := range input.Excludes {
_, err := regexp.Compile(r)
if err != nil {
return makeConfigGeneralResult(), fmt.Errorf("video exclusion pattern '%v' invalid: %w", r, err)
}
}
c.Set(config.Exclude, input.Excludes)
}
if input.ImageExcludes != nil {
for _, r := range input.ImageExcludes {
_, err := regexp.Compile(r)
if err != nil {
return makeConfigGeneralResult(), fmt.Errorf("image/gallery exclusion pattern '%v' invalid: %w", r, err)
}
}
c.Set(config.ImageExclude, input.ImageExcludes)
}
@@ -280,6 +293,19 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
c.Set(config.PythonPath, input.PythonPath)
}
if input.TranscodeInputArgs != nil {
c.Set(config.TranscodeInputArgs, input.TranscodeInputArgs)
}
if input.TranscodeOutputArgs != nil {
c.Set(config.TranscodeOutputArgs, input.TranscodeOutputArgs)
}
if input.LiveTranscodeInputArgs != nil {
c.Set(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs)
}
if input.LiveTranscodeOutputArgs != nil {
c.Set(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs)
}
if err := c.Write(); err != nil {
return makeConfigGeneralResult(), err
}
@@ -365,6 +391,12 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
setBool(config.CSSEnabled, input.CSSEnabled)
if input.Javascript != nil {
c.SetJavascript(*input.Javascript)
}
setBool(config.JavascriptEnabled, input.JavascriptEnabled)
if input.CustomLocales != nil {
c.SetCustomLocales(*input.CustomLocales)
}
@@ -445,6 +477,12 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigSc
}
if input.ExcludeTagPatterns != nil {
for _, r := range input.ExcludeTagPatterns {
_, err := regexp.Compile(r)
if err != nil {
return makeConfigScrapingResult(), fmt.Errorf("tag exclusion pattern '%v' invalid: %w", r, err)
}
}
c.Set(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns)
}

View File

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

View File

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

View File

@@ -156,3 +156,55 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
return nil, nil
}
func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input AnonymiseDatabaseInput) (*string, error) {
// 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 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)
if err != nil {
logger.Errorf("Error anonymising database: %v", err)
return nil, err
}
if download {
downloadHash, err := mgr.DownloadStore.RegisterFile(outPath, "", false)
if err != nil {
return nil, fmt.Errorf("error registering file for download: %w", err)
}
logger.Debugf("Generated anonymised file %s with hash %s", outPath, downloadHash)
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
fn := filepath.Base(database.DatabaseBackupPath(""))
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
return &ret, nil
} else {
logger.Infof("Successfully anonymised database to: %s", outPath)
}
return nil, nil
}

View File

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

View File

@@ -2,12 +2,10 @@ package api
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
"github.com/stashapp/stash/pkg/hash/md5"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/performer"
"github.com/stashapp/stash/pkg/plugin"
@@ -37,9 +35,6 @@ func stashIDPtrSliceToSlice(v []*models.StashID) []models.StashID {
}
func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerCreateInput) (*models.Performer, error) {
// generate checksum from performer name rather than image
checksum := md5.FromString(input.Name)
var imageData []byte
var err error
@@ -51,81 +46,99 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
return nil, err
}
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
// Populate a new performer from the input
currentTime := time.Now()
newPerformer := models.Performer{
Checksum: checksum,
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
Name: input.Name,
TagIDs: models.NewRelatedIDs(tagIDs),
StashIDs: models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)),
CreatedAt: currentTime,
UpdatedAt: currentTime,
}
if input.Disambiguation != nil {
newPerformer.Disambiguation = *input.Disambiguation
}
newPerformer.Name = sql.NullString{String: input.Name, Valid: true}
if input.URL != nil {
newPerformer.URL = sql.NullString{String: *input.URL, Valid: true}
newPerformer.URL = *input.URL
}
if input.Gender != nil {
newPerformer.Gender = sql.NullString{String: input.Gender.String(), Valid: true}
newPerformer.Gender = *input.Gender
}
if input.Birthdate != nil {
newPerformer.Birthdate = models.SQLiteDate{String: *input.Birthdate, Valid: true}
d := models.NewDate(*input.Birthdate)
newPerformer.Birthdate = &d
}
if input.Ethnicity != nil {
newPerformer.Ethnicity = sql.NullString{String: *input.Ethnicity, Valid: true}
newPerformer.Ethnicity = *input.Ethnicity
}
if input.Country != nil {
newPerformer.Country = sql.NullString{String: *input.Country, Valid: true}
newPerformer.Country = *input.Country
}
if input.EyeColor != nil {
newPerformer.EyeColor = sql.NullString{String: *input.EyeColor, Valid: true}
newPerformer.EyeColor = *input.EyeColor
}
if input.Height != nil {
newPerformer.Height = sql.NullString{String: *input.Height, Valid: true}
// prefer height_cm over height
if input.HeightCm != nil {
newPerformer.Height = input.HeightCm
} else if input.Height != nil {
h, err := strconv.Atoi(*input.Height)
if err != nil {
return nil, fmt.Errorf("invalid height: %s", *input.Height)
}
newPerformer.Height = &h
}
if input.Measurements != nil {
newPerformer.Measurements = sql.NullString{String: *input.Measurements, Valid: true}
newPerformer.Measurements = *input.Measurements
}
if input.FakeTits != nil {
newPerformer.FakeTits = sql.NullString{String: *input.FakeTits, Valid: true}
newPerformer.FakeTits = *input.FakeTits
}
if input.CareerLength != nil {
newPerformer.CareerLength = sql.NullString{String: *input.CareerLength, Valid: true}
newPerformer.CareerLength = *input.CareerLength
}
if input.Tattoos != nil {
newPerformer.Tattoos = sql.NullString{String: *input.Tattoos, Valid: true}
newPerformer.Tattoos = *input.Tattoos
}
if input.Piercings != nil {
newPerformer.Piercings = sql.NullString{String: *input.Piercings, Valid: true}
newPerformer.Piercings = *input.Piercings
}
if input.Aliases != nil {
newPerformer.Aliases = sql.NullString{String: *input.Aliases, Valid: true}
if input.AliasList != nil {
newPerformer.Aliases = models.NewRelatedStrings(input.AliasList)
} else if input.Aliases != nil {
newPerformer.Aliases = models.NewRelatedStrings(stringslice.FromString(*input.Aliases, ","))
}
if input.Twitter != nil {
newPerformer.Twitter = sql.NullString{String: *input.Twitter, Valid: true}
newPerformer.Twitter = *input.Twitter
}
if input.Instagram != nil {
newPerformer.Instagram = sql.NullString{String: *input.Instagram, Valid: true}
newPerformer.Instagram = *input.Instagram
}
if input.Favorite != nil {
newPerformer.Favorite = sql.NullBool{Bool: *input.Favorite, Valid: true}
} else {
newPerformer.Favorite = sql.NullBool{Bool: false, Valid: true}
newPerformer.Favorite = *input.Favorite
}
if input.Rating != nil {
newPerformer.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
newPerformer.Rating = sql.NullInt64{Valid: false}
if input.Rating100 != nil {
newPerformer.Rating = input.Rating100
} else if input.Rating != nil {
rating := models.Rating5To100(*input.Rating)
newPerformer.Rating = &rating
}
if input.Details != nil {
newPerformer.Details = sql.NullString{String: *input.Details, Valid: true}
newPerformer.Details = *input.Details
}
if input.DeathDate != nil {
newPerformer.DeathDate = models.SQLiteDate{String: *input.DeathDate, Valid: true}
d := models.NewDate(*input.DeathDate)
newPerformer.DeathDate = &d
}
if input.HairColor != nil {
newPerformer.HairColor = sql.NullString{String: *input.HairColor, Valid: true}
newPerformer.HairColor = *input.HairColor
}
if input.Weight != nil {
weight := int64(*input.Weight)
newPerformer.Weight = sql.NullInt64{Int64: weight, Valid: true}
newPerformer.Weight = input.Weight
}
if input.IgnoreAutoTag != nil {
newPerformer.IgnoreAutoTag = *input.IgnoreAutoTag
@@ -138,32 +151,17 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
}
// Start the transaction and save the performer
var performer *models.Performer
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
performer, err = qb.Create(ctx, newPerformer)
err = qb.Create(ctx, &newPerformer)
if err != nil {
return err
}
if len(input.TagIds) > 0 {
if err := r.updatePerformerTags(ctx, performer.ID, input.TagIds); err != nil {
return err
}
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(ctx, performer.ID, imageData); err != nil {
return err
}
}
// Save the stash_ids
if input.StashIds != nil {
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, performer.ID, stashIDJoins); err != nil {
if err := qb.UpdateImage(ctx, newPerformer.ID, imageData); err != nil {
return err
}
}
@@ -173,17 +171,14 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, performer.ID, plugin.PerformerCreatePost, input, nil)
return r.getPerformer(ctx, performer.ID)
r.hookExecutor.ExecutePostHooks(ctx, newPerformer.ID, plugin.PerformerCreatePost, input, nil)
return r.getPerformer(ctx, newPerformer.ID)
}
func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerUpdateInput) (*models.Performer, error) {
// Populate performer from the input
performerID, _ := strconv.Atoi(input.ID)
updatedPerformer := models.PerformerPartial{
ID: performerID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
}
updatedPerformer := models.NewPerformerPartial()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
@@ -199,58 +194,86 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
}
}
if input.Name != nil {
// generate checksum from performer name rather than image
checksum := md5.FromString(*input.Name)
updatedPerformer.Name = &sql.NullString{String: *input.Name, Valid: true}
updatedPerformer.Checksum = &checksum
}
updatedPerformer.URL = translator.nullString(input.URL, "url")
updatedPerformer.Name = translator.optionalString(input.Name, "name")
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
updatedPerformer.URL = translator.optionalString(input.URL, "url")
if translator.hasField("gender") {
if input.Gender != nil {
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
updatedPerformer.Gender = models.NewOptionalString(input.Gender.String())
} else {
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
updatedPerformer.Gender = models.NewOptionalStringPtr(nil)
}
}
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
updatedPerformer.Country = translator.nullString(input.Country, "country")
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
updatedPerformer.Height = translator.nullString(input.Height, "height")
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
updatedPerformer.Details = translator.nullString(input.Details, "details")
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = input.IgnoreAutoTag
updatedPerformer.Birthdate = translator.optionalDate(input.Birthdate, "birthdate")
updatedPerformer.Country = translator.optionalString(input.Country, "country")
updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color")
updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements")
// prefer height_cm over height
if translator.hasField("height_cm") {
updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm")
} else if translator.hasField("height") {
updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height")
if err != nil {
return nil, err
}
}
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedPerformer.Details = translator.optionalString(input.Details, "details")
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
if translator.hasField("alias_list") {
updatedPerformer.Aliases = &models.UpdateStrings{
Values: input.AliasList,
Mode: models.RelationshipUpdateModeSet,
}
} else if translator.hasField("aliases") {
updatedPerformer.Aliases = &models.UpdateStrings{
Values: stringslice.FromString(*input.Aliases, ","),
Mode: models.RelationshipUpdateModeSet,
}
}
if translator.hasField("tag_ids") {
updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
}
// Save the stash_ids
if translator.hasField("stash_ids") {
updatedPerformer.StashIDs = &models.UpdateStashIDs{
StashIDs: stashIDPtrSliceToSlice(input.StashIds),
Mode: models.RelationshipUpdateModeSet,
}
}
// Start the transaction and save the p
var p *models.Performer
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
// need to get existing performer
existing, err := qb.Find(ctx, updatedPerformer.ID)
existing, err := qb.Find(ctx, performerID)
if err != nil {
return err
}
if existing == nil {
return fmt.Errorf("performer with id %d not found", updatedPerformer.ID)
return fmt.Errorf("performer with id %d not found", performerID)
}
if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil {
@@ -259,34 +282,19 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
}
}
p, err = qb.Update(ctx, updatedPerformer)
_, err = qb.UpdatePartial(ctx, performerID, updatedPerformer)
if err != nil {
return err
}
// Save the tags
if translator.hasField("tag_ids") {
if err := r.updatePerformerTags(ctx, p.ID, input.TagIds); err != nil {
return err
}
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(ctx, p.ID, imageData); err != nil {
if err := qb.UpdateImage(ctx, performerID, imageData); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyImage(ctx, p.ID); err != nil {
return err
}
}
// Save the stash_ids
if translator.hasField("stash_ids") {
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, performerID, stashIDJoins); err != nil {
if err := qb.DestroyImage(ctx, performerID); err != nil {
return err
}
}
@@ -296,16 +304,8 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, p.ID, plugin.PerformerUpdatePost, input, translator.getFields())
return r.getPerformer(ctx, p.ID)
}
func (r *mutationResolver) updatePerformerTags(ctx context.Context, performerID int, tagsIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(tagsIDs)
if err != nil {
return err
}
return r.repository.Performer.UpdateTags(ctx, performerID, ids)
r.hookExecutor.ExecutePostHooks(ctx, performerID, plugin.PerformerUpdatePost, input, translator.getFields())
return r.getPerformer(ctx, performerID)
}
func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPerformerUpdateInput) ([]*models.Performer, error) {
@@ -315,43 +315,67 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
}
// Populate performer from the input
updatedTime := time.Now()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedPerformer := models.PerformerPartial{
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
updatedPerformer := models.NewPerformerPartial()
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
updatedPerformer.URL = translator.optionalString(input.URL, "url")
updatedPerformer.Birthdate = translator.optionalDate(input.Birthdate, "birthdate")
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.optionalString(input.Country, "country")
updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color")
// prefer height_cm over height
if translator.hasField("height_cm") {
updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm")
} else if translator.hasField("height") {
updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height")
if err != nil {
return nil, err
}
}
updatedPerformer.URL = translator.nullString(input.URL, "url")
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.nullString(input.Country, "country")
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
updatedPerformer.Height = translator.nullString(input.Height, "height")
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
updatedPerformer.Details = translator.nullString(input.Details, "details")
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = input.IgnoreAutoTag
updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements")
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedPerformer.Details = translator.optionalString(input.Details, "details")
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
if translator.hasField("alias_list") {
updatedPerformer.Aliases = &models.UpdateStrings{
Values: input.AliasList.Values,
Mode: input.AliasList.Mode,
}
} else if translator.hasField("aliases") {
updatedPerformer.Aliases = &models.UpdateStrings{
Values: stringslice.FromString(*input.Aliases, ","),
Mode: models.RelationshipUpdateModeSet,
}
}
if translator.hasField("gender") {
if input.Gender != nil {
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
updatedPerformer.Gender = models.NewOptionalString(input.Gender.String())
} else {
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
updatedPerformer.Gender = models.NewOptionalStringPtr(nil)
}
}
if translator.hasField("tag_ids") {
updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
}
@@ -378,24 +402,12 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
return err
}
performer, err := qb.Update(ctx, updatedPerformer)
performer, err := qb.UpdatePartial(ctx, performerID, updatedPerformer)
if err != nil {
return err
}
ret = append(ret, performer)
// Save the tags
if translator.hasField("tag_ids") {
tagIDs, err := adjustTagIDs(ctx, qb, performerID, *input.TagIds)
if err != nil {
return err
}
if err := qb.UpdateTags(ctx, performerID, tagIDs); err != nil {
return err
}
}
}
return nil

View File

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

View File

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

View File

@@ -58,11 +58,18 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateI
newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true}
}
if input.Rating != nil {
newStudio.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
newStudio.Rating = sql.NullInt64{Valid: false}
if input.Rating100 != nil {
newStudio.Rating = sql.NullInt64{
Int64: int64(*input.Rating100),
Valid: true,
}
} else if input.Rating != nil {
newStudio.Rating = sql.NullInt64{
Int64: int64(models.Rating5To100(*input.Rating)),
Valid: true,
}
}
if input.Details != nil {
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
}
@@ -150,7 +157,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI
updatedStudio.URL = translator.nullString(input.URL, "url")
updatedStudio.Details = translator.nullString(input.Details, "details")
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
updatedStudio.Rating = translator.ratingConversion(input.Rating, input.Rating100)
updatedStudio.IgnoreAutoTag = input.IgnoreAutoTag
// Start the transaction and save the studio

View File

@@ -123,6 +123,10 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
ScraperCDPPath: &scraperCDPPath,
StashBoxes: config.GetStashBoxes(),
PythonPath: config.GetPythonPath(),
TranscodeInputArgs: config.GetTranscodeInputArgs(),
TranscodeOutputArgs: config.GetTranscodeOutputArgs(),
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
}
}
@@ -142,6 +146,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
showStudioAsText := config.GetShowStudioAsText()
css := config.GetCSS()
cssEnabled := config.GetCSSEnabled()
javascript := config.GetJavascript()
javascriptEnabled := config.GetJavascriptEnabled()
customLocales := config.GetCustomLocales()
customLocalesEnabled := config.GetCustomLocalesEnabled()
language := config.GetLanguage()
@@ -166,6 +172,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
ContinuePlaylistDefault: &continuePlaylistDefault,
CSS: &css,
CSSEnabled: &cssEnabled,
Javascript: &javascript,
JavascriptEnabled: &javascriptEnabled,
CustomLocales: &customLocales,
CustomLocalesEnabled: &customLocalesEnabled,
Language: &language,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -185,6 +185,8 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, st
width := f.Width
height := f.Height
config := config.GetInstance()
options := ffmpeg.TranscodeStreamOptions{
Input: f.Path,
Codec: streamFormat,
@@ -194,7 +196,9 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, st
VideoHeight: height,
StartTime: ss,
MaxTranscodeSize: config.GetInstance().GetMaxStreamingTranscodeSize().GetMaxResolution(),
MaxTranscodeSize: config.GetMaxStreamingTranscodeSize().GetMaxResolution(),
ExtraInputArgs: config.GetLiveTranscodeInputArgs(),
ExtraOutputArgs: config.GetLiveTranscodeOutputArgs(),
}
if requestedSize != "" {
@@ -264,7 +268,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce
}
var title string
if err := txn.WithTxn(ctx, rs.txnManager, func(ctx context.Context) error {
if err := txn.WithReadTxn(ctx, rs.txnManager, func(ctx context.Context) error {
qb := rs.tagFinder
primaryTag, err := qb.Find(ctx, marker.PrimaryTagID)
if err != nil {
@@ -293,7 +297,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce
func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
var sceneMarkers []*models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarkers, err = rs.sceneMarkerFinder.FindBySceneID(ctx, scene.ID)
return err
@@ -349,7 +353,7 @@ func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang strin
s := r.Context().Value(sceneKey).(*models.Scene)
var captions []*models.VideoCaption
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
primaryFile := s.Files.Primary()
if primaryFile == nil {
@@ -377,14 +381,14 @@ func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang strin
sub, err := video.ReadSubs(caption.Path(s.Path))
if err != nil {
logger.Warnf("error while reading subs: %v", err)
http.Error(w, readTxnErr.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var b bytes.Buffer
err = sub.WriteToWebVTT(&b)
if err != nil {
http.Error(w, readTxnErr.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -423,7 +427,7 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request)
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@@ -450,7 +454,7 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@@ -487,7 +491,7 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@@ -528,7 +532,7 @@ func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler {
sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam)
var scene *models.Scene
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
qb := rs.sceneFinder
if sceneID == 0 {
var scenes []*models.Scene

View File

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

View File

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

View File

@@ -1,14 +1,17 @@
package api
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path"
"regexp"
"runtime/debug"
"strconv"
"strings"
@@ -31,6 +34,7 @@ import (
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/ui"
)
@@ -166,21 +170,8 @@ func Start() error {
}.Routes())
r.Mount("/downloads", downloadsRoutes{}.Routes())
r.HandleFunc("/css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
if !c.GetCSSEnabled() {
return
}
// search for custom.css in current directory, then $HOME/.stash
fn := c.GetCSSPath()
exists, _ := fsutil.FileExists(fn)
if !exists {
return
}
http.ServeFile(w, r, fn)
})
r.HandleFunc("/css", cssHandler(c, pluginCache))
r.HandleFunc("/javascript", javascriptHandler(c, pluginCache))
r.HandleFunc("/customlocales", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if c.GetCustomLocalesEnabled() {
@@ -216,18 +207,9 @@ func Start() error {
// Serve static folders
customServedFolders := c.GetCustomServedFolders()
if customServedFolders != nil {
r.HandleFunc("/custom/*", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.Replace(r.URL.Path, "/custom", "", 1)
// map the path to the applicable filesystem location
var dir string
r.URL.Path, dir = customServedFolders.GetFilesystemLocation(r.URL.Path)
if dir != "" {
http.FileServer(http.Dir(dir)).ServeHTTP(w, r)
} else {
http.NotFound(w, r)
}
})
r.Mount("/custom", customRoutes{
servedFolders: customServedFolders,
}.Routes())
}
customUILocation := c.GetCustomUILocation()
@@ -323,23 +305,136 @@ func Start() error {
return nil
}
func copyFile(w io.Writer, path string) (time.Time, error) {
f, err := os.Open(path)
if err != nil {
return time.Time{}, err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return time.Time{}, err
}
_, err = io.Copy(w, f)
return info.ModTime(), err
}
func serveFiles(w http.ResponseWriter, r *http.Request, name string, paths []string) {
buffer := bytes.Buffer{}
latestModTime := time.Time{}
for _, path := range paths {
modTime, err := copyFile(&buffer, path)
if err != nil {
logger.Errorf("error serving file %s: %v", path, err)
} else {
if modTime.After(latestModTime) {
latestModTime = modTime
}
buffer.Write([]byte("\n"))
}
}
bufferReader := bytes.NewReader(buffer.Bytes())
http.ServeContent(w, r, name, latestModTime, bufferReader)
}
func cssHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// concatenate with plugin css files
w.Header().Set("Content-Type", "text/css")
// add plugin css files first
var paths []string
for _, p := range pluginCache.ListPlugins() {
paths = append(paths, p.UI.CSS...)
}
if c.GetCSSEnabled() {
// search for custom.css in current directory, then $HOME/.stash
fn := c.GetCSSPath()
exists, _ := fsutil.FileExists(fn)
if exists {
paths = append(paths, fn)
}
}
serveFiles(w, r, "custom.css", paths)
}
}
func javascriptHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript")
// add plugin javascript files first
var paths []string
for _, p := range pluginCache.ListPlugins() {
paths = append(paths, p.UI.Javascript...)
}
if c.GetJavascriptEnabled() {
// search for custom.js in current directory, then $HOME/.stash
fn := c.GetJavascriptPath()
exists, _ := fsutil.FileExists(fn)
if exists {
paths = append(paths, fn)
}
}
serveFiles(w, r, "custom.js", paths)
}
}
func printVersion() {
versionString := githash
var versionString string
switch {
case version != "":
if githash != "" && !IsDevelop() {
versionString = version + " (" + githash + ")"
} else {
versionString = version
}
case githash != "":
versionString = githash
default:
versionString = "unknown"
}
if config.IsOfficialBuild() {
versionString += " - Official Build"
} else {
versionString += " - Unofficial Build"
}
if version != "" {
versionString = version + " (" + versionString + ")"
if buildstamp != "" {
versionString += " - " + buildstamp
}
fmt.Printf("stash version: %s - %s\n", versionString, buildstamp)
logger.Infof("stash version: %s\n", versionString)
}
func GetVersion() (string, string, string) {
return version, githash, buildstamp
}
func IsDevelop() bool {
if githash == "" {
return false
}
// if the version is suffixed with -x-xxxx, then we are running a development build
develop := false
re := regexp.MustCompile(`-\d+-g\w+$`)
if re.MatchString(version) {
develop = true
}
return develop
}
func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
c.InitTLS()
certFile, keyFile := c.GetTLSFiles()
@@ -360,12 +455,12 @@ func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
cert, err := os.ReadFile(certFile)
if err != nil {
return nil, fmt.Errorf("error reading SSL certificate file %s: %s", certFile, err.Error())
return nil, fmt.Errorf("error reading SSL certificate file %s: %v", certFile, err)
}
key, err := os.ReadFile(keyFile)
if err != nil {
return nil, fmt.Errorf("error reading SSL key file %s: %s", keyFile, err.Error())
return nil, fmt.Errorf("error reading SSL key file %s: %v", keyFile, err)
}
certs := make([]tls.Certificate, 1)

View File

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

View File

@@ -21,15 +21,17 @@ func TestGalleryPerformers(t *testing.T) {
const performerName = "performer name"
const performerID = 2
performer := models.Performer{
ID: performerID,
Name: models.NullString(performerName),
ID: performerID,
Name: performerName,
Aliases: models.NewRelatedStrings([]string{}),
}
const reversedPerformerName = "name performer"
const reversedPerformerID = 3
reversedPerformer := models.Performer{
ID: reversedPerformerID,
Name: models.NullString(reversedPerformerName),
ID: reversedPerformerID,
Name: reversedPerformerName,
Aliases: models.NewRelatedStrings([]string{}),
}
testTables := generateTestTable(performerName, galleryExt)

View File

@@ -18,15 +18,17 @@ func TestImagePerformers(t *testing.T) {
const performerName = "performer name"
const performerID = 2
performer := models.Performer{
ID: performerID,
Name: models.NullString(performerName),
ID: performerID,
Name: performerName,
Aliases: models.NewRelatedStrings([]string{}),
}
const reversedPerformerName = "name performer"
const reversedPerformerID = 3
reversedPerformer := models.Performer{
ID: reversedPerformerID,
Name: models.NullString(reversedPerformerName),
ID: reversedPerformerID,
Name: reversedPerformerName,
Aliases: models.NewRelatedStrings([]string{}),
}
testTables := generateTestTable(performerName, imageExt)

View File

@@ -86,12 +86,10 @@ func TestMain(m *testing.M) {
func createPerformer(ctx context.Context, pqb models.PerformerWriter) error {
// create the performer
performer := models.Performer{
Checksum: testName,
Name: sql.NullString{Valid: true, String: testName},
Favorite: sql.NullBool{Valid: true, Bool: false},
Name: testName,
}
_, err := pqb.Create(ctx, performer)
err := pqb.Create(ctx, &performer)
if err != nil {
return err
}
@@ -480,6 +478,10 @@ func withTxn(f func(ctx context.Context) error) error {
return txn.WithTxn(context.TODO(), db, f)
}
func withDB(f func(ctx context.Context) error) error {
return txn.WithDatabase(context.TODO(), db, f)
}
func populateDB() error {
if err := withTxn(func(ctx context.Context) error {
err := createPerformer(ctx, r.Performer)
@@ -539,9 +541,16 @@ func TestParsePerformerScenes(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, p := range performers {
if err := withTxn(func(ctx context.Context) error {
return PerformerScenes(ctx, p, nil, r.Scene, nil)
if err := withDB(func(ctx context.Context) error {
if err := p.LoadAliases(ctx, r.Performer); err != nil {
return err
}
return tagger.PerformerScenes(ctx, p, nil, r.Scene)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@@ -586,14 +595,18 @@ func TestParseStudioScenes(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range studios {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Studio.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return StudioScenes(ctx, s, nil, aliases, r.Scene, nil)
return tagger.StudioScenes(ctx, s, nil, aliases, r.Scene)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@@ -642,14 +655,18 @@ func TestParseTagScenes(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range tags {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Tag.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return TagScenes(ctx, s, nil, aliases, r.Scene, nil)
return tagger.TagScenes(ctx, s, nil, aliases, r.Scene)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@@ -694,9 +711,16 @@ func TestParsePerformerImages(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, p := range performers {
if err := withTxn(func(ctx context.Context) error {
return PerformerImages(ctx, p, nil, r.Image, nil)
if err := withDB(func(ctx context.Context) error {
if err := p.LoadAliases(ctx, r.Performer); err != nil {
return err
}
return tagger.PerformerImages(ctx, p, nil, r.Image)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@@ -742,14 +766,18 @@ func TestParseStudioImages(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range studios {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Studio.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return StudioImages(ctx, s, nil, aliases, r.Image, nil)
return tagger.StudioImages(ctx, s, nil, aliases, r.Image)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@@ -798,14 +826,18 @@ func TestParseTagImages(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range tags {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Tag.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return TagImages(ctx, s, nil, aliases, r.Image, nil)
return tagger.TagImages(ctx, s, nil, aliases, r.Image)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@@ -851,9 +883,16 @@ func TestParsePerformerGalleries(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, p := range performers {
if err := withTxn(func(ctx context.Context) error {
return PerformerGalleries(ctx, p, nil, r.Gallery, nil)
if err := withDB(func(ctx context.Context) error {
if err := p.LoadAliases(ctx, r.Performer); err != nil {
return err
}
return tagger.PerformerGalleries(ctx, p, nil, r.Gallery)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@@ -899,14 +938,18 @@ func TestParseStudioGalleries(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range studios {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Studio.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return StudioGalleries(ctx, s, nil, aliases, r.Gallery, nil)
return tagger.StudioGalleries(ctx, s, nil, aliases, r.Gallery)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@@ -955,14 +998,18 @@ func TestParseTagGalleries(t *testing.T) {
return
}
tagger := Tagger{
TxnManager: db,
}
for _, s := range tags {
if err := withTxn(func(ctx context.Context) error {
if err := withDB(func(ctx context.Context) error {
aliases, err := r.Tag.GetAliases(ctx, s.ID)
if err != nil {
return err
}
return TagGalleries(ctx, s, nil, aliases, r.Gallery, nil)
return tagger.TagGalleries(ctx, s, nil, aliases, r.Gallery)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
"github.com/stashapp/stash/pkg/txn"
)
type SceneQueryPerformerUpdater interface {
@@ -29,77 +30,111 @@ type GalleryQueryPerformerUpdater interface {
gallery.PartialUpdater
}
func getPerformerTagger(p *models.Performer, cache *match.Cache) tagger {
return tagger{
func getPerformerTaggers(p *models.Performer, cache *match.Cache) []tagger {
ret := []tagger{{
ID: p.ID,
Type: "performer",
Name: p.Name.String,
Name: p.Name,
cache: cache,
}
}}
// TODO - disabled until we can have finer control over alias matching
// for _, a := range p.Aliases.List() {
// ret = append(ret, tagger{
// ID: p.ID,
// Type: "performer",
// Name: a,
// cache: cache,
// })
// }
return ret
}
// PerformerScenes searches for scenes whose path matches the provided performer name and tags the scene with the performer.
func PerformerScenes(ctx context.Context, p *models.Performer, paths []string, rw SceneQueryPerformerUpdater, cache *match.Cache) error {
t := getPerformerTagger(p, cache)
// Performer aliases must be loaded.
func (tagger *Tagger) PerformerScenes(ctx context.Context, p *models.Performer, paths []string, rw SceneQueryPerformerUpdater) error {
t := getPerformerTaggers(p, tagger.Cache)
return t.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
for _, tt := range t {
if err := tt.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
}
existing := o.PerformerIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
return scene.AddPerformer(ctx, rw, o, p.ID)
}); err != nil {
return false, err
}
return true, nil
}); err != nil {
return err
}
existing := o.PerformerIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := scene.AddPerformer(ctx, rw, o, p.ID); err != nil {
return false, err
}
return true, nil
})
}
return nil
}
// PerformerImages searches for images whose path matches the provided performer name and tags the image with the performer.
func PerformerImages(ctx context.Context, p *models.Performer, paths []string, rw ImageQueryPerformerUpdater, cache *match.Cache) error {
t := getPerformerTagger(p, cache)
func (tagger *Tagger) PerformerImages(ctx context.Context, p *models.Performer, paths []string, rw ImageQueryPerformerUpdater) error {
t := getPerformerTaggers(p, tagger.Cache)
return t.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) {
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
for _, tt := range t {
if err := tt.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) {
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
}
existing := o.PerformerIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
return image.AddPerformer(ctx, rw, o, p.ID)
}); err != nil {
return false, err
}
return true, nil
}); err != nil {
return err
}
existing := o.PerformerIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := image.AddPerformer(ctx, rw, o, p.ID); err != nil {
return false, err
}
return true, nil
})
}
return nil
}
// PerformerGalleries searches for galleries whose path matches the provided performer name and tags the gallery with the performer.
func PerformerGalleries(ctx context.Context, p *models.Performer, paths []string, rw GalleryQueryPerformerUpdater, cache *match.Cache) error {
t := getPerformerTagger(p, cache)
func (tagger *Tagger) PerformerGalleries(ctx context.Context, p *models.Performer, paths []string, rw GalleryQueryPerformerUpdater) error {
t := getPerformerTaggers(p, tagger.Cache)
return t.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
for _, tt := range t {
if err := tt.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
}
existing := o.PerformerIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error {
return gallery.AddPerformer(ctx, rw, o, p.ID)
}); err != nil {
return false, err
}
return true, nil
}); err != nil {
return err
}
existing := o.PerformerIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := gallery.AddPerformer(ctx, rw, o, p.ID); err != nil {
return false, err
}
return true, nil
})
}
return nil
}

View File

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

View File

@@ -151,15 +151,17 @@ func TestScenePerformers(t *testing.T) {
const performerName = "performer name"
const performerID = 2
performer := models.Performer{
ID: performerID,
Name: models.NullString(performerName),
ID: performerID,
Name: performerName,
Aliases: models.NewRelatedStrings([]string{}),
}
const reversedPerformerName = "name performer"
const reversedPerformerID = 3
reversedPerformer := models.Performer{
ID: reversedPerformerID,
Name: models.NullString(reversedPerformerName),
ID: reversedPerformerID,
Name: reversedPerformerName,
Aliases: models.NewRelatedStrings([]string{}),
}
testTables := generateTestTable(performerName, sceneExt)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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