Compare commits

...

286 Commits

Author SHA1 Message Date
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
WithoutPants
02c2ad3f58 Update changelog 2022-10-25 11:42:27 +11:00
WithoutPants
479ebfc88d Reimplement case-insensitivity move bug fix (#3047)
* Use eq for FindByPath for case sensitivity
* Handle case sensitive moves
2022-10-25 11:37:54 +11:00
WithoutPants
1c92336798 Fix symlink size calculation (#3046) 2022-10-25 10:57:37 +11:00
WithoutPants
5fae3cf127 Update changelog 2022-10-24 14:40:51 +11:00
WithoutPants
47395ce13f Use basename as title if empty when scraping by fragment (#3040)
* Fallback to file basename if title empty in scrape
* Populate dialog from basename if title empty
2022-10-24 14:36:22 +11:00
WithoutPants
091950615e Ignore non-existing scenes in fingerprint submits (#3039) 2022-10-24 10:26:21 +11:00
DingDongSoLong4
4db0e48f73 Fix zip gallery renaming (#3036) 2022-10-24 09:38:02 +11:00
WithoutPants
33de28ce5d Warn when failed to migrate from placeholder 2022-10-20 01:18:32 +00:00
WithoutPants
b8a8909a8e Add hotfix changelogs 2022-10-20 01:02:13 +00:00
WithoutPants
0cf06728d4 Ignore NULL values during migration 2022-10-20 01:01:18 +00:00
WithoutPants
3acece2438 Fix export zip paths when exporting from Windows (#3022)
* Use correct zip path for export in windows
* Fix recursive loop when importing tag hierarchy
2022-10-20 11:41:46 +11:00
WithoutPants
7104bb67ca Handle null video durations in migration (#3021) 2022-10-20 10:58:42 +11:00
WithoutPants
c4c6a3f9c0 Add Ukrainian language option 2022-10-19 22:40:55 +00:00
stash-translation-bot
86b52fe938 Translations update from Stash (#2931)
* Translated using Weblate (Italian)

Currently translated at 100.0% (826 of 826 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (826 of 826 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 100.0% (826 of 826 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 100.0% (826 of 826 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (823 of 826 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (829 of 829 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% (829 of 829 strings)

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

* Translated using Weblate (Czech)

Currently translated at 59.5% (494 of 829 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (829 of 829 strings)

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

* Added translation using Weblate (Ukrainian)

* Translated using Weblate (Ukrainian)

Currently translated at 11.0% (92 of 829 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (829 of 829 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (837 of 837 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% (837 of 837 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% (837 of 837 strings)

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

* Translated using Weblate (French)

Currently translated at 98.5% (825 of 837 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 98.5% (825 of 837 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% (845 of 845 strings)

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

* Translated using Weblate (Swedish)

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/sv/

* 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 (Polish)

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/pl/

* 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 (Italian)

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/it/

* Translated using Weblate (French)

Currently translated at 97.6% (825 of 845 strings)

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

* Translated using Weblate (Polish)

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/pl/

* Translated using Weblate (French)

Currently translated at 97.6% (825 of 845 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 93.3% (789 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 98.1% (829 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 98.2% (830 of 845 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 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 (German)

Currently translated at 98.6% (834 of 845 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 98.3% (831 of 845 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 97.7% (826 of 845 strings)

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

Co-authored-by: BViking78 <bviking78@gmail.com>
Co-authored-by: Alpaca Serious <srhsgsef@gmail.com>
Co-authored-by: 風林火山 <nezoko@digdig.org>
Co-authored-by: Coscosname <coscosname@gmail.com>
Co-authored-by: ponei <poneialt@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Foucoubou <foucoubou26@email.cz>
Co-authored-by: Still <dev@stillu.cc>
Co-authored-by: John <erse@tutanota.com>
Co-authored-by: MrOV3RDOSE <mrov3rdose@gmail.com>
Co-authored-by: Philip Wang <philpw99@gmail.com>
Co-authored-by: Luc <luc@lucspoelder.nl>
Co-authored-by: bogay <pojay11523@gmail.com>
Co-authored-by: Phasetime <phasetime@protonmail.com>
Co-authored-by: brestu <brestu@protonmail.com>
2022-10-19 17:02:37 -04:00
WithoutPants
a64e0929d4 Update notes 2022-10-18 23:12:22 +00:00
WithoutPants
4c286d7ab5 Fix dataloaders not loading in js plugins (#3014)
* Include dataloaders in js plugin gql calls
* Handle gql errors correctly in js plugins
2022-10-18 11:09:54 +11:00
WithoutPants
bd44571a91 Fix video looping instead of continuing playlist (#3007)
* Fix loop overriding continue queue
* Add id to queue continue checkbox
2022-10-14 11:21:26 +11:00
WithoutPants
396c1ffc6d Format 2022-10-12 06:10:15 +00:00
WithoutPants
6dcb1279a7 Fix + character not handled correctly in query URL 2022-10-12 16:36:02 +11:00
WithoutPants
5e1948516d Fix phash match presentation (#2997) 2022-10-12 11:06:32 +11:00
WithoutPants
99bbd157d6 Clarify backup database description 2022-10-11 14:28:31 +11:00
WithoutPants
6488a4236e Update changelog 2022-10-11 14:27:26 +11:00
WithoutPants
a6fd577f03 Fix video playback hanging at end (#2996)
Co-authored-by: gerit1a <10052885+gerit1a@users.noreply.github.com>
2022-10-11 14:24:09 +11:00
WithoutPants
6b5d5cc628 Create missing covers during scan (#2995)
* Create missing covers during scan
* Update changelog and release notes
2022-10-11 14:22:23 +11:00
WithoutPants
e3cd36f25f Use primary tag name as marker title where title is empty (#2994)
* Fix display of marker popovers
* Use primary tag as title where marker title empty
2022-10-11 14:21:56 +11:00
WithoutPants
68a1547e8b Include primary tag in tag marker count sorting (#2993) 2022-10-11 14:21:28 +11:00
WithoutPants
9bff498c28 Fix tag/studio alias and caption null filtering (#2990)
* Fix null filter for alias/captions
* Fix error when selecting is null for captions
2022-10-11 14:21:07 +11:00
WithoutPants
6ce409cd56 Prevent errors from stopping scan 2022-10-11 14:20:24 +11:00
JackDawson94
b1193227d0 Fixes gender-mapping with StashBox (#2992) 2022-10-11 09:40:14 +11:00
WithoutPants
90fdc6b322 Fix direct streaming 2022-10-10 18:54:55 +11:00
WithoutPants
043b67e076 Fix setup/migrate redirects on subpath proxies (#2982) 2022-10-10 10:12:07 +11:00
WithoutPants
6c04f9199f Add apikey to streams (#2981) 2022-10-10 10:11:51 +11:00
CJ
351dcb708b Fix subtitles not loading (#2987) 2022-10-10 10:09:28 +11:00
7dJx1qP
bb250d1232 fix padding for non-checkbox cells (#2980) 2022-10-07 12:01:37 +11:00
WithoutPants
0e0d201ff3 Ignore other conflicts in 35 migration 2022-10-06 14:54:14 +11:00
WithoutPants
e96a09d9fd Update changelog 2022-10-06 14:51:32 +11:00
WithoutPants
ef9e138a2d [Files Refactor] Object file management (#2790)
* Add Make Primary file function
* Add delete file functionality
2022-10-06 14:50:06 +11:00
7dJx1qP
83359b00d5 Make scenes page list view checkbox fill entire cell (#2974) 2022-10-06 13:08:43 +11:00
Joe Scylla
9083796a42 #1810 Truncate large numbers on buttons (#2781)
* #1810 Truncate large numbers on buttons
* Apply to card popovers as well

Co-authored-by: Roland Karle <roland.karle@aufwind-group.de>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-10-06 12:43:17 +11:00
WithoutPants
b160c3bb97 Associate funscript files on scan (#2978) 2022-10-06 11:17:01 +11:00
WithoutPants
55001ddcf1 Ignore conflicts on performers_galleries 2022-10-06 10:05:43 +11:00
pickleahead
4c73f2f845 Add descriptions to tags and display tag cards on hover (#2708)
* add descriptions to tags
* display tag description and tag image on hover

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-10-06 10:01:06 +11:00
WithoutPants
480ae46dde Update changelog 2022-10-03 13:02:28 +11:00
WithoutPants
6ba9f55df0 Add default thumbnails for scenes and images (#2949)
* Use default thumbnail for scene covers
* Use defautl thumbnail for image thumbnails
2022-10-03 13:01:35 +11:00
WithoutPants
88bfda1980 Ignore duplicates for movies_scenes migration 2022-10-03 08:20:04 +11:00
WithoutPants
060ac00fc7 Take out _stash_ids from 35 migration 2022-10-01 08:45:16 +10:00
WithoutPants
7b83d81820 Fix backup issues (#2966)
Make backup directory setting optional. Use the basename of the database file for the naming.
2022-09-30 20:57:28 +10:00
WithoutPants
9e44e13f6d Fix update duplicate ids (#2965) 2022-09-30 18:44:37 +10:00
WithoutPants
51f4dd8a59 Delete identical studio stash id rows 2022-09-30 18:10:55 +10:00
WithoutPants
30f7a05ebf Delete identical rows from performer_stash_ids 2022-09-30 17:25:40 +10:00
WithoutPants
8f594e7fed Fix migration 2022-09-30 16:23:50 +10:00
WithoutPants
c8cbb36fd5 Ignore multiple identical rows in performers_scenes 2022-09-30 15:59:29 +10:00
JackDawson94
554448594c Add unix timestamp parsing to scrapers parseDate (#2817)
* Add unix timestamp parsing to scrapers parseDate
* Add documentation
* Update ScraperDevelopment.md
* Add unit test

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-09-30 15:35:56 +10:00
WithoutPants
6c6e0b6236 Fix saved front page filters being corrupted 2022-09-30 14:47:35 +10:00
WithoutPants
b8b62a36c6 Update changelog 2022-09-30 11:30:23 +10:00
HijackHornet
b588597f3e [Feature] Config option for sub content display (#2832)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-09-30 11:28:47 +10:00
JackDawson94
c63c06de1c Stashbox tagger reorder (#2840)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-09-30 11:26:37 +10:00
DingDongSoLong4
25bc750295 Performance improvements (#2925)
* Add sqlite_stat4 build tag
* Simplify studio filter criterion queries
* Prevent useList loading data before filter initialized
2022-09-30 10:49:51 +10:00
7dJx1qP
d274f86390 Add backup directory path setting (#2953)
* add backup directory path setting
* Don't default backup path
* handle migration backup path input when given filename or path

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-09-30 10:00:50 +10:00
WithoutPants
ad7fbce5f7 Rebuild association tables, ensure file-system-based galleries cannot be changed (#2955)
* Re-create tables to include primary keys
* Filesystem-based galleries cannot change images
2022-09-30 09:18:58 +10:00
WithoutPants
dce90a3ed9 Handle file rescan (#2951)
* Fire handlers when file updated or moved
* Create galleries as needed
* Clean empty galleries
* Handle cleaning zip folders when path changed
* Fix gallery association on duplicate images
* Re-create missing folder-based galleries
2022-09-28 16:08:00 +10:00
DingDongSoLong4
00820a8789 Suppress new context closed errors (#2947) 2022-09-26 11:41:28 +10:00
WithoutPants
d4e706daef Only set video algorithm after migration 12 (#2946) 2022-09-26 11:27:53 +10:00
WithoutPants
0848b02e93 Various bug fixes (#2945)
* Only update fingerprints if changed
* Fix panic when loading primary file fails
* Fix gallery/scene association
* Fix display of scene gallery in card
* Use natural_cs collation with paths for title sorting
2022-09-25 12:07:55 +10:00
DingDongSoLong4
4089a5fccc Fix JSON.parse console error (#2943) 2022-09-25 10:06:32 +10:00
HijackHornet
74191c73ed Custom localization (#2837)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-09-22 19:49:35 +10:00
dependabot[bot]
c10d53ba8e Bump moment from 2.29.2 to 2.29.4 in /ui/v2.5 (#2739)
Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  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-09-22 16:33:29 +10:00
halorrr
10655586b0 add ruby and faraday gem to stash for use in scrapers (#2707) 2022-09-22 16:14:40 +10:00
WithoutPants
b74428cb42 Various bug fixes (#2938)
* Don't recalculate MD5 if not enabled

Remove MD5 if oshash has changed and MD5 was not calculated.

* Fix panic in paged DLNA
* Prevent identical hashes in stash-box drafts
2022-09-21 15:39:41 +10:00
WithoutPants
cffcd9f4b8 Various bug fixes (#2935)
* Sort scene/image/gallery tags by name
* Calculate md5 if missing
* Prevent multiple folder create logs
2022-09-20 17:02:14 +10:00
WithoutPants
3fa7b470e7 More timestamp corrections (#2933)
* Fix incorrect timestamp updates
* Correct folder time fields
* Add migration with new indexes
* Correct mod_time format
* Add mod_time to data massage
2022-09-20 13:52:37 +10:00
WithoutPants
98e3610ade Fix gallery checksum resolver error (#2929) 2022-09-19 16:50:04 +10:00
DingDongSoLong4
8efbcc1c4d Suppress benign broken pipe and context closed warnings (#2927) 2022-09-19 15:01:40 +10:00
DingDongSoLong4
5e97ecd260 Set explicit SameSite=Lax on session cookie (#2926) 2022-09-19 14:56:05 +10:00
WithoutPants
1207629a76 Fix incorrectly formatted timestamps (#2918)
* Update updated_at when adding file to object
* Use models.SQLTimestamp for timestamps
* Add data massage to fix incorrect timestamps
2022-09-19 14:53:46 +10:00
WithoutPants
2564351265 Use post commit hook for post-create plugin hooks (#2920) 2022-09-19 14:53:06 +10:00
WithoutPants
0359ce2ed8 Fix bulk add movie to scenes and autotag transaction error (#2928)
* Fix bulk add movie to scene
* Fix already in transaction error for autotag
2022-09-19 14:52:40 +10:00
WithoutPants
648247aa00 Split by whitespace for path includes/excludes (#2919) 2022-09-16 14:45:08 +10:00
WithoutPants
90726086e5 Fix relationship not loaded panic (#2915) 2022-09-16 11:27:38 +10:00
DingDongSoLong4
7a75313a1c Fix URL encoding (#2899)
* Fix URL encoding
* Optimize nullable criterion encoding
2022-09-16 11:17:19 +10:00
TgSeed
593477cbe1 Performer/Studio/Tag/Gallery Create compnent has default name as search query (#2701)
* feat: Closes #2618
fix: New button is now available even if pathname ends with '/'
2022-09-16 11:05:33 +10:00
WithoutPants
781a767fb6 Fix https detection with reverse proxy (#2910) 2022-09-15 09:54:36 +10:00
WithoutPants
d558902dfb Update changelog 2022-09-14 14:29:57 +10:00
WithoutPants
7f5f1c7e0d Fix continue queue checkbox not persisting (#2895) 2022-09-14 14:22:44 +10:00
WithoutPants
e6b7d40784 Apply autostartVideoOnPlaySelected to queue (#2896) 2022-09-14 14:22:11 +10:00
WithoutPants
5db42f4882 Fix incorrect gallery when clicking queue item (#2897) 2022-09-14 14:21:30 +10:00
WithoutPants
df9c7594c7 Disable http2 and fix https detection (#2900) 2022-09-14 14:20:53 +10:00
WithoutPants
2368269e63 Handle error in newImageBox WalkDir (#2894) 2022-09-14 14:20:09 +10:00
WithoutPants
32911367b0 Remove files-refactor build automation 2022-09-14 14:00:50 +10:00
WithoutPants
c43c695f5d Merge pull request #2907 from stashapp/files-refactor 2022-09-14 13:36:46 +10:00
WithoutPants
8b79eaca67 [Files Refactor] Use batching for pre/post-migration (#2906)
* Use batching for pre/post-migration
* Clarify release notes
2022-09-14 10:57:00 +10:00
DingDongSoLong4
5c383da5ec Optimize database after migration (#2904) 2022-09-14 09:15:36 +10:00
WithoutPants
cfc8222b9a [Files Refactor] Cleanup (#2893)
* Clean up notes for develop merge
* Remove commented code
* Lint
2022-09-07 16:50:15 +10:00
WithoutPants
9e08edc76f [Files Refactor] Don't require fingerprint calculation post-migrate (#2892) 2022-09-07 14:21:10 +10:00
WithoutPants
cc9fc2150e Fix scan settings not persisting (#2888) 2022-09-06 07:04:52 +00:00
WithoutPants
13bdba5b24 Fix json filename generation (#2887) 2022-09-06 07:04:52 +00:00
WithoutPants
0c513a604d Fix stack overflow creating windows network folders (#2886) 2022-09-06 07:04:52 +00:00
WithoutPants
276f14cdcb Fix filename generation in export (#2883) 2022-09-06 07:04:52 +00:00
WithoutPants
6b0bcdea88 [Files Refactor] Set primary flag when cleaning (#2880)
* Ensure single primary per object
* Set primary file during clean
* Only show migration notes for actual migrations
2022-09-06 07:04:52 +00:00
WithoutPants
7159ab69a3 Use DisplayName in autotag errors (#2873) 2022-09-06 07:04:52 +00:00
WithoutPants
94d39da706 [Files Refactor] Bug fixes (#2868)
* Return error if multiple rows returned for id
* Add missing LoadFiles calls
* Show id if path is empty
2022-09-06 07:04:52 +00:00
WithoutPants
273cf0383d [Files Refactor] Performance tuning (#2865)
* Don't load image files by default
* Don't load gallery files by default
* Don't load scene files by default
* Retry locked transactions forever
* Don't show release notes if config not loaded
* Don't translate path slashes in export
2022-09-06 07:04:52 +00:00
WithoutPants
0b534d89c6 [Files Refactor] Import export fixup (#2763)
* Adjust json schema
* Remove mappings file from export
* Import file/folder support
* Update documentation
* Make gallery filenames unique
2022-09-06 07:04:52 +00:00
WithoutPants
1222b7b87b Fix files not being cleaned correctly (#2862) 2022-09-06 07:04:52 +00:00
WithoutPants
7b439556c0 [Files Refactor] Rollback platform-agnostic paths (#2852)
* Rollback platform agnostic path storage
* Add release note for database change
2022-09-06 07:04:52 +00:00
WithoutPants
0c7b5cf6a1 [Files Refactor] Fix lint github action error (#2853)
* Use alternative config file to disable linters that cause out of mem error
2022-09-06 07:04:52 +00:00
WithoutPants
f4825fadf4 [Files refactor] Bug fixes (#2849)
* Fix scene sorting
* Fix folder-based gallery path sorting
* Fix gallery path filter
* Fix stash-box performer submission
* Fix identify logging
* Remove govet from linter
2022-09-06 07:03:43 +00:00
WithoutPants
00608c167a [Files Refactor] Performance tuning (#2819)
* Load scene relationships on demand
* Load image relationships on demand
* Load gallery relationships on demand
* Add dataloaden
* Use dataloaders
* Use where in for other find many functions
2022-09-06 07:03:42 +00:00
WithoutPants
9b31b20fed [Files Refactor] Performance tuning (#2813)
* Do database txn in same thread. Retry on locked db
* Remove captions from slimscenedata
* Fix tracing
* Use where in instead of individual selects
* Remove scenes_query view
* Remove image query view
* Remove gallery query view
* Use where in for FindMany
* Don't interrupt scanning zip files
* Fix image filesize sort
2022-09-06 07:03:42 +00:00
WithoutPants
87167736f6 [Files Refactor] bug fixes (#2811)
* Fix scan options not saving
* Fix duration stat calculation
2022-09-06 07:03:42 +00:00
WithoutPants
569c3a872a [Files Refactor] Performance tuning (#2809)
* Use cache during migration
* Avoid use of query views
* Use FindMany to find related objects
* Log slow queries
* Add folders to generated files
* Use SlimScene for scene queries
* Include filename in migration error message
2022-09-06 07:03:42 +00:00
WithoutPants
c825cf5d09 Correctly delete files when specified (#2804) 2022-09-06 07:03:42 +00:00
WithoutPants
5843fdcecc [Files Refactor] Migration fix (#2796)
* Fix large wal file during migration
* Fix migration dropping / from network share paths
2022-09-06 07:03:42 +00:00
WithoutPants
bc47932343 [Files Refactor] Performance tuning (#2784)
* Improve image query performance
* Tune queries
* Fix db generator
* Don't show release notes in setup
* Further tune indexes
* Log when creating screenshot
2022-09-06 07:03:42 +00:00
WithoutPants
abb574205a Files refactor fixes (#2743)
* Fix destroy gallery not destroying file
* Re-add minModTime functionality
* Deprecate useFileMetadata and stripFileExtension
* Optimise files post migration
* Decorate moved files. Use first missing file in move
* Include path in thumbnail generation error log
* Fix stash-box draft submission
* Don't destroy files unless deleting
* Call handler for files with no associated objects
* Fix moved zips causing error on scan
2022-09-06 07:03:42 +00:00
WithoutPants
461068462c [Files Refactor] Filter and sort by file count (#2744)
* Add filtering on file count
* Add sorting by file count
2022-09-06 07:03:42 +00:00
WithoutPants
5495d72849 File storage rewrite (#2676)
* Restructure data layer part 2 (#2599)
* Refactor and separate image model
* Refactor image query builder
* Handle relationships in image query builder
* Remove relationship management methods
* Refactor gallery model/query builder
* Add scenes to gallery model
* Convert scene model
* Refactor scene models
* Remove unused methods
* Add unit tests for gallery
* Add image tests
* Add scene tests
* Convert unnecessary scene value pointers to values
* Convert unnecessary pointer values to values
* Refactor scene partial
* Add scene partial tests
* Refactor ImagePartial
* Add image partial tests
* Refactor gallery partial update
* Add partial gallery update tests
* Use zero/null package for null values
* Add files and scan system
* Add sqlite implementation for files/folders
* Add unit tests for files/folders
* Image refactors
* Update image data layer
* Refactor gallery model and creation
* Refactor scene model
* Refactor scenes
* Don't set title from filename
* Allow galleries to freely add/remove images
* Add multiple scene file support to graphql and UI
* Add multiple file support for images in graphql/UI
* Add multiple file for galleries in graphql/UI
* Remove use of some deprecated fields
* Remove scene path usage
* Remove gallery path usage
* Remove path from image
* Move funscript to video file
* Refactor caption detection
* Migrate existing data
* Add post commit/rollback hook system
* Lint. Comment out import/export tests
* Add WithDatabase read only wrapper
* Prepend tasks to list
* Add 32 pre-migration
* Add warnings in release and migration notes
2022-09-06 07:03:42 +00:00
WithoutPants
30877c75fb Release notes dialog (#2726)
* Move manual docs
* Move changelog docs
* Add migration notes
* Move changelog to settings
* Add release notes dialog
* Add new changelog
2022-09-06 07:03:40 +00:00
WithoutPants
964b559309 Restructure data layer (#2532)
* Add new txn manager interface
* Add txn management to sqlite
* Rename get to getByID
* Add contexts to repository methods
* Update query builders
* Add context to reader writer interfaces
* Use repository in resolver
* Tighten interfaces
* Tighten interfaces in dlna
* Tighten interfaces in match package
* Tighten interfaces in scraper package
* Tighten interfaces in scan code
* Tighten interfaces on autotag package
* Remove ReaderWriter usage
* Merge database package into sqlite
2022-09-06 07:03:40 +00:00
WithoutPants
7b5bd80515 Separate graphql API from rest of the system (#2503)
* Move graphql generated files to api
* Refactor identify options
* Remove models.StashBoxes
* Move ScraperSource to scraper package
* Rename field strategy enums
* Rename identify.TaskOptions to Options
2022-09-06 07:03:40 +00:00
halorrr
9dcf03eb70 show stashids on studio details (#2810) 2022-09-05 22:41:01 -07:00
v-helmholtz
628afce516 Fix unclosed div tag in login.html (#2846) 2022-09-05 22:34:57 -07:00
dependabot[bot]
f5f4cbef1e Bump vite from 2.7.1 to 2.9.13 in /ui/v2.5 (#2863)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 2.7.1 to 2.9.13.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v2.9.13/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:production
...

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-09-05 22:33:45 -07:00
stash-translation-bot
c387550c63 Translations update from Stash (#2838)
* Translated using Weblate (Swedish)

Currently translated at 100.0% (819 of 819 strings)

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

* Translated using Weblate (Thai)

Currently translated at 31.1% (255 of 819 strings)

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

* Translated using Weblate (Thai)

Currently translated at 32.6% (267 of 819 strings)

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

* Translated using Weblate (Thai)

Currently translated at 37.3% (306 of 819 strings)

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

* Translated using Weblate (Russian)

Currently translated at 55.0% (451 of 819 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 99.5% (815 of 819 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.5% (815 of 819 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 56.1% (460 of 819 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 99.5% (815 of 819 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 62.3% (511 of 819 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 99.5% (815 of 819 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 66.4% (544 of 819 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 77.7% (637 of 819 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 96.4% (790 of 819 strings)

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

* Translated using Weblate (Russian)

Currently translated at 78.1% (640 of 819 strings)

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

Co-authored-by: Alpaca Serious <srhsgsef@gmail.com>
Co-authored-by: Pakin Kornthamonphokin <themorajr@gmail.com>
Co-authored-by: ApxuBbI <nick1232@inbox.lv>
Co-authored-by: MrOV3RDOSE <mrov3rdose@gmail.com>
Co-authored-by: onlyxxxhentai <onlyxxxhentai@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
2022-09-05 22:32:04 -07:00
kermieisinthehouse
30879389ec Fix golangci OOM (#2889)
* Fix golangci OOM

* Fix all lints
2022-09-05 22:12:59 -07:00
HijackHornet
90baf7a469 Fixed cross-compile doc command (#2825)
Current doc pointed at the old image of the cross compiler docker image.
Thanks bnkai for telling me :)
2022-08-28 23:02:36 -07:00
stash-translation-bot
10bc2c6689 Translations update from Stash (#2725)
* Translated using Weblate (French)

Currently translated at 94.1% (771 of 819 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 94.1% (771 of 819 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 94.2% (772 of 819 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 15.0% (123 of 819 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 96.7% (792 of 819 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 23.9% (196 of 819 strings)

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

* Translated using Weblate (Czech)

Currently translated at 37.9% (311 of 819 strings)

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

* Translated using Weblate (Czech)

Currently translated at 51.1% (419 of 819 strings)

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

* Translated using Weblate (Czech)

Currently translated at 51.2% (420 of 819 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 96.7% (792 of 819 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 96.7% (792 of 819 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.5% (799 of 819 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 93.5% (766 of 819 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (819 of 819 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% (819 of 819 strings)

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

* Translated using Weblate (Czech)

Currently translated at 57.5% (471 of 819 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.4% (798 of 819 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 98.5% (807 of 819 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 100.0% (819 of 819 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% (819 of 819 strings)

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

* Translated using Weblate (Danish)

Currently translated at 98.9% (810 of 819 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 98.5% (807 of 819 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% (813 of 819 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% (819 of 819 strings)

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

* Translated using Weblate (Danish)

Currently translated at 100.0% (819 of 819 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 89.9% (737 of 819 strings)

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

Co-authored-by: - <adr.web@hotmail.fr>
Co-authored-by: Foucoubou <foucoubou26@email.cz>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Aa <jarruraita@outlook.com>
Co-authored-by: BViking78 <bviking78@gmail.com>
Co-authored-by: Coscosname <coscosname@gmail.com>
Co-authored-by: Steff <sbeuze@gmail.com>
Co-authored-by: Phasetime <phasetime@protonmail.com>
Co-authored-by: 風林火山 <nezoko@digdig.org>
Co-authored-by: Porn Mancer <pornmancer1337@gmail.com>
Co-authored-by: MrOV3RDOSE <mrov3rdose@gmail.com>
Co-authored-by: Philip Wang <philpw99@gmail.com>
Co-authored-by: Hidden Hiddenson <078emil@protonmail.com>
Co-authored-by: cumladhier <jasper.belle@outlook.com>
2022-08-17 09:15:02 -07:00
WithoutPants
cba0fddf61 Fix release publishing 2022-07-26 14:42:18 +10:00
7dJx1qP
8e222ae387 Fix locale import missing translation properties (#2772)
* Fix locale import missing translation properties
2022-07-25 10:46:43 +10:00
WithoutPants
7f86509674 Update changelog 2022-07-22 17:34:39 +10:00
WithoutPants
4b7ef76321 Focus scene player after loading new scene (#2758)
* Focus scene player after loading new scene
2022-07-22 17:32:03 +10:00
WithoutPants
c21c334553 Fix % character in tag name causing UI crash (#2757)
* Fix % character in tag name causing UI crash
2022-07-22 17:29:16 +10:00
WithoutPants
db3138b33f Fix keyboard shortcuts not working after selecting (#2756)
* Fix keyboard shortcuts not working after selecting
2022-07-22 17:25:48 +10:00
WithoutPants
e532f9a683 Allow unauthenticated access to UI assets (#2755)
* Allow unauthenticated access to UI assets
2022-07-22 17:23:40 +10:00
peolic
b8262f5641 Fix non-default video stream from ffprobe result (#2752)
* Fix non-default video stream from ffprobe result
2022-07-22 17:21:39 +10:00
WithoutPants
6f7cc11b86 Fix incorrect image displayed in lightbox (#2754)
* Fix incorrect image displayed in lightbox
* Add changelog
2022-07-22 17:18:11 +10:00
WithoutPants
9ac5d3f3d9 Fix messages when adding/removing gallery image (#2748) 2022-07-18 11:07:17 +10:00
WithoutPants
11d5c7f386 Fix UI crash when scene has stash ids but no stash-box configured (#2749)
* Add error boundary to show navbar on error
* Fix crash when scene has stash-ids but no stash-box configured
2022-07-18 10:46:39 +10:00
WithoutPants
2427519100 Enable automated build for files-refactor (#2738) 2022-07-13 15:46:44 +10:00
DingDongSoLong4
91e3fcc7ff Fix lodash-es typos (#2724)
* Fix LocalForage not saving items
* Fix bulk update typos
2022-07-13 10:08:02 +10:00
kermieisinthehouse
4fa103dd08 Fix alphabetical css (#2733) 2022-07-09 21:51:39 -07:00
erri120
3a3a296995 Add nowrap to rating stars and overflow-wrap to title for mobile (#2731) 2022-07-09 21:23:17 -07:00
stash-translation-bot
351472f654 Translations update from Stash (#2690)
* Translated using Weblate (French)

Currently translated at 93.7% (768 of 819 strings)

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

* Added translation using Weblate (Thai)

* Translated using Weblate (Thai)

Currently translated at 5.2% (43 of 819 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (819 of 819 strings)

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

* Translated using Weblate (Korean)

Currently translated at 99.3% (814 of 819 strings)

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

* Translated using Weblate (Korean)

Currently translated at 99.8% (818 of 819 strings)

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

* Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (819 of 819 strings)

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

* Translated using Weblate (Romanian)

Currently translated at 45.6% (374 of 819 strings)

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

Co-authored-by: - <adr.web@hotmail.fr>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Mr.Kay <taksayir@gmail.com>
Co-authored-by: Philip Wang <philpw99@gmail.com>
Co-authored-by: Barack Obama <starebod@gmail.com>
Co-authored-by: yc <yechan24680@gmail.com>
Co-authored-by: Bogdan <bogddan1337@pm.me>
2022-07-04 17:22:53 -07:00
bnkai
92acd78401 Fix name,path filter naming (#2712)
* Fix name,path filter naming

* * fmt-ui
2022-07-04 17:07:19 -07:00
Russell Harmon
813495c7f7 Set crossorigin use-credentials for manifest.json. (#2706)
This is necessary when running behind a password-protected reverse
proxy, as otherwise Chrome doesn't send HTTP basic auth credentials when
requesting this file.

See
https://stackoverflow.com/questions/6294622/html5-manifest-cache-behind-basic-auth
for more information.
2022-06-29 21:49:04 -07:00
WithoutPants
d32e375521 Treat edited values as scraped (#2702)
* Treat edited values as scraped
* Fix scrape selection on change
2022-06-30 09:25:13 +10:00
DingDongSoLong4
b3bc5b999f Fix saved filter search (#2698)
* Fix saved filter search excluding filters with uppercase characters
2022-06-27 11:38:13 +10:00
WithoutPants
f3e6cb7b0e Fix proxy prefix replacement (#2694) 2022-06-27 09:53:40 +10:00
WithoutPants
5e7bf1c2d7 Update changelog 2022-06-23 11:18:20 +10:00
DingDongSoLong4
63e1bbf35d UI and filter fixes (#2686)
* Use primitive string in recommendation row props
* Use unique keys in recommendation rows

The keys for the cards used while loading clash with the ids of the actual cards, causing a list unique key warning.

* List filter alignment tweaks
* Rework list hook filtering
* Internationalise checksum correctly
2022-06-22 15:45:47 +10:00
WithoutPants
3b4b20e9b2 React code splitting (#2603)
* Code split using react lazy
* Split locales
* Move to lodash-es
* Import individual icons
2022-06-22 14:41:31 +10:00
stash-translation-bot
33b68b4464 Translations update from Stash (#2653)
* Translated using Weblate (Swedish)

Currently translated at 100.0% (818 of 818 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (817 of 818 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (817 of 818 strings)

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

* Translated using Weblate (French)

Currently translated at 93.7% (767 of 818 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 100.0% (818 of 818 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (819 of 819 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% (819 of 819 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.7% (817 of 819 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Translated using Weblate (Spanish)

Currently translated at 98.1% (804 of 819 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (819 of 819 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 93.5% (766 of 819 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 93.7% (768 of 819 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (819 of 819 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (819 of 819 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (819 of 819 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Co-authored-by: Alpaca Serious <srhsgsef@gmail.com>
Co-authored-by: GeneX <gxetrek@gmail.com>
Co-authored-by: ponei <poneialt@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: - <adr.web@hotmail.fr>
Co-authored-by: Phasetime <phasetime@protonmail.com>
Co-authored-by: BViking78 <bviking78@gmail.com>
Co-authored-by: Coscosname <coscosname@gmail.com>
Co-authored-by: Still <dev@stillu.cc>
Co-authored-by: Alex <majocorsi@gmail.com>
Co-authored-by: 風林火山 <nezoko@digdig.org>
Co-authored-by: rather not <big_brother_is_watching4u@protonmail.com>
2022-06-21 19:02:34 -07:00
iampabber
1ab02a1748 Use hotkeys '[' and ']' to scrub video player forwards and backwards by 10% of the scene (#2678)
* Use hotkeys '[' and ']' to scrub video player forwards and backwards by 10% of the scene
* Don't loop back to beginning
* Add manual keybind entry

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-06-22 11:33:17 +10:00
bnkai
6cfb7fe79d Fix Synopsis json string in movie jsonschema (#2664)
* Fix Synopsis json string in movie jsonschema
* backwards compatible movie synopsis import
2022-06-22 10:59:39 +10:00
TgSeed
abd76f7e58 Fix/ffprobe unmarshalling error (#2685)
Fix/ffprobe unmarshalling error
2022-06-22 10:49:14 +10:00
WithoutPants
733ca2aa6f Run build on all pull requests 2022-06-19 11:54:00 +10:00
WithoutPants
c2f7617952 Separate filter buttons from query field (#2668) 2022-06-15 11:51:05 +10:00
WithoutPants
75a795b2e6 Add tag recommendation row (#2673) 2022-06-15 11:23:39 +10:00
WithoutPants
6029918d22 Fix ui config conversion (#2672) 2022-06-15 10:31:09 +10:00
dumdum7
582ffa1420 Don't switch to landscape for portrait videos (#2665) 2022-06-14 11:19:12 +10:00
WithoutPants
900ba936a9 Update changelog 2022-06-14 10:41:35 +10:00
WithoutPants
a2e8f69028 Fix scene player event handler initialisation (#2656) 2022-06-14 10:39:46 +10:00
CJ
9264c15540 Customize recommendations (#2592)
* refactored common code in recommendation row
* Implement front page options in config
* Allow customisation from front page
* Rename recommendations to front page
* Add generic UI settings
* Support adding premade filters

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-06-14 10:34:04 +10:00
WithoutPants
ff724d82cc Don't trim extension for folders when auto-tagging galleries (#2658) 2022-06-08 09:02:11 +10:00
WithoutPants
456e9409e0 Update scene screenshot in edit panel (#2657) 2022-06-08 08:58:59 +10:00
WithoutPants
5e455d6530 Add alias for Laos (#2655) 2022-06-08 08:58:42 +10:00
WithoutPants
3d52bad9bd Add missing gallery card classes (#2654) 2022-06-08 08:58:25 +10:00
stash-translation-bot
803d865348 Translations update from Stash (#2627)
* Translated using Weblate (French)

Currently translated at 88.8% (725 of 816 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 97.6% (797 of 816 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 89.4% (730 of 816 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 89.4% (730 of 816 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% (816 of 816 strings)

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

* Added translation using Weblate (Hungarian)

* Translated using Weblate (German)

Currently translated at 98.1% (803 of 818 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.7% (808 of 818 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (818 of 818 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 48.5% (397 of 818 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (818 of 818 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (816 of 818 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (818 of 818 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 93.7% (767 of 818 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 91.5% (749 of 818 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 91.5% (749 of 818 strings)

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

Co-authored-by: - <adr.web@hotmail.fr>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Phasetime <phasetime@protonmail.com>
Co-authored-by: Still <dev@stillu.cc>
Co-authored-by: Foltin <foltincsaba@gmail.com>
Co-authored-by: ponei <poneialt@gmail.com>
Co-authored-by: Coscosname <coscosname@gmail.com>
Co-authored-by: BViking78 <bviking78@gmail.com>
Co-authored-by: Aa <jarruraita@outlook.com>
2022-06-05 17:06:17 -07:00
Emilo2
8a1c349976 Fix scraping more than 40 scenes from stash-box (#2638) 2022-06-03 09:37:24 +10:00
InfiniteTF
d68d022893 Add support for submitting stash-box scene updates by draft (#2577) 2022-06-01 14:53:31 +10:00
InfiniteTF
e51083c26d Update stash-box fingerprint query to fully support distance matching (#2509) 2022-06-01 12:59:06 +10:00
DingDongSoLong4
49f579e08e Fix gallery updating (#2611) 2022-06-01 11:58:44 +10:00
HijackHornet
1c18ec1501 Fixed windows install links (#2635) 2022-05-30 08:43:01 -07:00
kermieisinthehouse
88111e4064 Remove (Preview) From Korean 2022-05-25 19:37:31 -07:00
kermieisinthehouse
ba2979096a Update README.md 2022-05-25 19:35:24 -07:00
stash-translation-bot
34eb624438 Translations update from Stash (#2604)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (806 of 816 strings)

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

* Translated using Weblate (Korean)

Currently translated at 77.6% (634 of 816 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 86.0% (702 of 816 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 87.2% (712 of 816 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 87.2% (712 of 816 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 93.2% (761 of 816 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 83.7% (683 of 816 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% (816 of 816 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 87.6% (715 of 816 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 87.5% (714 of 816 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 87.5% (714 of 816 strings)

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

Co-authored-by: LiboSUN <learbo@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: yc <yechan24680@gmail.com>
Co-authored-by: - <adr.web@hotmail.fr>
Co-authored-by: Aa <jarruraita@outlook.com>
2022-05-25 19:33:11 -07:00
kermieisinthehouse
a142ec223e Add Korean Language (#2601) 2022-05-19 15:38:42 -07:00
WithoutPants
b2ac022357 Update changelog for #2594 2022-05-18 12:11:56 +10:00
stash-translation-bot
2653342ae1 Translations update from Stash (#2589)
* Translated using Weblate (Swedish)

Currently translated at 100.0% (816 of 816 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 100.0% (816 of 816 strings)

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

* Added translation using Weblate (Romanian)

* Translated using Weblate (Romanian)

Currently translated at 5.7% (47 of 816 strings)

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

* Added translation using Weblate (Korean)

* Translated using Weblate (Korean)

Currently translated at 68.8% (562 of 816 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

Co-authored-by: Alpaca Serious <srhsgsef@gmail.com>
Co-authored-by: 風林火山 <nezoko@digdig.org>
Co-authored-by: Imagine <pyrolaziano@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: yc <yechan24680@gmail.com>
2022-05-17 19:07:50 -07:00
DingDongSoLong4
df24f90735 Fix gallery scan (#2594)
The incorrect error check means that images is always 0, which means scanImages is also always true, which causes the images inside the zip to be unnecessarily rescanned every time.
2022-05-18 12:03:54 +10:00
kermieisinthehouse
1200d4472a CSS: Fix mobile filter tags (#2584) 2022-05-16 09:50:51 +10:00
kermieisinthehouse
55366fa66f Add Danish and Polish i18n support (#2586) 2022-05-13 12:45:05 -07:00
stash-translation-bot
c232372f0c Translations update from Stash (#2576)
* Translated using Weblate (Swedish)

Currently translated at 100.0% (797 of 797 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 100.0% (815 of 815 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (815 of 815 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.3% (810 of 815 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (816 of 816 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% (816 of 816 strings)

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

* Translated using Weblate (Danish)

Currently translated at 100.0% (816 of 816 strings)

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

* Added translation using Weblate (Czech)

* Translated using Weblate (Czech)

Currently translated at 0.4% (4 of 816 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Co-authored-by: Alpaca Serious <srhsgsef@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Hidden Hiddenson <078emil@protonmail.com>
Co-authored-by: Wasylq <Wasylq@protonmail.com>
Co-authored-by: BViking78 <bviking78@gmail.com>
Co-authored-by: Thor Skinderholm <el.thoro@gmail.com>
Co-authored-by: Mrgrpm <mrgrpm58@gmail.com>
2022-05-13 12:44:41 -07:00
kermieisinthehouse
53c6ae4ea5 Correct 'reload scrapes' path (#2583) (#2585)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
Co-authored-by: techie2000 <38585780+techie2000@users.noreply.github.com>
2022-05-13 12:08:20 -07:00
cj
dce4591911 Recommendations page bug fixes and refactoring (#2578)
* Changed Most Active Studios to Latest Studios
* dynamically create view all link and created message for view all
* created shared determineSlidesToScroll method
* removed added code in Shared/index.ts
* renamed getSlickSettings to getSlickSliderSettings
* Updated row headers to follow plex naming convention
* removed extra s in Sceness
* updated row header css to better align header text with view all anchor
2022-05-12 12:57:41 +10:00
WithoutPants
31cb8e2cbd Fix direct stream not working in Safari (#2581) 2022-05-12 12:10:46 +10:00
WithoutPants
ea2fcd9d7f Improve Handy integration (#2555)
* Refactor interactive into context
* Stop the interactive device when leaving page
* Show interactive state if not ready
* Handle navigation and looping
2022-05-10 16:38:34 +10:00
cj
bc85614ff9 Recommendation home page (#2571) 2022-05-10 11:51:20 +10:00
stash-translation-bot
1e7b85fbe5 Translations update from Stash (#2499)
* Translated using Weblate (Japanese)

Currently translated at 100.0% (791 of 791 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 99.8% (790 of 791 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% (791 of 791 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (791 of 791 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.8% (790 of 791 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (791 of 791 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (794 of 794 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (794 of 794 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (794 of 794 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.7% (792 of 794 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% (794 of 794 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (794 of 794 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 100.0% (794 of 794 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.8% (793 of 794 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (794 of 794 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% (794 of 794 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 27.2% (216 of 794 strings)

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

Co-authored-by: 風林火山 <nezoko@digdig.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Still <dev@stillu.cc>
Co-authored-by: BViking78 <5@example.com>
Co-authored-by: Coscosname <coscosname@gmail.com>
Co-authored-by: failead0r <6@example.com>
Co-authored-by: Alpaca Serious <srhsgsef@gmail.com>
Co-authored-by: Philip Wang <philpw99@gmail.com>
Co-authored-by: Hidden Hiddenson <078emil@protonmail.com>
2022-05-09 16:48:57 -07:00
WithoutPants
9ca3874707 Fix slideshow autoplaying when on another tab (#2563) 2022-05-06 12:23:06 +10:00
WithoutPants
73ded0d97d Prevent lightbox transition until specific number of scroll events (#2544)
* Delay before nav to next image on scroll
* Add config for scroll attempts before transition
2022-05-06 12:22:26 +10:00
cj
c1a096a1a6 Caption support (#2462)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-05-06 11:59:28 +10:00
HijackHornet
ab1b30ffb7 Clarifying Mingw installation and windows usage (#2566)
Because i lost over an hour making it work
2022-05-05 11:11:52 -07:00
WithoutPants
00e98a9c09 Fix thumbnail generation (#2561) 2022-05-05 12:11:10 +10:00
WithoutPants
d77f47824c Don't show dialog box when run from terminal (#2560) 2022-05-05 11:30:10 +10:00
WithoutPants
727644fab3 Update changelog 2022-05-05 11:09:59 +10:00
WithoutPants
dcd7595c07 Change playback rates and fix rates after seeking (#2550) 2022-05-05 11:06:47 +10:00
WithoutPants
ce175dcfc6 Fix audio not in video previews (#2547) 2022-05-05 11:04:01 +10:00
WithoutPants
1964481ff2 Play scene when scrubber clicked (#2546) 2022-05-05 11:03:46 +10:00
WithoutPants
a8456dd188 Remove windowsgui from cross-compile-windows build target 2022-05-04 17:15:28 +10:00
WithoutPants
a6f15273d9 Improve Windows stash behaviour (#2543)
* Rename manager.instance to Manager
* Show dialog message on fatal error on Windows
* Hide console windows explicitly on icon launch

Gets rid of the windowsgui flag, which causes all sorts of issues.
Instead, checks if stash was launched from the icon, and if so hides the console.

* Remove fixconsole
* Add changelog entries
2022-05-04 10:37:32 +10:00
WithoutPants
36aa51a187 Migrate vtt contents when hash changes (#2554) 2022-05-04 09:29:20 +10:00
WithoutPants
1a6f5619ae Encode query string (#2552) 2022-05-04 09:29:05 +10:00
WithoutPants
21a26fadd8 Generate single preview video for short scenes (#2553) 2022-05-04 09:28:48 +10:00
WithoutPants
20ffd4d51d Accept gif and webp for images (#2551) 2022-05-04 09:27:50 +10:00
WithoutPants
e87fd516d6 Fix streaming scenes not able to be deleted (#2549)
* Don't navigate away from scene if delete failed
* Close connection on cancel
2022-05-04 09:27:22 +10:00
InfiniteTF
0c2dc17e8e Fix crash when cancelling pending tasks (#2527) 2022-04-25 16:21:21 +10:00
WithoutPants
1b91937004 Fix image thumbnail generation (#2524)
* Better logging for thumbnail generation errors
* Reduce verbosity for thumbnail generation
* Provide stdin during thumbnail generation
2022-04-25 15:56:06 +10:00
WithoutPants
9e606feb76 Refresh marker panel on marker create (#2502)
* Update scene markers on create
* Improve display of markers without titles
* Fix marker title not populating
2022-04-21 11:33:04 +10:00
DingDongSoLong4
340f47cda8 Fix GraphQL Playground CSP directives for Firefox (#2518) 2022-04-18 12:25:46 +10:00
WithoutPants
aacf07feef Restructure ffmpeg (#2392)
* Refactor transcode generation
* Move phash generation into separate package
* Refactor image thumbnail generation
* Move JSONTime to separate package
* Ffmpeg refactoring
* Refactor live transcoding
* Refactor scene marker preview generation
* Refactor preview generation
* Refactor screenshot generation
* Refactor sprite generation
* Change ffmpeg.IsStreamable to return error
* Move frame rate calculation into ffmpeg
* Refactor file locking
* Refactor title set during scan
* Add missing lockmanager instance
* Return error instead of logging in MatchContainer
2022-04-18 10:50:10 +10:00
InfiniteTF
cdaa191155 Fix submission of scene drafts without performers (#2515) 2022-04-18 10:43:27 +10:00
SmallCoccinelle
401660e6a3 Hoist context, enable errchkjson (#2488)
* Make the script scraper context-aware

Connect the context to the command execution. This means command
execution can be aborted if the context is canceled. The context is
usually bound to user-interaction, i.e., a scraper operation issued
by the user. Hence, it seems correct to abort a command if the user
aborts.

* Enable errchkjson

Some json marshal calls are *safe* in that they can never fail. This is
conditional on the types of the the data being encoded. errchkjson finds
those calls which are unsafe, and also not checked for errors.

Add logging warnings to the place where unsafe encodings might happen.
This can help uncover usage bugs early in stash if they are tripped,
making debugging easier.

While here, keep the checker enabled in the linter to capture future
uses of json marshalling.

* Pass the context for zip file scanning.

* Pass the context in scanning

* Pass context, replace context.TODO()

Where applicable, pass the context down toward the lower functions in
the call stack. Replace uses of context.TODO() with the passed context.

This makes the code more context-aware, and you can rely on aborting
contexts to clean up subsystems to a far greater extent now.

I've left the cases where there is a context in a struct. My gut feeling
is that they have solutions that are nice, but they require more deep
thinking to unveil how to handle it.

* Remove context from task-structs

As a rule, contexts are better passed explicitly to functions than they
are passed implicitly via structs. In the case of tasks, we already
have a valid context in scope when creating the struct, so remove ctx
from the struct and use the scoped context instead.

With this change it is clear that the scanning functions are under a
context, and the task-starting caller has jurisdiction over the context
and its lifetime. A reader of the code don't have to figure out where
the context are coming from anymore.

While here, connect context.TODO() to the newly scoped context in most
of the scan code.

* Remove context from autotag struct too

* Make more context-passing explicit

In all of these cases, there is an applicable context which is close
in the call-tree. Hook up to this context.

* Simplify context passing in manager

The managers context handling generally wants to use an outer context
if applicable. However, the code doesn't pass it explicitly, but stores
it in a struct. Pull out the context from the struct and use it to
explicitly pass it.

At a later point in time, we probably want to handle this by handing
over the job to a different (program-lifetime) context for background
jobs, but this will do for a start.
2022-04-15 11:34:53 +10:00
WithoutPants
a7beeb32b0 Fix video layout on touch enabled devices (#2501) 2022-04-15 10:51:49 +10:00
dependabot[bot]
50e83a3555 Bump moment from 2.29.1 to 2.29.2 in /ui/v2.5 (#2495)
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-14 08:51:47 +10:00
1339 changed files with 133237 additions and 53350 deletions

View File

@@ -4,7 +4,6 @@ on:
push:
branches: [ develop, master ]
pull_request:
branches: [ develop ]
release:
types: [ published ]
@@ -13,7 +12,7 @@ concurrency:
cancel-in-progress: true
env:
COMPILER_IMAGE: stashapp/compiler:6
COMPILER_IMAGE: stashapp/compiler:7
jobs:
build:
@@ -28,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:
@@ -36,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
@@ -45,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
@@ -150,7 +149,9 @@ jobs:
CHECKSUMS_SHA1
- name: Master release
if: ${{ github.event_name == 'release' && github.ref != 'refs/tags/latest_develop' }}
# NOTE: this isn't perfect, but should cover most scenarios
# DON'T create tag names starting with "v" if they are not stable releases
if: ${{ github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') }}
uses: WithoutPants/github-release@v2.0.4
with:
token: "${{ secrets.GITHUB_TOKEN }}"
@@ -181,7 +182,9 @@ jobs:
bash ./docker/ci/x86_64/docker_push.sh development
- name: Release Docker
if: ${{ github.repository == 'stashapp/stash' && github.event_name == 'release' && github.ref != 'refs/tags/latest_develop' }}
# NOTE: this isn't perfect, but should cover most scenarios
# DON'T create tag names starting with "v" if they are not stable releases
if: ${{ github.repository == 'stashapp/stash' && github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') }}
env:
DOCKER_CLI_EXPERIMENTAL: enabled
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}

View File

@@ -9,7 +9,7 @@ on:
pull_request:
env:
COMPILER_IMAGE: stashapp/compiler:6
COMPILER_IMAGE: stashapp/compiler:7
jobs:
golangci:
@@ -36,13 +36,13 @@ jobs:
uses: golangci/golangci-lint-action@v2
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v1.45.2
version: latest
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
args: --modules-download-mode=vendor --timeout=3m
args: --modules-download-mode=vendor --timeout=5m
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true

10
.gitignore vendored
View File

@@ -16,13 +16,19 @@
*.out
# GraphQL generated output
pkg/models/generated_*.go
internal/api/generated_*.go
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

@@ -1,26 +1,23 @@
# options for analysis running
run:
timeout: 3m
timeout: 5m
modules-download-mode: vendor
linters:
disable-all: true
enable:
# Default set of linters from golangci-lint
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
# Linters added by the stash project.
# - contextcheck
- dogsled
# - errchkjson
- errchkjson
- errorlint
# - exhaustive
- exportloopref

View File

@@ -7,6 +7,8 @@ client:
models:
Date:
model: github.com/99designs/gqlgen/graphql.String
SceneDraftInput:
model: github.com/stashapp/stash/pkg/scraper/stashbox/graphql.SceneDraftInput
endpoint:
# This points to stashdb.org currently, but can be directed at any stash-box
# instance. It is used for generation only.

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,20 +47,21 @@ ifndef STASH_VERSION
endif
ifndef OFFICIAL_BUILD
$(eval OFFICIAL_BUILD := false)
$(eval OFFICIAL_BUILD := false)
endif
ifdef IS_WIN_OS
ifndef SUPPRESS_WINDOWSGUI
PLATFORM_SPECIFIC_LDFLAGS := -H windowsgui
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 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
@@ -77,7 +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: PLATFORM_SPECIFIC_LDFLAGS := -H windowsgui
cross-compile-windows: GO_BUILD_TAGS := $(GO_BUILD_TAGS_WINDOWS)
cross-compile-windows: build-release-static
cross-compile-macos-intel: export GOOS := darwin
@@ -85,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
@@ -93,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
@@ -113,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
@@ -131,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
@@ -138,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
@@ -169,6 +178,10 @@ generate-frontend:
generate-backend: touch-ui
go generate -mod=vendor ./cmd/stash
.PHONY: generate-dataloaders
generate-dataloaders:
go generate -mod=vendor ./internal/api/loaders
# Regenerates stash-box client files
.PHONY: generate-stash-box-client
generate-stash-box-client:

View File

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

View File

@@ -7,7 +7,6 @@ import (
"os/signal"
"syscall"
"github.com/apenwarr/fixconsole"
"github.com/stashapp/stash/internal/api"
"github.com/stashapp/stash/internal/desktop"
"github.com/stashapp/stash/internal/manager"
@@ -17,17 +16,22 @@ import (
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func init() {
// On Windows, attach to parent shell
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
fmt.Printf("FixConsoleOutput: %v\n", err)
}
}
func main() {
manager.Initialize()
api.Start()
defer recoverPanic()
_, err := manager.Initialize()
if err != nil {
panic(err)
}
go func() {
defer recoverPanic()
if err := api.Start(); err != nil {
handleError(err)
} else {
manager.GetInstance().Shutdown(0)
}
}()
go handleSignals()
desktop.Start(manager.GetInstance(), &manager.FaviconProvider{UIBox: ui.UIBox})
@@ -35,6 +39,21 @@ func main() {
blockForever()
}
func recoverPanic() {
if p := recover(); p != nil {
handleError(fmt.Errorf("Panic: %v", p))
}
}
func handleError(err error) {
if desktop.IsDesktop() {
desktop.FatalError(err)
manager.GetInstance().Shutdown(0)
} else {
panic(err)
}
}
func handleSignals() {
// handle signals
signals := make(chan os.Signal, 1)

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,7 @@ 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 && pip install --no-cache-dir mechanicalsoup cloudscraper
RUN ln -s /usr/bin/python3 /usr/bin/python
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
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

@@ -15,12 +15,12 @@ NOTE: You may need to run the `go get` commands outside the project directory to
### Windows
1. Download and install [Go for Windows](https://golang.org/dl/)
2. Download and install [MingW](https://sourceforge.net/projects/mingw-w64/)
2. Download and extract [MingW64](https://sourceforge.net/projects/mingw-w64/files/) (scroll down and select x86_64-posix-seh, dont use the autoinstaller it doesnt work)
3. Search for "advanced system settings" and open the system properties dialog.
1. Click the `Environment Variables` button
2. Under system variables find the `Path`. Edit and add `C:\Program Files\mingw-w64\*\mingw64\bin` (replace * with the correct path).
2. Under system variables find the `Path`. Edit and add `C:\MinGW\bin` (replace with the correct path to where you extracted MingW64).
NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
NOTE: The `make` command in Windows will be `mingw32-make` with MingW. For example `make pre-ui` will be `mingw32-make pre-ui`
### macOS
@@ -43,9 +43,10 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
## 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
@@ -53,7 +54,7 @@ This project uses a modification of the [CI-GoReleaser](https://github.com/bep/d
where the app can be cross-compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following
command to open a bash shell to the container to poke around:
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashappdev/compiler:latest /bin/bash`
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashapp/compiler:latest /bin/bash`
## Profiling

23
go.mod
View File

@@ -19,7 +19,7 @@ require (
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/jmoiron/sqlx v1.3.1
github.com/json-iterator/go v1.1.12
github.com/mattn/go-sqlite3 v1.14.6
github.com/mattn/go-sqlite3 v1.14.7
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/remeh/sizedwaitgroup v1.0.0
@@ -36,31 +36,36 @@ require (
github.com/vektra/mockery/v2 v2.10.0
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9
golang.org/x/sys v0.0.0-20220329152356-43be30ef3008
golang.org/x/net v0.0.0-20220722155237-a158d28d115b
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/text v0.3.7
golang.org/x/tools v0.1.10 // indirect
golang.org/x/tools v0.1.12 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
github.com/asticode/go-astisub v0.20.0
github.com/doug-martin/goqu/v9 v9.18.0
github.com/go-chi/httplog v0.2.1
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/hashicorp/golang-lru v0.5.4
github.com/kermieisinthehouse/gosx-notifier v0.1.1
github.com/kermieisinthehouse/systray v1.2.4
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/spf13/cast v1.4.1
github.com/vearutop/statigz v1.1.6
github.com/vektah/dataloaden v0.3.0
github.com/vektah/gqlparser/v2 v2.4.1
gopkg.in/guregu/null.v4 v4.0.0
)
require (
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/antchfx/xpath v1.2.0 // indirect
github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab // indirect
github.com/asticode/go-astikit v0.20.0 // indirect
github.com/asticode/go-astits v1.8.0 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -89,7 +94,6 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/zerolog v1.26.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.4.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/objx v0.2.0 // indirect
@@ -97,12 +101,11 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/urfave/cli/v2 v2.4.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
go 1.17
go 1.19

47
go.sum
View File

@@ -65,6 +65,8 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@@ -97,10 +99,6 @@ github.com/antchfx/xpath v1.2.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwq
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/arrow/go/arrow v0.0.0-20200601151325-b2287a20f230/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0=
github.com/apache/arrow/go/arrow v0.0.0-20210521153258-78c88a9f517b/go.mod h1:R4hW3Ug0s+n4CUsWHKOj00Pu01ZqU4x/hSF5kXUcXKQ=
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29/go.mod h1:JYWahgHer+Z2xbsgHPtaDYVWzeHDminu+YIBWkxpCAY=
github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab h1:CMGzRRCjnD50RjUFSArBLuCxiDvdp7b8YPAcikBEQ+k=
github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab/go.mod h1:nfFtvHn2Hgs9G1u0/J6LHQv//EksNC+7G8vXmd1VTJ8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@@ -108,6 +106,12 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asticode/go-astikit v0.20.0 h1:+7N+J4E4lWx2QOkRdOf6DafWJMv6O4RRfgClwQokrH8=
github.com/asticode/go-astikit v0.20.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astisub v0.20.0 h1:mKuLwgGkQj35RRHFiTcq+2hgR7g1mHiYiIkr9UNTmXw=
github.com/asticode/go-astisub v0.20.0/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8=
github.com/asticode/go-astits v1.8.0 h1:rf6aiiGn/QhlFjNON1n5plqF3Fs025XLUwiQ0NB6oZg=
github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ=
github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v1.3.2/go.mod h1:7OaACgj2SX3XGWnrIjGlJM22h6yD6MEWKvm7levnnM8=
github.com/aws/aws-sdk-go-v2 v1.6.0/go.mod h1:tI4KhsR5VkzlUa2DZAdwx7wCAYGwkZZ1H31PYrBFx1w=
@@ -204,6 +208,8 @@ github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompati
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY=
github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ=
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
@@ -246,8 +252,9 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
@@ -533,8 +540,9 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
@@ -568,8 +576,9 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
@@ -629,6 +638,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -690,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=
@@ -743,6 +752,8 @@ github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I=
github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
github.com/vearutop/statigz v1.1.6 h1:si1zvulh/6P4S/SjFticuKQ8/EgQISglaRuycj8PWso=
github.com/vearutop/statigz v1.1.6/go.mod h1:czAv7iXgPv/s+xsgXpVEhhD0NSOQ4wZPgmM/n7LANDI=
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/vektah/gqlparser/v2 v2.4.1 h1:QOyEn8DAPMUMARGMeshKDkDgNmVoEaEGiDB0uWxcSlQ=
github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
@@ -853,8 +864,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -910,8 +922,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 h1:0qxwC5n+ttVOINCBeRHO0nq9X7uy8SDsPoi5OaCdIEI=
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -955,7 +967,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1035,11 +1046,10 @@ 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-20220329152356-43be30ef3008 h1:pq9pwoi2rjLWvmiVser/lIOgiyA3fli4M+RfGVMA7nE=
golang.org/x/sys v0.0.0-20220329152356-43be30ef3008/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=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1069,6 +1079,7 @@ golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@@ -1126,14 +1137,14 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@@ -1298,6 +1309,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg=
gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

View File

@@ -4,43 +4,122 @@ schema:
- "graphql/schema/types/*.graphql"
- "graphql/schema/*.graphql"
exec:
filename: pkg/models/generated_exec.go
filename: internal/api/generated_exec.go
model:
filename: pkg/models/generated_models.go
filename: internal/api/generated_models.go
resolver:
filename: internal/api/resolver.go
type: Resolver
struct_tag: gqlgen
autobind:
- github.com/stashapp/stash/pkg/models
- github.com/stashapp/stash/pkg/plugin
- github.com/stashapp/stash/pkg/scraper
- github.com/stashapp/stash/internal/identify
- github.com/stashapp/stash/internal/dlna
- github.com/stashapp/stash/pkg/scraper/stashbox
models:
# Scalars
Timestamp:
model: github.com/stashapp/stash/pkg/models.Timestamp
# Objects
Gallery:
model: github.com/stashapp/stash/pkg/models.Gallery
Int64:
model: github.com/stashapp/stash/pkg/models.Int64
# define to force resolvers
Image:
model: github.com/stashapp/stash/pkg/models.Image
ImageFileType:
model: github.com/stashapp/stash/pkg/models.ImageFileType
Performer:
model: github.com/stashapp/stash/pkg/models.Performer
Scene:
model: github.com/stashapp/stash/pkg/models.Scene
SceneMarker:
model: github.com/stashapp/stash/pkg/models.SceneMarker
ScrapedItem:
model: github.com/stashapp/stash/pkg/models.ScrapedItem
Studio:
model: github.com/stashapp/stash/pkg/models.Studio
Movie:
model: github.com/stashapp/stash/pkg/models.Movie
Tag:
model: github.com/stashapp/stash/pkg/models.Tag
SceneFileType:
model: github.com/stashapp/stash/pkg/models.SceneFileType
SavedFilter:
model: github.com/stashapp/stash/pkg/models.SavedFilter
StashID:
fields:
title:
resolver: true
# autobind on config causes generation issues
StashConfig:
model: github.com/stashapp/stash/internal/manager/config.StashConfig
StashConfigInput:
model: github.com/stashapp/stash/internal/manager/config.StashConfigInput
StashBoxInput:
model: github.com/stashapp/stash/internal/manager/config.StashBoxInput
ConfigImageLightboxResult:
model: github.com/stashapp/stash/internal/manager/config.ConfigImageLightboxResult
ImageLightboxDisplayMode:
model: github.com/stashapp/stash/internal/manager/config.ImageLightboxDisplayMode
ImageLightboxScrollMode:
model: github.com/stashapp/stash/internal/manager/config.ImageLightboxScrollMode
ConfigDisableDropdownCreate:
model: github.com/stashapp/stash/internal/manager/config.ConfigDisableDropdownCreate
ScanMetadataOptions:
model: github.com/stashapp/stash/internal/manager/config.ScanMetadataOptions
AutoTagMetadataOptions:
model: github.com/stashapp/stash/internal/manager/config.AutoTagMetadataOptions
SceneParserInput:
model: github.com/stashapp/stash/internal/manager.SceneParserInput
SceneParserResult:
model: github.com/stashapp/stash/internal/manager.SceneParserResult
SceneMovieID:
model: github.com/stashapp/stash/internal/manager.SceneMovieID
SystemStatus:
model: github.com/stashapp/stash/internal/manager.SystemStatus
SystemStatusEnum:
model: github.com/stashapp/stash/internal/manager.SystemStatusEnum
ImportDuplicateEnum:
model: github.com/stashapp/stash/internal/manager.ImportDuplicateEnum
SetupInput:
model: github.com/stashapp/stash/internal/manager.SetupInput
MigrateInput:
model: github.com/stashapp/stash/internal/manager.MigrateInput
ScanMetadataInput:
model: github.com/stashapp/stash/internal/manager.ScanMetadataInput
GenerateMetadataInput:
model: github.com/stashapp/stash/internal/manager.GenerateMetadataInput
GeneratePreviewOptionsInput:
model: github.com/stashapp/stash/internal/manager.GeneratePreviewOptionsInput
AutoTagMetadataInput:
model: github.com/stashapp/stash/internal/manager.AutoTagMetadataInput
CleanMetadataInput:
model: github.com/stashapp/stash/internal/manager.CleanMetadataInput
StashBoxBatchPerformerTagInput:
model: github.com/stashapp/stash/internal/manager.StashBoxBatchPerformerTagInput
SceneStreamEndpoint:
model: github.com/stashapp/stash/internal/manager.SceneStreamEndpoint
ExportObjectTypeInput:
model: github.com/stashapp/stash/internal/manager.ExportObjectTypeInput
ExportObjectsInput:
model: github.com/stashapp/stash/internal/manager.ExportObjectsInput
ImportObjectsInput:
model: github.com/stashapp/stash/internal/manager.ImportObjectsInput
ScanMetaDataFilterInput:
model: github.com/stashapp/stash/internal/manager.ScanMetaDataFilterInput
# renamed types
BulkUpdateIdMode:
model: github.com/stashapp/stash/pkg/models.RelationshipUpdateMode
DLNAStatus:
model: github.com/stashapp/stash/internal/dlna.Status
DLNAIP:
model: github.com/stashapp/stash/internal/dlna.Dlnaip
IdentifySource:
model: github.com/stashapp/stash/internal/identify.Source
IdentifyMetadataTaskOptions:
model: github.com/stashapp/stash/internal/identify.Options
IdentifyMetadataInput:
model: github.com/stashapp/stash/internal/identify.Options
IdentifyMetadataOptions:
model: github.com/stashapp/stash/internal/identify.MetadataOptions
IdentifyFieldOptions:
model: github.com/stashapp/stash/internal/identify.FieldOptions
IdentifyFieldStrategy:
model: github.com/stashapp/stash/internal/identify.FieldStrategy
ScraperSource:
model: github.com/stashapp/stash/pkg/scraper.Source
# rebind inputs to types
StashIDInput:
model: github.com/stashapp/stash/pkg/models.StashID
IdentifySourceInput:
model: github.com/stashapp/stash/internal/identify.Source
IdentifyFieldOptionsInput:
model: github.com/stashapp/stash/internal/identify.FieldOptions
IdentifyMetadataOptionsInput:
model: github.com/stashapp/stash/internal/identify.MetadataOptions
ScraperSourceInput:
model: github.com/stashapp/stash/pkg/scraper.Source

View File

@@ -5,6 +5,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
excludeImage
}
databasePath
backupDirectoryPath
generatedPath
metadataPath
scrapersPath
@@ -62,6 +63,10 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
showStudioAsText
css
cssEnabled
javascript
javascriptEnabled
customLocales
customLocalesEnabled
language
imageLightbox {
slideshowDelay
@@ -69,6 +74,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
scaleUp
resetZoomOnNav
scrollMode
scrollAttemptsBeforeChange
}
disableDropdownCreate {
performer
@@ -184,4 +190,5 @@ fragment ConfigData on ConfigResult {
defaults {
...ConfigDefaultSettingsData
}
ui
}

View File

@@ -0,0 +1,46 @@
fragment FolderData on Folder {
id
path
}
fragment VideoFileData on VideoFile {
id
path
size
mod_time
duration
video_codec
audio_codec
width
height
frame_rate
bit_rate
fingerprints {
type
value
}
}
fragment ImageFileData on ImageFile {
id
path
size
mod_time
width
height
fingerprints {
type
value
}
}
fragment GalleryFileData on GalleryFile {
id
path
size
mod_time
fingerprints {
type
value
}
}

View File

@@ -1,19 +1,21 @@
fragment SlimGalleryData on Gallery {
id
checksum
path
title
date
url
details
rating
rating100
organized
files {
...GalleryFileData
}
folder {
...FolderData
}
image_count
cover {
file {
size
width
height
files {
...ImageFileData
}
paths {
@@ -37,8 +39,6 @@ fragment SlimGalleryData on Gallery {
image_path
}
scenes {
id
title
path
...SlimSceneData
}
}

View File

@@ -1,18 +1,21 @@
fragment GalleryData on Gallery {
id
checksum
path
created_at
updated_at
title
date
url
details
rating
rating100
organized
images {
...SlimImageData
files {
...GalleryFileData
}
folder {
...FolderData
}
cover {
...SlimImageData
}

View File

@@ -1,16 +1,12 @@
fragment SlimImageData on Image {
id
checksum
title
rating
rating100
organized
o_counter
path
file {
size
width
height
files {
...ImageFileData
}
paths {
@@ -20,8 +16,13 @@ fragment SlimImageData on Image {
galleries {
id
path
title
files {
path
}
folder {
path
}
}
studio {

View File

@@ -1,18 +1,14 @@
fragment ImageData on Image {
id
checksum
title
rating
rating100
organized
o_counter
path
created_at
updated_at
file {
size
width
height
files {
...ImageFileData
}
paths {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,22 @@
fragment SlimSceneData on Scene {
id
checksum
oshash
title
code
details
director
url
date
rating
rating100
o_counter
organized
path
phash
interactive
interactive_speed
resume_time
play_duration
play_count
file {
size
duration
video_codec
audio_codec
width
height
framerate
bitrate
files {
...VideoFileData
}
paths {
@@ -31,21 +25,27 @@ fragment SlimSceneData on Scene {
stream
webp
vtt
chapters_vtt
sprite
funscript
interactive_heatmap
caption
}
scene_markers {
id
title
seconds
primary_tag {
id
name
}
}
galleries {
id
path
files {
path
}
title
}

View File

@@ -1,30 +1,29 @@
fragment SceneData on Scene {
id
checksum
oshash
title
code
details
director
url
date
rating
rating100
o_counter
organized
path
phash
interactive
interactive_speed
captions {
language_code
caption_type
}
created_at
updated_at
resume_time
last_played_at
play_duration
play_count
file {
size
duration
video_codec
audio_codec
width
height
framerate
bitrate
files {
...VideoFileData
}
paths {
@@ -33,10 +32,10 @@ fragment SceneData on Scene {
stream
webp
vtt
chapters_vtt
sprite
funscript
interactive_heatmap
caption
}
scene_markers {

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
fragment TagData on Tag {
id
name
description
aliases
ignore_auto_tag
image_path

View File

@@ -36,6 +36,10 @@ mutation ConfigureDefaults($input: ConfigDefaultSettingsInput!) {
}
}
mutation ConfigureUI($input: Map!) {
configureUI(input: $input)
}
mutation GenerateAPIKey($input: GenerateAPIKeyInput!) {
generateAPIKey(input: $input)
}

View File

@@ -0,0 +1,3 @@
mutation DeleteFiles($ids: [ID!]!) {
deleteFiles(ids: $ids)
}

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

@@ -1,4 +1,10 @@
query FindSavedFilters($mode: FilterMode!) {
query FindSavedFilter($id: ID!) {
findSavedFilter(id: $id) {
...SavedFilterData
}
}
query FindSavedFilters($mode: FilterMode) {
findSavedFilters(mode: $mode) {
...SavedFilterData
}

View File

@@ -4,7 +4,7 @@ query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene
filesize
duration
scenes {
...SceneData
...SlimSceneData
}
}
}
@@ -52,7 +52,9 @@ query ParseSceneFilenames($filter: FindFilterType!, $config: SceneParserInput!)
...SlimSceneData
}
title
code
details
director
url
date
rating

View File

@@ -1,7 +1,8 @@
"""The query root for this schema"""
type Query {
# Filters
findSavedFilters(mode: FilterMode!): [SavedFilter!]!
findSavedFilter(id: ID!): SavedFilter
findSavedFilters(mode: FilterMode): [SavedFilter!]!
findDefaultFilter(mode: FilterMode!): SavedFilter
"""Find a scene by ID or Checksum"""
@@ -114,12 +115,6 @@ type Query {
"""Scrape a list of performers from a query"""
scrapeFreeonesPerformerList(query: String!): [String!]! @deprecated(reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones")
"""Query StashBox for scenes"""
queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]! @deprecated(reason: "use scrapeSingleScene or scrapeMultiScenes")
"""Query StashBox for performers"""
queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]! @deprecated(reason: "use scrapeSinglePerformer or scrapeMultiPerformers")
# === end deprecated methods ===
# Plugins
"""List loaded plugins"""
plugins: [Plugin!]
@@ -167,7 +162,9 @@ type Mutation {
setup(input: SetupInput!): Boolean!
migrate(input: MigrateInput!): Boolean!
sceneCreate(input: SceneCreateInput!): Scene
sceneUpdate(input: SceneUpdateInput!): Scene
sceneMerge(input: SceneMergeInput!): Scene
bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!]
sceneDestroy(input: SceneDestroyInput!): Boolean!
scenesDestroy(input: ScenesDestroyInput!): Boolean!
@@ -180,6 +177,12 @@ type Mutation {
"""Resets the o-counter for a scene to 0. Returns the new value"""
sceneResetO(id: ID!): Int!
"""Sets the resume time point (if provided) and adds the provided duration to the scene's play duration"""
sceneSaveActivity(id: ID!, resume_time: Float, playDuration: Float): Boolean!
"""Increments the play count for the scene. Returns the new play count value."""
sceneIncrementPlayCount(id: ID!): Int!
"""Generates screenshot at specified time in seconds. Leave empty to generate default screenshot"""
sceneGenerateScreenshot(id: ID!, at: Float): String!
@@ -187,6 +190,8 @@ type Mutation {
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean!
sceneAssignFile(input: AssignSceneFileInput!): Boolean!
imageUpdate(input: ImageUpdateInput!): Image
bulkImageUpdate(input: BulkImageUpdateInput!): [Image!]
imageDestroy(input: ImageDestroyInput!): Boolean!
@@ -232,6 +237,8 @@ type Mutation {
tagsDestroy(ids: [ID!]!): Boolean!
tagsMerge(input: TagsMergeInput!): Tag
deleteFiles(ids: [ID!]!): Boolean!
# Saved filters
saveFilter(input: SaveFilterInput!): SavedFilter!
destroySavedFilter(input: DestroyFilterInput!): Boolean!
@@ -244,6 +251,11 @@ type Mutation {
configureScraping(input: ConfigScrapingInput!): ConfigScrapingResult!
configureDefaults(input: ConfigDefaultSettingsInput!): ConfigDefaultSettingsResult!
# overwrites the entire UI configuration
configureUI(input: Map!): Map!
# sets a single UI key value
configureUISetting(key: String!, value: Any): Map!
"""Generate and set (or clear) API key"""
generateAPIKey(input: GenerateAPIKeyInput!): String!

View File

@@ -37,6 +37,8 @@ input ConfigGeneralInput {
stashes: [StashConfigInput!]
"""Path to the SQLite database"""
databasePath: String
"""Path to backup directory"""
backupDirectoryPath: String
"""Path to generated files"""
generatedPath: String
"""Path to import/export files"""
@@ -116,6 +118,8 @@ type ConfigGeneralResult {
stashes: [StashConfig!]!
"""Path to the SQLite database"""
databasePath: String!
"""Path to backup directory"""
backupDirectoryPath: String!
"""Path to generated files"""
generatedPath: String!
"""Path to import/export files"""
@@ -217,6 +221,7 @@ input ConfigImageLightboxInput {
scaleUp: Boolean
resetZoomOnNav: Boolean
scrollMode: ImageLightboxScrollMode
scrollAttemptsBeforeChange: Int
}
type ConfigImageLightboxResult {
@@ -225,6 +230,7 @@ type ConfigImageLightboxResult {
scaleUp: Boolean
resetZoomOnNav: Boolean
scrollMode: ImageLightboxScrollMode
scrollAttemptsBeforeChange: Int!
}
input ConfigInterfaceInput {
@@ -257,6 +263,14 @@ input ConfigInterfaceInput {
"""Custom CSS"""
css: String
cssEnabled: Boolean
"""Custom Javascript"""
javascript: String
javascriptEnabled: Boolean
"""Custom Locales"""
customLocales: String
customLocalesEnabled: Boolean
"""Interface language"""
language: String
@@ -320,6 +334,14 @@ type ConfigInterfaceResult {
css: String
cssEnabled: Boolean
"""Custom Javascript"""
javascript: String
javascriptEnabled: Boolean
"""Custom Locales"""
customLocales: String
customLocalesEnabled: Boolean
"""Interface language"""
language: String
@@ -411,6 +433,7 @@ type ConfigResult {
dlna: ConfigDLNAResult!
scraping: ConfigScrapingResult!
defaults: ConfigDefaultSettingsResult!
ui: Map!
}
"""Directory structure of a path"""

View File

@@ -0,0 +1,97 @@
type Fingerprint {
type: String!
value: String!
}
type Folder {
id: ID!
path: String!
parent_folder_id: ID
zip_file_id: ID
mod_time: Time!
created_at: Time!
updated_at: Time!
}
interface BaseFile {
id: ID!
path: String!
basename: String!
parent_folder_id: ID!
zip_file_id: ID
mod_time: Time!
size: Int64!
fingerprints: [Fingerprint!]!
created_at: Time!
updated_at: Time!
}
type VideoFile implements BaseFile {
id: ID!
path: String!
basename: String!
parent_folder_id: ID!
zip_file_id: ID
mod_time: Time!
size: Int64!
fingerprints: [Fingerprint!]!
format: String!
width: Int!
height: Int!
duration: Float!
video_codec: String!
audio_codec: String!
frame_rate: Float!
bit_rate: Int!
created_at: Time!
updated_at: Time!
}
type ImageFile implements BaseFile {
id: ID!
path: String!
basename: String!
parent_folder_id: ID!
zip_file_id: ID
mod_time: Time!
size: Int64!
fingerprints: [Fingerprint!]!
width: Int!
height: Int!
created_at: Time!
updated_at: Time!
}
type GalleryFile implements BaseFile {
id: ID!
path: String!
basename: String!
parent_folder_id: ID!
zip_file_id: ID
mod_time: Time!
size: Int64!
fingerprints: [Fingerprint!]!
created_at: Time!
updated_at: Time!
}

View File

@@ -39,6 +39,14 @@ input PHashDuplicationCriterionInput {
distance: Int
}
input StashIDCriterionInput {
"""If present, this value is treated as a predicate.
That is, it will filter based on stash_ids with the matching endpoint"""
endpoint: String
stash_id: String
modifier: CriterionModifier!
}
input PerformerFilterType {
AND: PerformerFilterType
OR: PerformerFilterType
@@ -60,7 +68,9 @@ input PerformerFilterType {
"""Filter by eye color"""
eye_color: StringCriterionInput
"""Filter by height"""
height: StringCriterionInput
height: StringCriterionInput @deprecated(reason: "Use height_cm instead")
"""Filter by height in cm"""
height_cm: IntCriterionInput
"""Filter by measurements"""
measurements: StringCriterionInput
"""Filter by fake tits value"""
@@ -88,9 +98,13 @@ input PerformerFilterType {
"""Filter by gallery count"""
gallery_count: IntCriterionInput
"""Filter by StashID"""
stash_id: StringCriterionInput
stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead")
"""Filter by StashID"""
stash_id_endpoint: StashIDCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"""Filter by url"""
url: StringCriterionInput
"""Filter by hair color"""
@@ -103,6 +117,14 @@ input PerformerFilterType {
studios: HierarchicalMultiCriterionInput
"""Filter by autotag ignore value"""
ignore_auto_tag: Boolean
"""Filter by birthdate"""
birthdate: DateCriterionInput
"""Filter by death date"""
death_date: DateCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input SceneMarkerFilterType {
@@ -114,6 +136,16 @@ input SceneMarkerFilterType {
scene_tags: HierarchicalMultiCriterionInput
"""Filter to only include scene markers with these performers"""
performers: MultiCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
"""Filter by scene date"""
scene_date: DateCriterionInput
"""Filter by cscene reation time"""
scene_created_at: TimestampCriterionInput
"""Filter by lscene ast update time"""
scene_updated_at: TimestampCriterionInput
}
input SceneFilterType {
@@ -121,8 +153,11 @@ input SceneFilterType {
OR: SceneFilterType
NOT: SceneFilterType
id: IntCriterionInput
title: StringCriterionInput
code: StringCriterionInput
details: StringCriterionInput
director: StringCriterionInput
"""Filter by file oshash"""
oshash: StringCriterionInput
@@ -132,8 +167,12 @@ input SceneFilterType {
phash: StringCriterionInput
"""Filter by path"""
path: StringCriterionInput
"""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"""
@@ -167,13 +206,29 @@ 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"""
interactive: Boolean
"""Filter by InteractiveSpeed"""
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 {
@@ -185,7 +240,9 @@ input MovieFilterType {
"""Filter by duration (in seconds)"""
duration: IntCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"""Filter to only include movies with this studio"""
studios: HierarchicalMultiCriterionInput
"""Filter to only include movies missing this property"""
@@ -194,6 +251,12 @@ input MovieFilterType {
url: StringCriterionInput
"""Filter to only include movies where performer appears in a scene"""
performers: MultiCriterionInput
"""Filter by date"""
date: DateCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input StudioFilterType {
@@ -206,11 +269,15 @@ input StudioFilterType {
"""Filter to only include studios with this parent studio"""
parents: MultiCriterionInput
"""Filter by StashID"""
stash_id: StringCriterionInput
stash_id: StringCriterionInput @deprecated(reason: "Use stash_id_endpoint instead")
"""Filter by StashID"""
stash_id_endpoint: StashIDCriterionInput
"""Filter to only include studios missing this property"""
is_missing: String
"""Filter by rating"""
rating: IntCriterionInput
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"""Filter by scene count"""
scene_count: IntCriterionInput
"""Filter by image count"""
@@ -223,6 +290,10 @@ input StudioFilterType {
aliases: StringCriterionInput
"""Filter by autotag ignore value"""
ignore_auto_tag: Boolean
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input GalleryFilterType {
@@ -230,6 +301,7 @@ input GalleryFilterType {
OR: GalleryFilterType
NOT: GalleryFilterType
id: IntCriterionInput
title: StringCriterionInput
details: StringCriterionInput
@@ -237,12 +309,16 @@ input GalleryFilterType {
checksum: StringCriterionInput
"""Filter by path"""
path: StringCriterionInput
"""Filter by zip-file count"""
file_count: IntCriterionInput
"""Filter to only include galleries missing this property"""
is_missing: String
"""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"""
@@ -267,6 +343,12 @@ input GalleryFilterType {
image_count: IntCriterionInput
"""Filter by url"""
url: StringCriterionInput
"""Filter by date"""
date: DateCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input TagFilterType {
@@ -280,6 +362,9 @@ input TagFilterType {
"""Filter by tag aliases"""
aliases: StringCriterionInput
"""Filter by tag description"""
description: StringCriterionInput
"""Filter to only include tags missing this property"""
is_missing: String
@@ -312,6 +397,12 @@ input TagFilterType {
"""Filter by autotag ignore value"""
ignore_auto_tag: Boolean
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
input ImageFilterType {
@@ -321,12 +412,18 @@ input ImageFilterType {
title: StringCriterionInput
""" Filter by image id"""
id: IntCriterionInput
"""Filter by file checksum"""
checksum: StringCriterionInput
"""Filter by path"""
path: StringCriterionInput
"""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"""
@@ -351,6 +448,10 @@ input ImageFilterType {
performer_favorite: Boolean
"""Filter to only include images with these galleries"""
galleries: MultiCriterionInput
"""Filter by creation time"""
created_at: TimestampCriterionInput
"""Filter by last update time"""
updated_at: TimestampCriterionInput
}
enum CriterionModifier {
@@ -407,6 +508,18 @@ input HierarchicalMultiCriterionInput {
depth: Int
}
input DateCriterionInput {
value: String!
value2: String
modifier: CriterionModifier!
}
input TimestampCriterionInput {
value: String!
value2: String
modifier: CriterionModifier!
}
enum FilterMode {
SCENES,
PERFORMERS,

View File

@@ -1,17 +1,23 @@
"""Gallery type"""
type Gallery {
id: ID!
checksum: String!
path: String
checksum: String! @deprecated(reason: "Use files.fingerprints")
path: String @deprecated(reason: "Use files.path")
title: String
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!
file_mod_time: Time
file_mod_time: Time @deprecated(reason: "Use files.mod_time")
files: [GalleryFile!]!
folder: Folder
scenes: [Scene!]!
studio: Studio
@@ -20,22 +26,19 @@ type Gallery {
performers: [Performer!]!
"""The images in the gallery"""
images: [Image!]! # Resolver
images: [Image!]! @deprecated(reason: "Use findImages")
cover: Image
}
type GalleryFilesType {
index: Int!
name: String
path: String
}
input GalleryCreateInput {
title: String!
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
@@ -50,12 +53,17 @@ 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
tag_ids: [ID!]
performer_ids: [ID!]
primary_file_id: ID
}
input BulkGalleryUpdateInput {
@@ -64,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

@@ -1,16 +1,21 @@
type Image {
id: ID!
checksum: String
checksum: String @deprecated(reason: "Use files.fingerprints")
title: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
o_counter: Int
organized: Boolean!
path: String!
path: String! @deprecated(reason: "Use files.path")
created_at: Time!
updated_at: Time!
file_mod_time: Time
file_mod_time: Time @deprecated(reason: "Use files.mod_time")
file: ImageFileType! # Resolver
file: ImageFileType! @deprecated(reason: "Use files.mod_time")
files: [ImageFile!]!
paths: ImagePathsType! # Resolver
galleries: [Gallery!]!
@@ -20,9 +25,10 @@ type Image {
}
type ImageFileType {
size: Int
width: Int
height: Int
mod_time: Time!
size: Int!
width: Int!
height: Int!
}
type ImagePathsType {
@@ -34,20 +40,28 @@ input ImageUpdateInput {
clientMutationId: String
id: ID!
title: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
studio_id: ID
performer_ids: [ID!]
tag_ids: [ID!]
gallery_ids: [ID!]
primary_file_id: ID
}
input BulkImageUpdateInput {
clientMutationId: String
ids: [ID!]
title: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
studio_id: ID

View File

@@ -71,10 +71,19 @@ input ScanMetaDataFilterInput {
input ScanMetadataInput {
paths: [String!]
# useFileMetadata is deprecated with the new file management system
# if this functionality is desired, then we can make a built in scraper instead.
"""Set name, date, details from metadata (if present)"""
useFileMetadata: Boolean
useFileMetadata: Boolean @deprecated(reason: "Not implemented")
# stripFileExtension is deprecated since we no longer set the title from the
# filename - it is automatically returned if the object has no title. If this
# functionality is desired, then we could make this an option to not include
# the extension in the auto-generated title.
"""Strip file extension from title"""
stripFileExtension: Boolean
stripFileExtension: Boolean @deprecated(reason: "Not implemented")
"""Generate previews during scan"""
scanGeneratePreviews: Boolean
"""Generate image previews during scan"""

View File

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

View File

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

View File

@@ -4,4 +4,11 @@ Timestamp is a point in time. It is always output as RFC3339-compatible time poi
It can be input as a RFC3339 string, or as "<4h" for "4 hours in the past" or ">5m"
for "5 minutes in the future"
"""
scalar Timestamp
scalar Timestamp
# generic JSON object
scalar Map
scalar Any
scalar Int64

View File

@@ -15,10 +15,11 @@ 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
caption: String # Resolver
}
type SceneMovie {
@@ -26,26 +27,46 @@ type SceneMovie {
scene_index: Int
}
type VideoCaption {
language_code: String!
caption_type: String!
}
type Scene {
id: ID!
checksum: String
oshash: String
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!
phash: String
path: String! @deprecated(reason: "Use files.path")
phash: String @deprecated(reason: "Use files.fingerprints")
interactive: Boolean!
interactive_speed: Int
captions: [VideoCaption!]
created_at: Time!
updated_at: Time!
file_mod_time: Time
"""The last time play count was updated"""
last_played_at: Time
"""The time index a scene was left at"""
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! # Resolver
file: SceneFileType! @deprecated(reason: "Use files")
files: [VideoFile!]!
paths: ScenePathsType! # Resolver
scene_markers: [SceneMarker!]!
@@ -65,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!]
@@ -82,6 +106,44 @@ input SceneUpdateInput {
"""This should be a URL or a base64 encoded data URL"""
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
}
enum BulkUpdateIdMode {
@@ -99,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
@@ -147,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!]
@@ -173,3 +245,17 @@ type SceneStreamEndpoint {
mime_type: String
label: String
}
input AssignSceneFileInput {
scene_id: ID!
file_id: ID!
}
input SceneMergeInput {
"""If destination scene has no files, then the primary file of the
first source scene will be assigned as primary"""
source: [ID!]!
destination: ID!
# values defined here will override values in the destination
values: SceneUpdateInput
}

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
type Tag {
id: ID!
name: String!
description: String
aliases: [String!]!
ignore_auto_tag: Boolean!
created_at: Time!
@@ -19,6 +20,7 @@ type Tag {
input TagCreateInput {
name: String!
description: String
aliases: [String!]
ignore_auto_tag: Boolean
@@ -32,6 +34,7 @@ input TagCreateInput {
input TagUpdateInput {
id: ID!
name: String
description: String
aliases: [String!]
ignore_auto_tag: Boolean

View File

@@ -94,7 +94,9 @@ fragment FingerprintFragment on Fingerprint {
fragment SceneFragment on Scene {
id
title
code
details
director
duration
date
urls {
@@ -129,6 +131,12 @@ query FindScenesByFullFingerprints($fingerprints: [FingerprintQueryInput!]!) {
}
}
query FindScenesBySceneFingerprints($fingerprints: [[FingerprintQueryInput!]!]!) {
findScenesBySceneFingerprints(fingerprints: $fingerprints) {
...SceneFragment
}
}
query SearchScene($term: String!) {
searchScene(term: $term) {
...SceneFragment

View File

@@ -26,7 +26,8 @@ const (
)
func allowUnauthenticated(r *http.Request) bool {
return strings.HasPrefix(r.URL.Path, loginEndPoint) || r.URL.Path == "/css"
// #2715 - allow access to UI files
return strings.HasPrefix(r.URL.Path, loginEndPoint) || r.URL.Path == "/css" || strings.HasPrefix(r.URL.Path, "/assets")
}
func authenticateHandler() func(http.Handler) http.Handler {

View File

@@ -3,7 +3,9 @@ package api
import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/models"
@@ -18,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{} {
@@ -89,6 +107,22 @@ 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{}
}
return models.NewOptionalStringPtr(value)
}
func (t changesetTranslator) sqliteDate(value *string, field string) *models.SQLiteDate {
if !t.hasField(field) {
return nil
@@ -104,6 +138,42 @@ func (t changesetTranslator) sqliteDate(value *string, field string) *models.SQL
return ret
}
func (t changesetTranslator) optionalDate(value *string, field string) models.OptionalDate {
if !t.hasField(field) {
return models.OptionalDate{}
}
if value == nil {
return models.OptionalDate{
Set: true,
Null: true,
}
}
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
@@ -119,6 +189,64 @@ 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{}
}
return models.NewOptionalIntPtr(value)
}
func (t changesetTranslator) nullInt64FromString(value *string, field string) *sql.NullInt64 {
if !t.hasField(field) {
return nil
@@ -134,17 +262,45 @@ func (t changesetTranslator) nullInt64FromString(value *string, field string) *s
return ret
}
func (t changesetTranslator) nullBool(value *bool, field string) *sql.NullBool {
func (t changesetTranslator) optionalIntFromString(value *string, field string) (models.OptionalInt, error) {
if !t.hasField(field) {
return nil
return models.OptionalInt{}, nil
}
ret := &sql.NullBool{}
if value != nil {
ret.Bool = *value
ret.Valid = true
if value == nil {
return models.OptionalInt{
Set: true,
Null: true,
}, nil
}
return ret
vv, err := strconv.Atoi(*value)
if err != nil {
return models.OptionalInt{}, fmt.Errorf("converting %v to int: %w", *value, err)
}
return models.NewOptionalInt(vv), nil
}
func (t changesetTranslator) bool(value *bool, field string) bool {
if value == nil {
return false
}
return *value
}
func (t changesetTranslator) optionalBool(value *bool, field string) models.OptionalBool {
if !t.hasField(field) {
return models.OptionalBool{}
}
return models.NewOptionalBoolPtr(value)
}
func (t changesetTranslator) optionalFloat64(value *float64, field string) models.OptionalFloat64 {
if !t.hasField(field) {
return models.OptionalFloat64{}
}
return models.NewOptionalFloat64Ptr(value)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/hash"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)
type imageBox struct {
@@ -32,6 +33,10 @@ func newImageBox(box fs.FS) (*imageBox, error) {
}
err := fs.WalkDir(box, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
@@ -82,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.
@@ -91,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

@@ -0,0 +1,261 @@
//go:generate go run -mod=vendor github.com/vektah/dataloaden SceneLoader int *github.com/stashapp/stash/pkg/models.Scene
//go:generate go run -mod=vendor github.com/vektah/dataloaden GalleryLoader int *github.com/stashapp/stash/pkg/models.Gallery
//go:generate go run -mod=vendor github.com/vektah/dataloaden ImageLoader int *github.com/stashapp/stash/pkg/models.Image
//go:generate go run -mod=vendor github.com/vektah/dataloaden PerformerLoader int *github.com/stashapp/stash/pkg/models.Performer
//go:generate go run -mod=vendor github.com/vektah/dataloaden StudioLoader int *github.com/stashapp/stash/pkg/models.Studio
//go:generate go run -mod=vendor github.com/vektah/dataloaden TagLoader int *github.com/stashapp/stash/pkg/models.Tag
//go:generate go run -mod=vendor github.com/vektah/dataloaden MovieLoader int *github.com/stashapp/stash/pkg/models.Movie
//go:generate go run -mod=vendor github.com/vektah/dataloaden FileLoader github.com/stashapp/stash/pkg/file.ID github.com/stashapp/stash/pkg/file.File
//go:generate go run -mod=vendor github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
//go:generate go run -mod=vendor github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
//go:generate go run -mod=vendor github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
package loaders
import (
"context"
"net/http"
"time"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
)
type contextKey struct{ name string }
var (
loadersCtxKey = &contextKey{"loaders"}
)
const (
wait = 1 * time.Millisecond
maxBatch = 100
)
type Loaders struct {
SceneByID *SceneLoader
SceneFiles *SceneFileIDsLoader
ImageFiles *ImageFileIDsLoader
GalleryFiles *GalleryFileIDsLoader
GalleryByID *GalleryLoader
ImageByID *ImageLoader
PerformerByID *PerformerLoader
StudioByID *StudioLoader
TagByID *TagLoader
MovieByID *MovieLoader
FileByID *FileLoader
}
type Middleware struct {
DatabaseProvider txn.DatabaseProvider
Repository manager.Repository
}
func (m Middleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ldrs := Loaders{
SceneByID: &SceneLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchScenes(ctx),
},
GalleryByID: &GalleryLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchGalleries(ctx),
},
ImageByID: &ImageLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchImages(ctx),
},
PerformerByID: &PerformerLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchPerformers(ctx),
},
StudioByID: &StudioLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchStudios(ctx),
},
TagByID: &TagLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchTags(ctx),
},
MovieByID: &MovieLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchMovies(ctx),
},
FileByID: &FileLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchFiles(ctx),
},
SceneFiles: &SceneFileIDsLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchScenesFileIDs(ctx),
},
ImageFiles: &ImageFileIDsLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchImagesFileIDs(ctx),
},
GalleryFiles: &GalleryFileIDsLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchGalleriesFileIDs(ctx),
},
}
newCtx := context.WithValue(r.Context(), loadersCtxKey, ldrs)
next.ServeHTTP(w, r.WithContext(newCtx))
})
}
func From(ctx context.Context) Loaders {
return ctx.Value(loadersCtxKey).(Loaders)
}
func toErrorSlice(err error) []error {
if err != nil {
return []error{err}
}
return nil
}
func (m Middleware) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithDatabase(ctx, m.DatabaseProvider, fn)
}
func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models.Scene, []error) {
return func(keys []int) (ret []*models.Scene, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Scene.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models.Image, []error) {
return func(keys []int) (ret []*models.Image, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Image.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchGalleries(ctx context.Context) func(keys []int) ([]*models.Gallery, []error) {
return func(keys []int) (ret []*models.Gallery, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Gallery.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*models.Performer, []error) {
return func(keys []int) (ret []*models.Performer, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Performer.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*models.Studio, []error) {
return func(keys []int) (ret []*models.Studio, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Studio.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.Tag, []error) {
return func(keys []int) (ret []*models.Tag, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Tag.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models.Movie, []error) {
return func(keys []int) (ret []*models.Movie, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Movie.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchFiles(ctx context.Context) func(keys []file.ID) ([]file.File, []error) {
return func(keys []file.ID) (ret []file.File, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.File.Find(ctx, keys...)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
return func(keys []int) (ret [][]file.ID, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Scene.GetManyFileIDs(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
return func(keys []int) (ret [][]file.ID, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Image.GetManyFileIDs(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
return func(keys []int) (ret [][]file.ID, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Gallery.GetManyFileIDs(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}

View File

@@ -0,0 +1,221 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/file"
)
// FileLoaderConfig captures the config to create a new FileLoader
type FileLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []file.ID) ([]file.File, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewFileLoader creates a new FileLoader given a fetch, wait, and maxBatch
func NewFileLoader(config FileLoaderConfig) *FileLoader {
return &FileLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// FileLoader batches and caches requests
type FileLoader struct {
// this method provides the data for the loader
fetch func(keys []file.ID) ([]file.File, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[file.ID]file.File
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *fileLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type fileLoaderBatch struct {
keys []file.ID
data []file.File
error []error
closing bool
done chan struct{}
}
// Load a File by key, batching and caching will be applied automatically
func (l *FileLoader) Load(key file.ID) (file.File, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a File.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *FileLoader) LoadThunk(key file.ID) func() (file.File, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (file.File, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &fileLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (file.File, error) {
<-batch.done
var data file.File
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *FileLoader) LoadAll(keys []file.ID) ([]file.File, []error) {
results := make([]func() (file.File, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
files := make([]file.File, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
files[i], errors[i] = thunk()
}
return files, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Files.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *FileLoader) LoadAllThunk(keys []file.ID) func() ([]file.File, []error) {
results := make([]func() (file.File, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]file.File, []error) {
files := make([]file.File, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
files[i], errors[i] = thunk()
}
return files, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *FileLoader) Prime(key file.ID, value file.File) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
l.unsafeSet(key, value)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *FileLoader) Clear(key file.ID) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *FileLoader) unsafeSet(key file.ID, value file.File) {
if l.cache == nil {
l.cache = map[file.ID]file.File{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *fileLoaderBatch) keyIndex(l *FileLoader, key file.ID) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *fileLoaderBatch) startTimer(l *FileLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *fileLoaderBatch) end(l *FileLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,225 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/file"
)
// GalleryFileIDsLoaderConfig captures the config to create a new GalleryFileIDsLoader
type GalleryFileIDsLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([][]file.ID, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewGalleryFileIDsLoader creates a new GalleryFileIDsLoader given a fetch, wait, and maxBatch
func NewGalleryFileIDsLoader(config GalleryFileIDsLoaderConfig) *GalleryFileIDsLoader {
return &GalleryFileIDsLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// GalleryFileIDsLoader batches and caches requests
type GalleryFileIDsLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([][]file.ID, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int][]file.ID
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *galleryFileIDsLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type galleryFileIDsLoaderBatch struct {
keys []int
data [][]file.ID
error []error
closing bool
done chan struct{}
}
// Load a ID by key, batching and caching will be applied automatically
func (l *GalleryFileIDsLoader) Load(key int) ([]file.ID, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a ID.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *GalleryFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() ([]file.ID, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &galleryFileIDsLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() ([]file.ID, error) {
<-batch.done
var data []file.ID
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *GalleryFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
results := make([]func() ([]file.ID, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
iDs := make([][]file.ID, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
iDs[i], errors[i] = thunk()
}
return iDs, errors
}
// LoadAllThunk returns a function that when called will block waiting for a IDs.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *GalleryFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
results := make([]func() ([]file.ID, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([][]file.ID, []error) {
iDs := make([][]file.ID, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
iDs[i], errors[i] = thunk()
}
return iDs, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *GalleryFileIDsLoader) Prime(key int, value []file.ID) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := make([]file.ID, len(value))
copy(cpy, value)
l.unsafeSet(key, cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *GalleryFileIDsLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *GalleryFileIDsLoader) unsafeSet(key int, value []file.ID) {
if l.cache == nil {
l.cache = map[int][]file.ID{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *galleryFileIDsLoaderBatch) keyIndex(l *GalleryFileIDsLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *galleryFileIDsLoaderBatch) startTimer(l *GalleryFileIDsLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *galleryFileIDsLoaderBatch) end(l *GalleryFileIDsLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// GalleryLoaderConfig captures the config to create a new GalleryLoader
type GalleryLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Gallery, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewGalleryLoader creates a new GalleryLoader given a fetch, wait, and maxBatch
func NewGalleryLoader(config GalleryLoaderConfig) *GalleryLoader {
return &GalleryLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// GalleryLoader batches and caches requests
type GalleryLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Gallery, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Gallery
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *galleryLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type galleryLoaderBatch struct {
keys []int
data []*models.Gallery
error []error
closing bool
done chan struct{}
}
// Load a Gallery by key, batching and caching will be applied automatically
func (l *GalleryLoader) Load(key int) (*models.Gallery, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Gallery.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *GalleryLoader) LoadThunk(key int) func() (*models.Gallery, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Gallery, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &galleryLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Gallery, error) {
<-batch.done
var data *models.Gallery
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *GalleryLoader) LoadAll(keys []int) ([]*models.Gallery, []error) {
results := make([]func() (*models.Gallery, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
gallerys := make([]*models.Gallery, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
gallerys[i], errors[i] = thunk()
}
return gallerys, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Gallerys.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *GalleryLoader) LoadAllThunk(keys []int) func() ([]*models.Gallery, []error) {
results := make([]func() (*models.Gallery, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Gallery, []error) {
gallerys := make([]*models.Gallery, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
gallerys[i], errors[i] = thunk()
}
return gallerys, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *GalleryLoader) Prime(key int, value *models.Gallery) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *GalleryLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *GalleryLoader) unsafeSet(key int, value *models.Gallery) {
if l.cache == nil {
l.cache = map[int]*models.Gallery{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *galleryLoaderBatch) keyIndex(l *GalleryLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *galleryLoaderBatch) startTimer(l *GalleryLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *galleryLoaderBatch) end(l *GalleryLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,225 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/file"
)
// ImageFileIDsLoaderConfig captures the config to create a new ImageFileIDsLoader
type ImageFileIDsLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([][]file.ID, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewImageFileIDsLoader creates a new ImageFileIDsLoader given a fetch, wait, and maxBatch
func NewImageFileIDsLoader(config ImageFileIDsLoaderConfig) *ImageFileIDsLoader {
return &ImageFileIDsLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// ImageFileIDsLoader batches and caches requests
type ImageFileIDsLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([][]file.ID, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int][]file.ID
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *imageFileIDsLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type imageFileIDsLoaderBatch struct {
keys []int
data [][]file.ID
error []error
closing bool
done chan struct{}
}
// Load a ID by key, batching and caching will be applied automatically
func (l *ImageFileIDsLoader) Load(key int) ([]file.ID, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a ID.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *ImageFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() ([]file.ID, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &imageFileIDsLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() ([]file.ID, error) {
<-batch.done
var data []file.ID
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *ImageFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
results := make([]func() ([]file.ID, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
iDs := make([][]file.ID, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
iDs[i], errors[i] = thunk()
}
return iDs, errors
}
// LoadAllThunk returns a function that when called will block waiting for a IDs.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *ImageFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
results := make([]func() ([]file.ID, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([][]file.ID, []error) {
iDs := make([][]file.ID, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
iDs[i], errors[i] = thunk()
}
return iDs, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *ImageFileIDsLoader) Prime(key int, value []file.ID) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := make([]file.ID, len(value))
copy(cpy, value)
l.unsafeSet(key, cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *ImageFileIDsLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *ImageFileIDsLoader) unsafeSet(key int, value []file.ID) {
if l.cache == nil {
l.cache = map[int][]file.ID{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *imageFileIDsLoaderBatch) keyIndex(l *ImageFileIDsLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *imageFileIDsLoaderBatch) startTimer(l *ImageFileIDsLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *imageFileIDsLoaderBatch) end(l *ImageFileIDsLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// ImageLoaderConfig captures the config to create a new ImageLoader
type ImageLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Image, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewImageLoader creates a new ImageLoader given a fetch, wait, and maxBatch
func NewImageLoader(config ImageLoaderConfig) *ImageLoader {
return &ImageLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// ImageLoader batches and caches requests
type ImageLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Image, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Image
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *imageLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type imageLoaderBatch struct {
keys []int
data []*models.Image
error []error
closing bool
done chan struct{}
}
// Load a Image by key, batching and caching will be applied automatically
func (l *ImageLoader) Load(key int) (*models.Image, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Image.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *ImageLoader) LoadThunk(key int) func() (*models.Image, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Image, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &imageLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Image, error) {
<-batch.done
var data *models.Image
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *ImageLoader) LoadAll(keys []int) ([]*models.Image, []error) {
results := make([]func() (*models.Image, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
images := make([]*models.Image, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
images[i], errors[i] = thunk()
}
return images, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Images.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *ImageLoader) LoadAllThunk(keys []int) func() ([]*models.Image, []error) {
results := make([]func() (*models.Image, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Image, []error) {
images := make([]*models.Image, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
images[i], errors[i] = thunk()
}
return images, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *ImageLoader) Prime(key int, value *models.Image) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *ImageLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *ImageLoader) unsafeSet(key int, value *models.Image) {
if l.cache == nil {
l.cache = map[int]*models.Image{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *imageLoaderBatch) keyIndex(l *ImageLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *imageLoaderBatch) startTimer(l *ImageLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *imageLoaderBatch) end(l *ImageLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// MovieLoaderConfig captures the config to create a new MovieLoader
type MovieLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Movie, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewMovieLoader creates a new MovieLoader given a fetch, wait, and maxBatch
func NewMovieLoader(config MovieLoaderConfig) *MovieLoader {
return &MovieLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// MovieLoader batches and caches requests
type MovieLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Movie, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Movie
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *movieLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type movieLoaderBatch struct {
keys []int
data []*models.Movie
error []error
closing bool
done chan struct{}
}
// Load a Movie by key, batching and caching will be applied automatically
func (l *MovieLoader) Load(key int) (*models.Movie, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Movie.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *MovieLoader) LoadThunk(key int) func() (*models.Movie, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Movie, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &movieLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Movie, error) {
<-batch.done
var data *models.Movie
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *MovieLoader) LoadAll(keys []int) ([]*models.Movie, []error) {
results := make([]func() (*models.Movie, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
movies := make([]*models.Movie, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
movies[i], errors[i] = thunk()
}
return movies, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Movies.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *MovieLoader) LoadAllThunk(keys []int) func() ([]*models.Movie, []error) {
results := make([]func() (*models.Movie, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Movie, []error) {
movies := make([]*models.Movie, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
movies[i], errors[i] = thunk()
}
return movies, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *MovieLoader) Prime(key int, value *models.Movie) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *MovieLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *MovieLoader) unsafeSet(key int, value *models.Movie) {
if l.cache == nil {
l.cache = map[int]*models.Movie{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *movieLoaderBatch) keyIndex(l *MovieLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *movieLoaderBatch) startTimer(l *MovieLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *movieLoaderBatch) end(l *MovieLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// PerformerLoaderConfig captures the config to create a new PerformerLoader
type PerformerLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Performer, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewPerformerLoader creates a new PerformerLoader given a fetch, wait, and maxBatch
func NewPerformerLoader(config PerformerLoaderConfig) *PerformerLoader {
return &PerformerLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// PerformerLoader batches and caches requests
type PerformerLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Performer, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Performer
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *performerLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type performerLoaderBatch struct {
keys []int
data []*models.Performer
error []error
closing bool
done chan struct{}
}
// Load a Performer by key, batching and caching will be applied automatically
func (l *PerformerLoader) Load(key int) (*models.Performer, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Performer.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *PerformerLoader) LoadThunk(key int) func() (*models.Performer, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Performer, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &performerLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Performer, error) {
<-batch.done
var data *models.Performer
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *PerformerLoader) LoadAll(keys []int) ([]*models.Performer, []error) {
results := make([]func() (*models.Performer, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
performers := make([]*models.Performer, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
performers[i], errors[i] = thunk()
}
return performers, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Performers.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *PerformerLoader) LoadAllThunk(keys []int) func() ([]*models.Performer, []error) {
results := make([]func() (*models.Performer, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Performer, []error) {
performers := make([]*models.Performer, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
performers[i], errors[i] = thunk()
}
return performers, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *PerformerLoader) Prime(key int, value *models.Performer) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *PerformerLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *PerformerLoader) unsafeSet(key int, value *models.Performer) {
if l.cache == nil {
l.cache = map[int]*models.Performer{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *performerLoaderBatch) keyIndex(l *PerformerLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *performerLoaderBatch) startTimer(l *PerformerLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *performerLoaderBatch) end(l *PerformerLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,225 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/file"
)
// SceneFileIDsLoaderConfig captures the config to create a new SceneFileIDsLoader
type SceneFileIDsLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([][]file.ID, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewSceneFileIDsLoader creates a new SceneFileIDsLoader given a fetch, wait, and maxBatch
func NewSceneFileIDsLoader(config SceneFileIDsLoaderConfig) *SceneFileIDsLoader {
return &SceneFileIDsLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// SceneFileIDsLoader batches and caches requests
type SceneFileIDsLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([][]file.ID, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int][]file.ID
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *sceneFileIDsLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type sceneFileIDsLoaderBatch struct {
keys []int
data [][]file.ID
error []error
closing bool
done chan struct{}
}
// Load a ID by key, batching and caching will be applied automatically
func (l *SceneFileIDsLoader) Load(key int) ([]file.ID, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a ID.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *SceneFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() ([]file.ID, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &sceneFileIDsLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() ([]file.ID, error) {
<-batch.done
var data []file.ID
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *SceneFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
results := make([]func() ([]file.ID, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
iDs := make([][]file.ID, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
iDs[i], errors[i] = thunk()
}
return iDs, errors
}
// LoadAllThunk returns a function that when called will block waiting for a IDs.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *SceneFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
results := make([]func() ([]file.ID, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([][]file.ID, []error) {
iDs := make([][]file.ID, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
iDs[i], errors[i] = thunk()
}
return iDs, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *SceneFileIDsLoader) Prime(key int, value []file.ID) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := make([]file.ID, len(value))
copy(cpy, value)
l.unsafeSet(key, cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *SceneFileIDsLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *SceneFileIDsLoader) unsafeSet(key int, value []file.ID) {
if l.cache == nil {
l.cache = map[int][]file.ID{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *sceneFileIDsLoaderBatch) keyIndex(l *SceneFileIDsLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *sceneFileIDsLoaderBatch) startTimer(l *SceneFileIDsLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *sceneFileIDsLoaderBatch) end(l *SceneFileIDsLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// SceneLoaderConfig captures the config to create a new SceneLoader
type SceneLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Scene, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewSceneLoader creates a new SceneLoader given a fetch, wait, and maxBatch
func NewSceneLoader(config SceneLoaderConfig) *SceneLoader {
return &SceneLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// SceneLoader batches and caches requests
type SceneLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Scene, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Scene
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *sceneLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type sceneLoaderBatch struct {
keys []int
data []*models.Scene
error []error
closing bool
done chan struct{}
}
// Load a Scene by key, batching and caching will be applied automatically
func (l *SceneLoader) Load(key int) (*models.Scene, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Scene.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *SceneLoader) LoadThunk(key int) func() (*models.Scene, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Scene, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &sceneLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Scene, error) {
<-batch.done
var data *models.Scene
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *SceneLoader) LoadAll(keys []int) ([]*models.Scene, []error) {
results := make([]func() (*models.Scene, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
scenes := make([]*models.Scene, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
scenes[i], errors[i] = thunk()
}
return scenes, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Scenes.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *SceneLoader) LoadAllThunk(keys []int) func() ([]*models.Scene, []error) {
results := make([]func() (*models.Scene, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Scene, []error) {
scenes := make([]*models.Scene, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
scenes[i], errors[i] = thunk()
}
return scenes, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *SceneLoader) Prime(key int, value *models.Scene) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *SceneLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *SceneLoader) unsafeSet(key int, value *models.Scene) {
if l.cache == nil {
l.cache = map[int]*models.Scene{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *sceneLoaderBatch) keyIndex(l *SceneLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *sceneLoaderBatch) startTimer(l *SceneLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *sceneLoaderBatch) end(l *SceneLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// StudioLoaderConfig captures the config to create a new StudioLoader
type StudioLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Studio, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewStudioLoader creates a new StudioLoader given a fetch, wait, and maxBatch
func NewStudioLoader(config StudioLoaderConfig) *StudioLoader {
return &StudioLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// StudioLoader batches and caches requests
type StudioLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Studio, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Studio
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *studioLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type studioLoaderBatch struct {
keys []int
data []*models.Studio
error []error
closing bool
done chan struct{}
}
// Load a Studio by key, batching and caching will be applied automatically
func (l *StudioLoader) Load(key int) (*models.Studio, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Studio.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *StudioLoader) LoadThunk(key int) func() (*models.Studio, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Studio, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &studioLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Studio, error) {
<-batch.done
var data *models.Studio
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *StudioLoader) LoadAll(keys []int) ([]*models.Studio, []error) {
results := make([]func() (*models.Studio, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
studios := make([]*models.Studio, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
studios[i], errors[i] = thunk()
}
return studios, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Studios.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *StudioLoader) LoadAllThunk(keys []int) func() ([]*models.Studio, []error) {
results := make([]func() (*models.Studio, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Studio, []error) {
studios := make([]*models.Studio, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
studios[i], errors[i] = thunk()
}
return studios, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *StudioLoader) Prime(key int, value *models.Studio) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *StudioLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *StudioLoader) unsafeSet(key int, value *models.Studio) {
if l.cache == nil {
l.cache = map[int]*models.Studio{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *studioLoaderBatch) keyIndex(l *StudioLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *studioLoaderBatch) startTimer(l *StudioLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *studioLoaderBatch) end(l *StudioLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// TagLoaderConfig captures the config to create a new TagLoader
type TagLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Tag, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewTagLoader creates a new TagLoader given a fetch, wait, and maxBatch
func NewTagLoader(config TagLoaderConfig) *TagLoader {
return &TagLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// TagLoader batches and caches requests
type TagLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Tag, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Tag
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *tagLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type tagLoaderBatch struct {
keys []int
data []*models.Tag
error []error
closing bool
done chan struct{}
}
// Load a Tag by key, batching and caching will be applied automatically
func (l *TagLoader) Load(key int) (*models.Tag, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Tag.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *TagLoader) LoadThunk(key int) func() (*models.Tag, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Tag, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &tagLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Tag, error) {
<-batch.done
var data *models.Tag
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *TagLoader) LoadAll(keys []int) ([]*models.Tag, []error) {
results := make([]func() (*models.Tag, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
tags := make([]*models.Tag, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
tags[i], errors[i] = thunk()
}
return tags, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Tags.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *TagLoader) LoadAllThunk(keys []int) func() ([]*models.Tag, []error) {
results := make([]func() (*models.Tag, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Tag, []error) {
tags := make([]*models.Tag, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
tags[i], errors[i] = thunk()
}
return tags, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *TagLoader) Prime(key int, value *models.Tag) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *TagLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *TagLoader) unsafeSet(key int, value *models.Tag) {
if l.cache == nil {
l.cache = map[int]*models.Tag{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *tagLoaderBatch) keyIndex(l *TagLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *tagLoaderBatch) startTimer(l *TagLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *tagLoaderBatch) end(l *TagLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -23,6 +23,17 @@ var matcher = language.NewMatcher([]language.Tag{
language.MustParse("nl-NL"),
language.MustParse("ru-RU"),
language.MustParse("tr-TR"),
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

@@ -11,6 +11,7 @@ import (
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/scraper"
"github.com/stashapp/stash/pkg/txn"
)
var (
@@ -30,7 +31,12 @@ type hookExecutor interface {
}
type Resolver struct {
txnManager models.TransactionManager
txnManager txn.Manager
repository manager.Repository
sceneService manager.SceneService
imageService manager.ImageService
galleryService manager.GalleryService
hookExecutor hookExecutor
}
@@ -38,37 +44,37 @@ func (r *Resolver) scraperCache() *scraper.Cache {
return manager.GetInstance().ScraperCache
}
func (r *Resolver) Gallery() models.GalleryResolver {
func (r *Resolver) Gallery() GalleryResolver {
return &galleryResolver{r}
}
func (r *Resolver) Mutation() models.MutationResolver {
func (r *Resolver) Mutation() MutationResolver {
return &mutationResolver{r}
}
func (r *Resolver) Performer() models.PerformerResolver {
func (r *Resolver) Performer() PerformerResolver {
return &performerResolver{r}
}
func (r *Resolver) Query() models.QueryResolver {
func (r *Resolver) Query() QueryResolver {
return &queryResolver{r}
}
func (r *Resolver) Scene() models.SceneResolver {
func (r *Resolver) Scene() SceneResolver {
return &sceneResolver{r}
}
func (r *Resolver) Image() models.ImageResolver {
func (r *Resolver) Image() ImageResolver {
return &imageResolver{r}
}
func (r *Resolver) SceneMarker() models.SceneMarkerResolver {
func (r *Resolver) SceneMarker() SceneMarkerResolver {
return &sceneMarkerResolver{r}
}
func (r *Resolver) Studio() models.StudioResolver {
func (r *Resolver) Studio() StudioResolver {
return &studioResolver{r}
}
func (r *Resolver) Movie() models.MovieResolver {
func (r *Resolver) Movie() MovieResolver {
return &movieResolver{r}
}
func (r *Resolver) Subscription() models.SubscriptionResolver {
func (r *Resolver) Subscription() SubscriptionResolver {
return &subscriptionResolver{r}
}
func (r *Resolver) Tag() models.TagResolver {
func (r *Resolver) Tag() TagResolver {
return &tagResolver{r}
}
@@ -85,17 +91,17 @@ type studioResolver struct{ *Resolver }
type movieResolver struct{ *Resolver }
type tagResolver struct{ *Resolver }
func (r *Resolver) withTxn(ctx context.Context, fn func(r models.Repository) error) error {
return r.txnManager.WithTxn(ctx, fn)
func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithTxn(ctx, r.txnManager, fn)
}
func (r *Resolver) withReadTxn(ctx context.Context, fn func(r models.ReaderRepository) error) error {
return r.txnManager.WithReadTxn(ctx, 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.SceneMarker().Wall(q)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.Wall(ctx, q)
return err
}); err != nil {
return nil, err
@@ -104,8 +110,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Scene().Wall(q)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.Wall(ctx, q)
return err
}); err != nil {
return nil, err
@@ -115,8 +121,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.SceneMarker().GetMarkerStrings(q, sort)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.GetMarkerStrings(ctx, q, sort)
return err
}); err != nil {
return nil, err
@@ -125,28 +131,29 @@ func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *stri
return ret, nil
}
func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) {
var ret models.StatsResultType
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
scenesQB := repo.Scene()
imageQB := repo.Image()
galleryQB := repo.Gallery()
studiosQB := repo.Studio()
performersQB := repo.Performer()
moviesQB := repo.Movie()
tagsQB := repo.Tag()
scenesCount, _ := scenesQB.Count()
scenesSize, _ := scenesQB.Size()
scenesDuration, _ := scenesQB.Duration()
imageCount, _ := imageQB.Count()
imageSize, _ := imageQB.Size()
galleryCount, _ := galleryQB.Count()
performersCount, _ := performersQB.Count()
studiosCount, _ := studiosQB.Count()
moviesCount, _ := moviesQB.Count()
tagsCount, _ := tagsQB.Count()
func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
var ret StatsResultType
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
repo := r.repository
scenesQB := repo.Scene
imageQB := repo.Image
galleryQB := repo.Gallery
studiosQB := repo.Studio
performersQB := repo.Performer
moviesQB := repo.Movie
tagsQB := repo.Tag
scenesCount, _ := scenesQB.Count(ctx)
scenesSize, _ := scenesQB.Size(ctx)
scenesDuration, _ := scenesQB.Duration(ctx)
imageCount, _ := imageQB.Count(ctx)
imageSize, _ := imageQB.Size(ctx)
galleryCount, _ := galleryQB.Count(ctx)
performersCount, _ := performersQB.Count(ctx)
studiosCount, _ := studiosQB.Count(ctx)
moviesCount, _ := moviesQB.Count(ctx)
tagsCount, _ := tagsQB.Count(ctx)
ret = models.StatsResultType{
ret = StatsResultType{
SceneCount: scenesCount,
ScenesSize: scenesSize,
ScenesDuration: scenesDuration,
@@ -167,10 +174,10 @@ func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, err
return &ret, nil
}
func (r *queryResolver) Version(ctx context.Context) (*models.Version, error) {
func (r *queryResolver) Version(ctx context.Context) (*Version, error) {
version, hash, buildtime := GetVersion()
return &models.Version{
return &Version{
Version: &version,
Hash: hash,
BuildTime: buildtime,
@@ -178,7 +185,7 @@ func (r *queryResolver) Version(ctx context.Context) (*models.Version, error) {
}
// Latestversion returns the latest git shorthash commit.
func (r *queryResolver) Latestversion(ctx context.Context) (*models.ShortVersion, error) {
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)
@@ -186,37 +193,37 @@ func (r *queryResolver) Latestversion(ctx context.Context) (*models.ShortVersion
logger.Errorf("Error while retrieving latest hash: %s", err)
}
return &models.ShortVersion{
return &ShortVersion{
Shorthash: ver,
URL: url,
}, err
}
// Get scene marker tags which show up under the video.
func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([]*models.SceneMarkerTag, error) {
func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([]*SceneMarkerTag, error) {
sceneID, err := strconv.Atoi(scene_id)
if err != nil {
return nil, err
}
var keys []int
tags := make(map[int]*models.SceneMarkerTag)
tags := make(map[int]*SceneMarkerTag)
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
sceneMarkers, err := repo.SceneMarker().FindBySceneID(sceneID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneMarkers, err := r.repository.SceneMarker.FindBySceneID(ctx, sceneID)
if err != nil {
return err
}
tqb := repo.Tag()
tqb := r.repository.Tag
for _, sceneMarker := range sceneMarkers {
markerPrimaryTag, err := tqb.Find(sceneMarker.PrimaryTagID)
markerPrimaryTag, err := tqb.Find(ctx, sceneMarker.PrimaryTagID)
if err != nil {
return err
}
_, hasKey := tags[markerPrimaryTag.ID]
if !hasKey {
sceneMarkerTag := &models.SceneMarkerTag{Tag: markerPrimaryTag}
sceneMarkerTag := &SceneMarkerTag{Tag: markerPrimaryTag}
tags[markerPrimaryTag.ID] = sceneMarkerTag
keys = append(keys, markerPrimaryTag.ID)
}
@@ -235,10 +242,20 @@ func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([
return a.SceneMarkers[0].Seconds < b.SceneMarkers[0].Seconds
})
var result []*models.SceneMarkerTag
var result []*SceneMarkerTag
for _, key := range keys {
result = append(result, tags[key])
}
return result, nil
}
func firstError(errs []error) error {
for _, e := range errs {
if e != nil {
return e
}
}
return nil
}

View File

@@ -2,34 +2,135 @@ package api
import (
"context"
"strconv"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *galleryResolver) Path(ctx context.Context, obj *models.Gallery) (*string, error) {
if obj.Path.Valid {
return &obj.Path.String, nil
func (r *galleryResolver) getPrimaryFile(ctx context.Context, obj *models.Gallery) (file.File, error) {
if obj.PrimaryFileID != nil {
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
if err != nil {
return nil, err
}
return f, nil
}
return nil, nil
}
func (r *galleryResolver) Title(ctx context.Context, obj *models.Gallery) (*string, error) {
if obj.Title.Valid {
return &obj.Title.String, nil
func (r *galleryResolver) getFiles(ctx context.Context, obj *models.Gallery) ([]file.File, error) {
fileIDs, err := loaders.From(ctx).GalleryFiles.Load(obj.ID)
if err != nil {
return nil, err
}
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
return files, firstError(errs)
}
func (r *galleryResolver) Files(ctx context.Context, obj *models.Gallery) ([]*GalleryFile, error) {
files, err := r.getFiles(ctx, obj)
if err != nil {
return nil, err
}
ret := make([]*GalleryFile, len(files))
for i, f := range files {
base := f.Base()
ret[i] = &GalleryFile{
ID: strconv.Itoa(int(base.ID)),
Path: base.Path,
Basename: base.Basename,
ParentFolderID: strconv.Itoa(int(base.ParentFolderID)),
ModTime: base.ModTime,
Size: base.Size,
CreatedAt: base.CreatedAt,
UpdatedAt: base.UpdatedAt,
Fingerprints: resolveFingerprints(base),
}
if base.ZipFileID != nil {
zipFileID := strconv.Itoa(int(*base.ZipFileID))
ret[i].ZipFileID = &zipFileID
}
}
return ret, nil
}
func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*Folder, error) {
if obj.FolderID == nil {
return nil, nil
}
var ret *file.Folder
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Folder.Find(ctx, *obj.FolderID)
if err != nil {
return err
}
return err
}); err != nil {
return nil, err
}
if ret == nil {
return nil, nil
}
rr := &Folder{
ID: ret.ID.String(),
Path: ret.Path,
ModTime: ret.ModTime,
CreatedAt: ret.CreatedAt,
UpdatedAt: ret.UpdatedAt,
}
if ret.ParentFolderID != nil {
pfidStr := ret.ParentFolderID.String()
rr.ParentFolderID = &pfidStr
}
if ret.ZipFileID != nil {
zfidStr := ret.ZipFileID.String()
rr.ZipFileID = &zfidStr
}
return rr, nil
}
func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f != nil {
return &f.Base().ModTime, nil
}
return nil, nil
}
// Images is deprecated, slow and shouldn't be used
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret []*models.Image, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
// #2376 - sort images by path
// doing this via Query is really slow, so stick with FindByGalleryID
ret, err = repo.Image().FindByGalleryID(obj.ID)
ret, err = r.repository.Image.FindByGalleryID(ctx, obj.ID)
if err != nil {
return err
}
@@ -43,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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
// doing this via Query is really slow, so stick with FindByGalleryID
imgs, err := repo.Image().FindByGalleryID(obj.ID)
if err != nil {
return err
}
if len(imgs) > 0 {
ret = imgs[0]
}
for _, img := range imgs {
if image.IsCover(img) {
ret = img
break
}
}
return nil
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
}
@@ -70,91 +156,91 @@ func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *
}
func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*string, error) {
if obj.Date.Valid {
result := utils.GetYMDFromDatabaseDate(obj.Date.String)
if obj.Date != nil {
result := obj.Date.String()
return &result, nil
}
return nil, nil
}
func (r *galleryResolver) URL(ctx context.Context, obj *models.Gallery) (*string, error) {
if obj.URL.Valid {
return &obj.URL.String, nil
func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (string, error) {
if !obj.Files.PrimaryLoaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPrimaryFile(ctx, r.repository.File)
}); err != nil {
return "", err
}
}
return nil, nil
}
func (r *galleryResolver) Details(ctx context.Context, obj *models.Gallery) (*string, error) {
if obj.Details.Valid {
return &obj.Details.String, nil
}
return nil, nil
return obj.PrimaryChecksum(), nil
}
func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*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 *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 err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Scene().FindByGalleryID(obj.ID)
return err
}); err != nil {
return nil, err
if !obj.SceneIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadSceneIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).SceneByID.LoadAll(obj.SceneIDs.List())
return ret, firstError(errs)
}
func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret *models.Studio, err error) {
if !obj.StudioID.Valid {
if obj.StudioID == nil {
return nil, nil
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
}
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Tag().FindByGalleryID(obj.ID)
return err
}); err != nil {
return nil, err
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Gallery)
}); 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 *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Performer().FindByGalleryID(obj.ID)
return err
}); err != nil {
return nil, err
if !obj.PerformerIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPerformerIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
return ret, firstError(errs)
}
func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (ret int, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = repo.Image().CountByGalleryID(obj.ID)
ret, err = r.repository.Image.CountByGalleryID(ctx, obj.ID)
return err
}); err != nil {
return 0, err
@@ -162,15 +248,3 @@ func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (
return ret, nil
}
func (r *galleryResolver) CreatedAt(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *galleryResolver) UpdatedAt(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}
func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
return &obj.FileModTime.Timestamp, nil
}

View File

@@ -2,105 +2,192 @@ package api
import (
"context"
"fmt"
"strconv"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/models"
)
func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string, error) {
ret := image.GetTitle(obj)
return &ret, nil
}
func (r *imageResolver) getPrimaryFile(ctx context.Context, obj *models.Image) (*file.ImageFile, error) {
if obj.PrimaryFileID != nil {
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
if err != nil {
return nil, err
}
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
ret, ok := f.(*file.ImageFile)
if !ok {
return nil, fmt.Errorf("file %T is not an image file", f)
}
return ret, nil
}
return nil, nil
}
func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*models.ImageFileType, error) {
width := int(obj.Width.Int64)
height := int(obj.Height.Int64)
size := int(obj.Size.Int64)
return &models.ImageFileType{
Size: &size,
Width: &width,
Height: &height,
func (r *imageResolver) getFiles(ctx context.Context, obj *models.Image) ([]*file.ImageFile, error) {
fileIDs, err := loaders.From(ctx).ImageFiles.Load(obj.ID)
if err != nil {
return nil, err
}
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
ret := make([]*file.ImageFile, len(files))
for i, bf := range files {
f, ok := bf.(*file.ImageFile)
if !ok {
return nil, fmt.Errorf("file %T is not an image file", f)
}
ret[i] = f
}
return ret, firstError(errs)
}
func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string, error) {
ret := obj.GetTitle()
return &ret, nil
}
func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFileType, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f == nil {
return nil, nil
}
width := f.Width
height := f.Height
size := f.Size
return &ImageFileType{
Size: int(size),
Width: width,
Height: height,
}, nil
}
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*models.ImagePathsType, error) {
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) {
files, err := r.getFiles(ctx, obj)
if err != nil {
return nil, err
}
ret := make([]*ImageFile, len(files))
for i, f := range files {
ret[i] = &ImageFile{
ID: strconv.Itoa(int(f.ID)),
Path: f.Path,
Basename: f.Basename,
ParentFolderID: strconv.Itoa(int(f.ParentFolderID)),
ModTime: f.ModTime,
Size: f.Size,
Width: f.Width,
Height: f.Height,
CreatedAt: f.CreatedAt,
UpdatedAt: f.UpdatedAt,
Fingerprints: resolveFingerprints(f.Base()),
}
if f.ZipFileID != nil {
zipFileID := strconv.Itoa(int(*f.ZipFileID))
ret[i].ZipFileID = &zipFileID
}
}
return ret, nil
}
func (r *imageResolver) FileModTime(ctx context.Context, obj *models.Image) (*time.Time, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f != nil {
return &f.ModTime, nil
}
return nil, nil
}
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*ImagePathsType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewImageURLBuilder(baseURL, obj)
thumbnailPath := builder.GetThumbnailURL()
imagePath := builder.GetImageURL()
return &models.ImagePathsType{
return &ImagePathsType{
Image: &imagePath,
Thumbnail: &thumbnailPath,
}, nil
}
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret []*models.Gallery, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Gallery().FindByImageID(obj.ID)
return err
}); err != nil {
return nil, err
if !obj.GalleryIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadGalleryIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).GalleryByID.LoadAll(obj.GalleryIDs.List())
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.Valid {
if obj.StudioID == nil {
return nil, nil
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
}
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindByImageID(obj.ID)
return err
}); err != nil {
return nil, err
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Image)
}); 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 *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret []*models.Performer, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().FindByImageID(obj.ID)
return err
}); err != nil {
return nil, err
if !obj.PerformerIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPerformerIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err
}
}
return ret, nil
}
func (r *imageResolver) CreatedAt(ctx context.Context, obj *models.Image) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *imageResolver) UpdatedAt(ctx context.Context, obj *models.Image) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}
func (r *imageResolver) FileModTime(ctx context.Context, obj *models.Image) (*time.Time, error) {
return &obj.FileModTime.Timestamp, nil
var errs []error
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
return ret, firstError(errs)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
@@ -47,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
@@ -56,14 +65,7 @@ func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, er
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *models.Studio, err error) {
if obj.StudioID.Valid {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
return loaders.From(ctx).StudioByID.Load(int(obj.StudioID.Int64))
}
return nil, nil
@@ -92,9 +94,9 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
img, err = repo.Movie().GetBackImage(obj.ID)
img, err = r.repository.Movie.GetBackImage(ctx, obj.ID)
if err != nil {
return err
}
@@ -115,8 +117,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = repo.Scene().CountByMovieID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Scene.CountByMovieID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -126,9 +128,9 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = repo.Scene().FindByMovieID(obj.ID)
ret, err = r.repository.Scene.FindByMovieID(ctx, obj.ID)
return err
}); err != nil {
return nil, err

View File

@@ -2,7 +2,7 @@ package api
import (
"context"
"time"
"strconv"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/gallery"
@@ -10,131 +10,26 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *performerResolver) Name(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Name.Valid {
return &obj.Name.String, nil
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Height != nil {
ret := strconv.Itoa(*obj.Height)
return &ret, nil
}
return nil, nil
}
func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.URL.Valid {
return &obj.URL.String, nil
}
return nil, nil
}
func (r *performerResolver) Gender(ctx context.Context, obj *models.Performer) (*models.GenderEnum, error) {
var ret models.GenderEnum
if obj.Gender.Valid {
ret = models.GenderEnum(obj.Gender.String)
if ret.IsValid() {
return &ret, nil
}
}
return nil, nil
}
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Twitter.Valid {
return &obj.Twitter.String, nil
}
return nil, nil
}
func (r *performerResolver) Instagram(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Instagram.Valid {
return &obj.Instagram.String, nil
}
return nil, nil
func (r *performerResolver) HeightCm(ctx context.Context, obj *models.Performer) (*int, error) {
return obj.Height, nil
}
func (r *performerResolver) Birthdate(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Birthdate.Valid {
return &obj.Birthdate.String, nil
if obj.Birthdate != nil {
ret := obj.Birthdate.String()
return &ret, nil
}
return nil, nil
}
func (r *performerResolver) Ethnicity(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Ethnicity.Valid {
return &obj.Ethnicity.String, nil
}
return nil, nil
}
func (r *performerResolver) Country(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Country.Valid {
return &obj.Country.String, nil
}
return nil, nil
}
func (r *performerResolver) EyeColor(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.EyeColor.Valid {
return &obj.EyeColor.String, nil
}
return nil, nil
}
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Height.Valid {
return &obj.Height.String, nil
}
return nil, nil
}
func (r *performerResolver) Measurements(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Measurements.Valid {
return &obj.Measurements.String, nil
}
return nil, nil
}
func (r *performerResolver) FakeTits(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.FakeTits.Valid {
return &obj.FakeTits.String, nil
}
return nil, nil
}
func (r *performerResolver) CareerLength(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.CareerLength.Valid {
return &obj.CareerLength.String, nil
}
return nil, nil
}
func (r *performerResolver) Tattoos(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Tattoos.Valid {
return &obj.Tattoos.String, nil
}
return nil, nil
}
func (r *performerResolver) Piercings(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Piercings.Valid {
return &obj.Piercings.String, nil
}
return nil, nil
}
func (r *performerResolver) Aliases(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Aliases.Valid {
return &obj.Aliases.String, nil
}
return nil, nil
}
func (r *performerResolver) Favorite(ctx context.Context, obj *models.Performer) (bool, error) {
if obj.Favorite.Valid {
return obj.Favorite.Bool, nil
}
return false, nil
}
func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer) (*string, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
imagePath := urlbuilders.NewPerformerURLBuilder(baseURL, obj).GetPerformerImageURL()
@@ -142,8 +37,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindByPerformerID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -154,8 +49,8 @@ func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (re
func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = repo.Scene().CountByPerformerID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Scene.CountByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -166,8 +61,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = image.CountByPerformerID(repo.Image(), obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = image.CountByPerformerID(ctx, r.repository.Image, obj.ID)
return err
}); err != nil {
return nil, err
@@ -178,8 +73,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = gallery.CountByPerformerID(repo.Gallery(), obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = gallery.CountByPerformerID(ctx, r.repository.Gallery, obj.ID)
return err
}); err != nil {
return nil, err
@@ -189,8 +84,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Scene().FindByPerformerID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -199,65 +94,42 @@ func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (
return ret, nil
}
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) (ret []*models.StashID, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().GetStashIDs(obj.ID)
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) {
var ret []models.StashID
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Performer.GetStashIDs(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
return stashIDsSliceToPtrSlice(ret), 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Movie().FindByPerformerID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -268,8 +140,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = repo.Movie().CountByPerformerID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return nil, err

View File

@@ -2,105 +2,196 @@ package api
import (
"context"
"fmt"
"strconv"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *sceneResolver) Checksum(ctx context.Context, obj *models.Scene) (*string, error) {
if obj.Checksum.Valid {
return &obj.Checksum.String, nil
func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (*file.VideoFile, error) {
if obj.PrimaryFileID != nil {
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
if err != nil {
return nil, err
}
ret, ok := f.(*file.VideoFile)
if !ok {
return nil, fmt.Errorf("file %T is not an image file", f)
}
obj.Files.SetPrimary(ret)
return ret, nil
} else {
_ = obj.LoadPrimaryFile(ctx, r.repository.File)
}
return nil, nil
}
func (r *sceneResolver) Oshash(ctx context.Context, obj *models.Scene) (*string, error) {
if obj.OSHash.Valid {
return &obj.OSHash.String, nil
func (r *sceneResolver) getFiles(ctx context.Context, obj *models.Scene) ([]*file.VideoFile, error) {
fileIDs, err := loaders.From(ctx).SceneFiles.Load(obj.ID)
if err != nil {
return nil, err
}
return nil, nil
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
ret := make([]*file.VideoFile, len(files))
for i, bf := range files {
f, ok := bf.(*file.VideoFile)
if !ok {
return nil, fmt.Errorf("file %T is not a video file", f)
}
ret[i] = f
}
obj.Files.Set(ret)
return ret, firstError(errs)
}
func (r *sceneResolver) Title(ctx context.Context, obj *models.Scene) (*string, error) {
if obj.Title.Valid {
return &obj.Title.String, nil
func (r *sceneResolver) FileModTime(ctx context.Context, obj *models.Scene) (*time.Time, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
return nil, nil
}
func (r *sceneResolver) Details(ctx context.Context, obj *models.Scene) (*string, error) {
if obj.Details.Valid {
return &obj.Details.String, nil
}
return nil, nil
}
func (r *sceneResolver) URL(ctx context.Context, obj *models.Scene) (*string, error) {
if obj.URL.Valid {
return &obj.URL.String, nil
if f != nil {
return &f.ModTime, nil
}
return nil, nil
}
func (r *sceneResolver) Date(ctx context.Context, obj *models.Scene) (*string, error) {
if obj.Date.Valid {
result := utils.GetYMDFromDatabaseDate(obj.Date.String)
if obj.Date != nil {
result := obj.Date.String()
return &result, nil
}
return nil, nil
}
// File is deprecated
func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.SceneFileType, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f == nil {
return nil, nil
}
bitrate := int(f.BitRate)
size := strconv.FormatInt(f.Size, 10)
return &models.SceneFileType{
Size: &size,
Duration: handleFloat64(f.Duration),
VideoCodec: &f.VideoCodec,
AudioCodec: &f.AudioCodec,
Width: &f.Width,
Height: &f.Height,
Framerate: handleFloat64(f.FrameRate),
Bitrate: &bitrate,
}, nil
}
func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoFile, error) {
files, err := r.getFiles(ctx, obj)
if err != nil {
return nil, err
}
ret := make([]*VideoFile, len(files))
for i, f := range files {
ret[i] = &VideoFile{
ID: strconv.Itoa(int(f.ID)),
Path: f.Path,
Basename: f.Basename,
ParentFolderID: strconv.Itoa(int(f.ParentFolderID)),
ModTime: f.ModTime,
Format: f.Format,
Size: f.Size,
Duration: handleFloat64Value(f.Duration),
VideoCodec: f.VideoCodec,
AudioCodec: f.AudioCodec,
Width: f.Width,
Height: f.Height,
FrameRate: handleFloat64Value(f.FrameRate),
BitRate: int(f.BitRate),
CreatedAt: f.CreatedAt,
UpdatedAt: f.UpdatedAt,
Fingerprints: resolveFingerprints(f.Base()),
}
if f.ZipFileID != nil {
zipFileID := strconv.Itoa(int(*f.ZipFileID))
ret[i].ZipFileID = &zipFileID
}
}
return ret, nil
}
func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*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 *sceneResolver) InteractiveSpeed(ctx context.Context, obj *models.Scene) (*int, error) {
if obj.InteractiveSpeed.Valid {
interactive_speed := int(obj.InteractiveSpeed.Int64)
return &interactive_speed, 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))
for i, fp := range f.Fingerprints {
ret[i] = &Fingerprint{
Type: fp.Type,
Value: formatFingerprint(fp.Fingerprint),
}
}
return nil, nil
return ret
}
func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.SceneFileType, error) {
width := int(obj.Width.Int64)
height := int(obj.Height.Int64)
bitrate := int(obj.Bitrate.Int64)
return &models.SceneFileType{
Size: &obj.Size.String,
Duration: handleFloat64(obj.Duration.Float64),
VideoCodec: &obj.VideoCodec.String,
AudioCodec: &obj.AudioCodec.String,
Width: &width,
Height: &height,
Framerate: handleFloat64(obj.Framerate.Float64),
Bitrate: &bitrate,
}, nil
func formatFingerprint(fp interface{}) string {
switch v := fp.(type) {
case int64:
return strconv.FormatUint(uint64(v), 16)
default:
return fmt.Sprintf("%v", fp)
}
}
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.ScenePathsType, error) {
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePathsType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
config := manager.GetInstance().Config
builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
builder.APIKey = config.GetAPIKey()
screenshotPath := builder.GetScreenshotURL(obj.UpdatedAt.Timestamp)
screenshotPath := builder.GetScreenshotURL(obj.UpdatedAt)
previewPath := builder.GetStreamPreviewURL()
streamPath := builder.GetStreamURL()
streamPath := builder.GetStreamURL().String()
webpPath := builder.GetStreamPreviewImageURL()
vttPath := builder.GetSpriteVTTURL()
spritePath := builder.GetSpriteURL()
chaptersVttPath := builder.GetChaptersVTTURL()
funscriptPath := builder.GetFunscriptURL()
captionBasePath := builder.GetCaptionURL()
interactiveHeatmap := builder.GetInteractiveHeatmapURL()
return &models.ScenePathsType{
return &ScenePathsType{
Screenshot: &screenshotPath,
Preview: &previewPath,
Stream: &streamPath,
@@ -110,12 +201,13 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.S
Sprite: &spritePath,
Funscript: &funscriptPath,
InteractiveHeatmap: &interactiveHeatmap,
Caption: &captionBasePath,
}, nil
}
func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (ret []*models.SceneMarker, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.SceneMarker().FindBySceneID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.FindBySceneID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -124,126 +216,186 @@ func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (re
return ret, nil
}
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Gallery().FindBySceneID(obj.ID)
return err
}); err != nil {
func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []*models.VideoCaption, err error) {
primaryFile, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
return ret, nil
}
func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *models.Studio, err error) {
if !obj.StudioID.Valid {
if primaryFile == nil {
return nil, nil
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.File.GetCaptions(ctx, primaryFile.Base().ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
return ret, err
}
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*models.SceneMovie, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Scene()
mqb := repo.Movie()
sceneMovies, err := qb.GetMovies(obj.ID)
if err != nil {
return err
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
if !obj.GalleryIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadGalleryIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
}
for _, sm := range sceneMovies {
movie, err := mqb.Find(sm.MovieID)
if err != nil {
return err
}
sceneIdx := sm.SceneIndex
sceneMovie := &models.SceneMovie{
Movie: movie,
}
if sceneIdx.Valid {
idx := int(sceneIdx.Int64)
sceneMovie.SceneIndex = &idx
}
ret = append(ret, sceneMovie)
}
return nil
}); err != nil {
return nil, err
}
var errs []error
ret, errs = loaders.From(ctx).GalleryByID.LoadAll(obj.GalleryIDs.List())
return ret, firstError(errs)
}
func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *models.Studio, err error) {
if obj.StudioID == nil {
return nil, nil
}
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
}
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) {
if !obj.Movies.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
return obj.LoadMovies(ctx, qb)
}); err != nil {
return nil, err
}
}
loader := loaders.From(ctx).MovieByID
for _, sm := range obj.Movies.List() {
movie, err := loader.Load(sm.MovieID)
if err != nil {
return nil, err
}
sceneIdx := sm.SceneIndex
sceneMovie := &SceneMovie{
Movie: movie,
SceneIndex: sceneIdx,
}
ret = append(ret, sceneMovie)
}
return ret, nil
}
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindBySceneID(obj.ID)
return err
}); err != nil {
return nil, err
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Scene)
}); 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 *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().FindBySceneID(obj.ID)
return err
}); err != nil {
return nil, err
if !obj.PerformerIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPerformerIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
return ret, firstError(errs)
}
func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID {
ret := make([]*models.StashID, len(v))
for i, vv := range v {
c := vv
ret[i] = &c
}
return ret
}
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Scene().GetStashIDs(obj.ID)
return err
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadStashIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
}
return ret, nil
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
}
func (r *sceneResolver) Phash(ctx context.Context, obj *models.Scene) (*string, error) {
if obj.Phash.Valid {
hexval := utils.PhashToString(obj.Phash.Int64)
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f == nil {
return nil, nil
}
val := f.Fingerprints.Get(file.FingerprintTypePhash)
if val == nil {
return nil, nil
}
phash, _ := val.(int64)
if phash != 0 {
hexval := utils.PhashToString(phash)
return &hexval, nil
}
return nil, nil
}
func (r *sceneResolver) CreatedAt(ctx context.Context, obj *models.Scene) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *sceneResolver) SceneStreams(ctx context.Context, obj *models.Scene) ([]*manager.SceneStreamEndpoint, error) {
// load the primary file into the scene
_, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
func (r *sceneResolver) UpdatedAt(ctx context.Context, obj *models.Scene) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}
func (r *sceneResolver) FileModTime(ctx context.Context, obj *models.Scene) (*time.Time, error) {
return &obj.FileModTime.Timestamp, nil
}
func (r *sceneResolver) SceneStreams(ctx context.Context, obj *models.Scene) ([]*models.SceneStreamEndpoint, error) {
config := manager.GetInstance().Config
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
builder.APIKey = config.GetAPIKey()
return manager.GetSceneStreamPaths(obj, builder.GetStreamURL(), config.GetMaxStreamingTranscodeSize())
}
func (r *sceneResolver) Interactive(ctx context.Context, obj *models.Scene) (bool, error) {
primaryFile, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return false, err
}
if primaryFile == nil {
return false, nil
}
return primaryFile.Interactive, nil
}
func (r *sceneResolver) InteractiveSpeed(ctx context.Context, obj *models.Scene) (*int, error) {
primaryFile, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if primaryFile == nil {
return nil, nil
}
return primaryFile.InteractiveSpeed, nil
}

View File

@@ -13,9 +13,9 @@ func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker
panic("Invalid scene id")
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneID := int(obj.SceneID.Int64)
ret, err = repo.Scene().Find(sceneID)
ret, err = r.repository.Scene.Find(ctx, sceneID)
return err
}); err != nil {
return nil, err
@@ -25,8 +25,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().Find(obj.PrimaryTagID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.Find(ctx, obj.PrimaryTagID)
return err
}); err != nil {
return nil, err
@@ -36,8 +36,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindBySceneMarkerID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindBySceneMarkerID(ctx, obj.ID)
return err
}); err != nil {
return nil, err

View File

@@ -4,6 +4,7 @@ import (
"context"
"time"
"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"
@@ -29,9 +30,9 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj).GetStudioImageURL()
var hasImage bool
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
hasImage, err = repo.Studio().HasImage(obj.ID)
hasImage, err = r.repository.Studio.HasImage(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -46,8 +47,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().GetAliases(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.GetAliases(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -58,8 +59,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = repo.Scene().CountByStudioID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Scene.CountByStudioID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -70,8 +71,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = image.CountByStudioID(repo.Image(), obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = image.CountByStudioID(ctx, r.repository.Image, obj.ID)
return err
}); err != nil {
return nil, err
@@ -82,8 +83,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = gallery.CountByStudioID(repo.Gallery(), obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = gallery.CountByStudioID(ctx, r.repository.Gallery, obj.ID)
return err
}); err != nil {
return nil, err
@@ -97,19 +98,12 @@ func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (
return nil, nil
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().Find(int(obj.ParentID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
return loaders.From(ctx).StudioByID.Load(int(obj.ParentID.Int64))
}
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (ret []*models.Studio, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().FindChildren(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.FindChildren(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -118,18 +112,28 @@ func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (
return ret, nil
}
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) (ret []*models.StashID, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().GetStashIDs(obj.ID)
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*models.StashID, error) {
var ret []models.StashID
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Studio.GetStashIDs(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
return stashIDsSliceToPtrSlice(ret), nil
}
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
@@ -153,8 +157,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Movie().FindByStudioID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -165,8 +169,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = repo.Movie().CountByStudioID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = r.repository.Movie.CountByStudioID(ctx, obj.ID)
return err
}); err != nil {
return nil, err

View File

@@ -10,9 +10,16 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *tagResolver) Description(ctx context.Context, obj *models.Tag) (*string, error) {
if obj.Description.Valid {
return &obj.Description.String, nil
}
return nil, nil
}
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindByChildTagID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByChildTagID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -22,8 +29,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindByParentTagID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByParentTagID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -33,8 +40,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().GetAliases(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.GetAliases(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -45,8 +52,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
count, err = repo.Scene().CountByTagID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
count, err = r.repository.Scene.CountByTagID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -57,8 +64,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
count, err = repo.SceneMarker().CountByTagID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
count, err = r.repository.SceneMarker.CountByTagID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -69,8 +76,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = image.CountByTagID(repo.Image(), obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = image.CountByTagID(ctx, r.repository.Image, obj.ID)
return err
}); err != nil {
return nil, err
@@ -81,8 +88,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = gallery.CountByTagID(repo.Gallery(), obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res, err = gallery.CountByTagID(ctx, r.repository.Gallery, obj.ID)
return err
}); err != nil {
return nil, err
@@ -93,8 +100,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
count, err = repo.Performer().CountByTagID(obj.ID)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
count, err = r.repository.Performer.CountByTagID(ctx, obj.ID)
return err
}); err != nil {
return nil, err

View File

@@ -15,21 +15,21 @@ import (
var ErrOverriddenConfig = errors.New("cannot set overridden value")
func (r *mutationResolver) Setup(ctx context.Context, input models.SetupInput) (bool, error) {
func (r *mutationResolver) Setup(ctx context.Context, input manager.SetupInput) (bool, error) {
err := manager.GetInstance().Setup(ctx, input)
return err == nil, err
}
func (r *mutationResolver) Migrate(ctx context.Context, input models.MigrateInput) (bool, error) {
func (r *mutationResolver) Migrate(ctx context.Context, input manager.MigrateInput) (bool, error) {
err := manager.GetInstance().Migrate(ctx, input)
return err == nil, err
}
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.ConfigGeneralInput) (*models.ConfigGeneralResult, error) {
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGeneralInput) (*ConfigGeneralResult, error) {
c := config.GetInstance()
existingPaths := c.GetStashPaths()
if len(input.Stashes) > 0 {
if input.Stashes != nil {
for _, s := range input.Stashes {
// Only validate existence of new paths
isNew := true
@@ -58,7 +58,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
}
validateDir := func(key string, value string, optional bool) error {
if err := checkConfigOverride(config.Metadata); err != nil {
if err := checkConfigOverride(key); err != nil {
return err
}
@@ -84,6 +84,15 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
c.Set(config.Database, input.DatabasePath)
}
existingBackupDirectoryPath := c.GetBackupDirectoryPath()
if input.BackupDirectoryPath != nil && existingBackupDirectoryPath != *input.BackupDirectoryPath {
if err := validateDir(config.BackupDirectoryPath, *input.BackupDirectoryPath, true); err != nil {
return makeConfigGeneralResult(), err
}
c.Set(config.BackupDirectoryPath, input.BackupDirectoryPath)
}
existingGeneratedPath := c.GetGeneratedPath()
if input.GeneratedPath != nil && existingGeneratedPath != *input.GeneratedPath {
if err := validateDir(config.Generated, *input.GeneratedPath, false); err != nil {
@@ -132,7 +141,9 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
}
// validate changing VideoFileNamingAlgorithm
if err := manager.ValidateVideoFileNamingAlgorithm(r.txnManager, *input.VideoFileNamingAlgorithm); err != nil {
if err := r.withTxn(context.TODO(), func(ctx context.Context) error {
return manager.ValidateVideoFileNamingAlgorithm(ctx, r.repository.Scene, *input.VideoFileNamingAlgorithm)
}); err != nil {
return makeConfigGeneralResult(), err
}
@@ -281,7 +292,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
return makeConfigGeneralResult(), nil
}
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) {
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) {
c := config.GetInstance()
setBool := func(key string, v *bool) {
@@ -338,10 +349,14 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
c.Set(config.ImageLightboxSlideshowDelay, *options.SlideshowDelay)
}
setString(config.ImageLightboxDisplayMode, (*string)(options.DisplayMode))
setString(config.ImageLightboxDisplayModeKey, (*string)(options.DisplayMode))
setBool(config.ImageLightboxScaleUp, options.ScaleUp)
setBool(config.ImageLightboxResetZoomOnNav, options.ResetZoomOnNav)
setString(config.ImageLightboxScrollMode, (*string)(options.ScrollMode))
setString(config.ImageLightboxScrollModeKey, (*string)(options.ScrollMode))
if options.ScrollAttemptsBeforeChange != nil {
c.Set(config.ImageLightboxScrollAttemptsBeforeChange, *options.ScrollAttemptsBeforeChange)
}
}
if input.CSS != nil {
@@ -350,6 +365,18 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
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)
}
setBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
if input.DisableDropdownCreate != nil {
ddc := input.DisableDropdownCreate
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
@@ -372,7 +399,7 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
return makeConfigInterfaceResult(), nil
}
func (r *mutationResolver) ConfigureDlna(ctx context.Context, input models.ConfigDLNAInput) (*models.ConfigDLNAResult, error) {
func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAInput) (*ConfigDLNAResult, error) {
c := config.GetInstance()
if input.ServerName != nil {
@@ -409,7 +436,7 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input models.Confi
return makeConfigDLNAResult(), nil
}
func (r *mutationResolver) ConfigureScraping(ctx context.Context, input models.ConfigScrapingInput) (*models.ConfigScrapingResult, error) {
func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigScrapingInput) (*ConfigScrapingResult, error) {
c := config.GetInstance()
refreshScraperCache := false
@@ -441,7 +468,7 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input models.C
return makeConfigScrapingResult(), nil
}
func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.ConfigDefaultSettingsInput) (*models.ConfigDefaultSettingsResult, error) {
func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input ConfigDefaultSettingsInput) (*ConfigDefaultSettingsResult, error) {
c := config.GetInstance()
if input.Identify != nil {
@@ -449,7 +476,7 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.C
}
if input.Scan != nil {
c.Set(config.DefaultScanSettings, input.Scan)
c.Set(config.DefaultScanSettings, input.Scan.ScanMetadataOptions)
}
if input.AutoTag != nil {
@@ -475,7 +502,7 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.C
return makeConfigDefaultsResult(), nil
}
func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input models.GenerateAPIKeyInput) (string, error) {
func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input GenerateAPIKeyInput) (string, error) {
c := config.GetInstance()
var newAPIKey string
@@ -497,3 +524,23 @@ func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input models.Gene
return newAPIKey, nil
}
func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) {
c := config.GetInstance()
c.SetUIConfiguration(input)
if err := c.Write(); err != nil {
return c.GetUIConfiguration(), err
}
return c.GetUIConfiguration(), nil
}
func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, value interface{}) (map[string]interface{}, error) {
c := config.GetInstance()
cfg := c.GetUIConfiguration()
cfg[key] = value
return r.ConfigureUI(ctx, cfg)
}

View File

@@ -5,10 +5,9 @@ import (
"time"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/models"
)
func (r *mutationResolver) EnableDlna(ctx context.Context, input models.EnableDLNAInput) (bool, error) {
func (r *mutationResolver) EnableDlna(ctx context.Context, input EnableDLNAInput) (bool, error) {
err := manager.GetInstance().DLNAService.Start(parseMinutes(input.Duration))
if err != nil {
return false, err
@@ -16,17 +15,17 @@ func (r *mutationResolver) EnableDlna(ctx context.Context, input models.EnableDL
return true, nil
}
func (r *mutationResolver) DisableDlna(ctx context.Context, input models.DisableDLNAInput) (bool, error) {
func (r *mutationResolver) DisableDlna(ctx context.Context, input DisableDLNAInput) (bool, error) {
manager.GetInstance().DLNAService.Stop(parseMinutes(input.Duration))
return true, nil
}
func (r *mutationResolver) AddTempDlnaip(ctx context.Context, input models.AddTempDLNAIPInput) (bool, error) {
func (r *mutationResolver) AddTempDlnaip(ctx context.Context, input AddTempDLNAIPInput) (bool, error) {
manager.GetInstance().DLNAService.AddTempDLNAIP(input.Address, parseMinutes(input.Duration))
return true, nil
}
func (r *mutationResolver) RemoveTempDlnaip(ctx context.Context, input models.RemoveTempDLNAIPInput) (bool, error) {
func (r *mutationResolver) RemoveTempDlnaip(ctx context.Context, input RemoveTempDLNAIPInput) (bool, error) {
ret := manager.GetInstance().DLNAService.RemoveTempDLNAIP(input.Address)
return ret, nil
}

View File

@@ -0,0 +1,74 @@
package api
import (
"context"
"fmt"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret bool, err error) {
fileIDs, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {
return false, err
}
fileDeleter := file.NewDeleter()
destroyer := &file.ZipDestroyer{
FileDestroyer: r.repository.File,
FolderDestroyer: r.repository.Folder,
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.File
for _, fileIDInt := range fileIDs {
fileID := file.ID(fileIDInt)
f, err := qb.Find(ctx, fileID)
if err != nil {
return err
}
path := f[0].Base().Path
// ensure not a primary file
isPrimary, err := qb.IsPrimary(ctx, fileID)
if err != nil {
return fmt.Errorf("checking if file %s is primary: %w", path, err)
}
if isPrimary {
return fmt.Errorf("cannot delete primary file %s", path)
}
// destroy files in zip file
inZip, err := qb.FindByZipFileID(ctx, fileID)
if err != nil {
return fmt.Errorf("finding zip file contents for %s: %w", path, err)
}
for _, ff := range inZip {
const deleteFileInZip = false
if err := file.Destroy(ctx, qb, ff, fileDeleter, deleteFileInZip); err != nil {
return fmt.Errorf("destroying file %s: %w", ff.Base().Path, err)
}
}
const deleteFile = true
if err := destroyer.DestroyZip(ctx, f[0], fileDeleter, deleteFile); err != nil {
return fmt.Errorf("deleting file %s: %w", path, err)
}
}
return nil
}); err != nil {
fileDeleter.Rollback()
return false, err
}
// perform the post-commit actions
fileDeleter.Commit()
return true, nil
}

View File

@@ -2,7 +2,6 @@ package api
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
@@ -11,18 +10,16 @@ import (
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/hash/md5"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/utils"
)
func (r *mutationResolver) getGallery(ctx context.Context, id int) (ret *models.Gallery, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Gallery().Find(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Gallery.Find(ctx, id)
return err
}); err != nil {
return nil, err
@@ -31,75 +28,63 @@ func (r *mutationResolver) getGallery(ctx context.Context, id int) (ret *models.
return ret, nil
}
func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.GalleryCreateInput) (*models.Gallery, error) {
func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreateInput) (*models.Gallery, error) {
// name must be provided
if input.Title == "" {
return nil, errors.New("title must not be empty")
}
// for manually created galleries, generate checksum from title
checksum := md5.FromString(input.Title)
// Populate a new performer from the input
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)
}
sceneIDs, err := stringslice.StringSliceToIntSlice(input.SceneIds)
if err != nil {
return nil, fmt.Errorf("converting scene ids: %w", err)
}
currentTime := time.Now()
newGallery := models.Gallery{
Title: sql.NullString{
String: input.Title,
Valid: true,
},
Checksum: checksum,
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
Title: input.Title,
PerformerIDs: models.NewRelatedIDs(performerIDs),
TagIDs: models.NewRelatedIDs(tagIDs),
SceneIDs: models.NewRelatedIDs(sceneIDs),
CreatedAt: currentTime,
UpdatedAt: currentTime,
}
if input.URL != nil {
newGallery.URL = sql.NullString{String: *input.URL, Valid: true}
newGallery.URL = *input.URL
}
if input.Details != nil {
newGallery.Details = sql.NullString{String: *input.Details, Valid: true}
}
if input.URL != nil {
newGallery.URL = sql.NullString{String: *input.URL, Valid: true}
newGallery.Details = *input.Details
}
if input.Date != nil {
newGallery.Date = models.SQLiteDate{String: *input.Date, Valid: true}
d := models.NewDate(*input.Date)
newGallery.Date = &d
}
if input.Rating != nil {
newGallery.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
// rating must be nullable
newGallery.Rating = sql.NullInt64{Valid: false}
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.ParseInt(*input.StudioID, 10, 64)
newGallery.StudioID = sql.NullInt64{Int64: studioID, Valid: true}
} else {
// studio must be nullable
newGallery.StudioID = sql.NullInt64{Valid: false}
studioID, _ := strconv.Atoi(*input.StudioID)
newGallery.StudioID = &studioID
}
// Start the transaction and save the gallery
var gallery *models.Gallery
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Gallery()
var err error
gallery, err = qb.Create(newGallery)
if err != nil {
return err
}
// Save the performers
if err := r.updateGalleryPerformers(qb, gallery.ID, input.PerformerIds); err != nil {
return err
}
// Save the tags
if err := r.updateGalleryTags(qb, gallery.ID, input.TagIds); err != nil {
return err
}
// Save the scenes
if err := r.updateGalleryScenes(qb, gallery.ID, input.SceneIds); err != nil {
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
if err := qb.Create(ctx, &newGallery, nil); err != nil {
return err
}
@@ -108,32 +93,12 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.Galle
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryCreatePost, input, nil)
return r.getGallery(ctx, gallery.ID)
r.hookExecutor.ExecutePostHooks(ctx, newGallery.ID, plugin.GalleryCreatePost, input, nil)
return r.getGallery(ctx, newGallery.ID)
}
func (r *mutationResolver) updateGalleryPerformers(qb models.GalleryReaderWriter, galleryID int, performerIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(performerIDs)
if err != nil {
return err
}
return qb.UpdatePerformers(galleryID, ids)
}
func (r *mutationResolver) updateGalleryTags(qb models.GalleryReaderWriter, galleryID int, tagIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(tagIDs)
if err != nil {
return err
}
return qb.UpdateTags(galleryID, ids)
}
func (r *mutationResolver) updateGalleryScenes(qb models.GalleryReaderWriter, galleryID int, sceneIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(sceneIDs)
if err != nil {
return err
}
return qb.UpdateScenes(galleryID, ids)
type GallerySceneUpdater interface {
UpdateScenes(ctx context.Context, galleryID int, sceneIDs []int) error
}
func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.GalleryUpdateInput) (ret *models.Gallery, err error) {
@@ -142,8 +107,8 @@ func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.Galle
}
// Start the transaction and save the gallery
if err := r.withTxn(ctx, func(repo models.Repository) error {
ret, err = r.galleryUpdate(input, translator, repo)
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.galleryUpdate(ctx, input, translator)
return err
}); err != nil {
return nil, err
@@ -158,13 +123,13 @@ func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.
inputMaps := getUpdateInputMaps(ctx)
// Start the transaction and save the gallery
if err := r.withTxn(ctx, func(repo models.Repository) error {
if err := r.withTxn(ctx, func(ctx context.Context) error {
for i, gallery := range input {
translator := changesetTranslator{
inputMap: inputMaps[i],
}
thisGallery, err := r.galleryUpdate(*gallery, translator, repo)
thisGallery, err := r.galleryUpdate(ctx, *gallery, translator)
if err != nil {
return err
}
@@ -196,8 +161,8 @@ func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.
return newRet, nil
}
func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Gallery, error) {
qb := repo.Gallery()
func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.GalleryUpdateInput, translator changesetTranslator) (*models.Gallery, error) {
qb := r.repository.Gallery
// Populate gallery from the input
galleryID, err := strconv.Atoi(input.ID)
@@ -205,7 +170,7 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, transl
return nil, err
}
originalGallery, err := qb.Find(galleryID)
originalGallery, err := qb.Find(ctx, galleryID)
if err != nil {
return nil, err
}
@@ -214,136 +179,139 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, transl
return nil, errors.New("not found")
}
updatedTime := time.Now()
updatedGallery := models.GalleryPartial{
ID: galleryID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
updatedGallery := models.NewGalleryPartial()
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")
}
// if gallery is not zip-based, then generate the checksum from the title
if !originalGallery.Path.Valid {
checksum := md5.FromString(*input.Title)
updatedGallery.Checksum = &checksum
}
updatedGallery.Title = &sql.NullString{String: *input.Title, Valid: true}
updatedGallery.Title = models.NewOptionalString(*input.Title)
}
updatedGallery.Details = translator.nullString(input.Details, "details")
updatedGallery.URL = translator.nullString(input.URL, "url")
updatedGallery.Date = translator.sqliteDate(input.Date, "date")
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedGallery.Organized = input.Organized
updatedGallery.Details = translator.optionalString(input.Details, "details")
updatedGallery.URL = translator.optionalString(input.URL, "url")
updatedGallery.Date = translator.optionalDate(input.Date, "date")
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
if input.PrimaryFileID != nil {
primaryFileID, err := strconv.Atoi(*input.PrimaryFileID)
if err != nil {
return nil, fmt.Errorf("converting primary file id: %w", err)
}
converted := file.ID(primaryFileID)
updatedGallery.PrimaryFileID = &converted
if err := originalGallery.LoadFiles(ctx, r.repository.Gallery); err != nil {
return nil, err
}
// ensure that new primary file is associated with scene
var f file.File
for _, ff := range originalGallery.Files.List() {
if ff.Base().ID == converted {
f = ff
}
}
if f == nil {
return nil, fmt.Errorf("file with id %d not associated with gallery", converted)
}
}
if translator.hasField("performer_ids") {
updatedGallery.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet)
if err != nil {
return nil, fmt.Errorf("converting performer ids: %w", err)
}
}
if translator.hasField("tag_ids") {
updatedGallery.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
}
if translator.hasField("scene_ids") {
updatedGallery.SceneIDs, err = translateUpdateIDs(input.SceneIds, models.RelationshipUpdateModeSet)
if err != nil {
return nil, fmt.Errorf("converting scene ids: %w", err)
}
}
// gallery scene is set from the scene only
gallery, err := qb.UpdatePartial(updatedGallery)
gallery, err := qb.UpdatePartial(ctx, galleryID, updatedGallery)
if err != nil {
return nil, err
}
// Save the performers
if translator.hasField("performer_ids") {
if err := r.updateGalleryPerformers(qb, galleryID, input.PerformerIds); err != nil {
return nil, err
}
}
// Save the tags
if translator.hasField("tag_ids") {
if err := r.updateGalleryTags(qb, galleryID, input.TagIds); err != nil {
return nil, err
}
}
// Save the scenes
if translator.hasField("scene_ids") {
if err := r.updateGalleryScenes(qb, galleryID, input.SceneIds); err != nil {
return nil, err
}
}
return gallery, nil
}
func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.BulkGalleryUpdateInput) ([]*models.Gallery, error) {
func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGalleryUpdateInput) ([]*models.Gallery, error) {
// Populate gallery from the input
updatedTime := time.Now()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedGallery := models.GalleryPartial{
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
updatedGallery := models.NewGalleryPartial()
updatedGallery.Details = translator.optionalString(input.Details, "details")
updatedGallery.URL = translator.optionalString(input.URL, "url")
updatedGallery.Date = translator.optionalDate(input.Date, "date")
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
var err error
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
if translator.hasField("performer_ids") {
updatedGallery.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode)
if err != nil {
return nil, fmt.Errorf("converting performer ids: %w", err)
}
}
updatedGallery.Details = translator.nullString(input.Details, "details")
updatedGallery.URL = translator.nullString(input.URL, "url")
updatedGallery.Date = translator.sqliteDate(input.Date, "date")
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedGallery.Organized = input.Organized
if translator.hasField("tag_ids") {
updatedGallery.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
}
if translator.hasField("scene_ids") {
updatedGallery.SceneIDs, err = translateUpdateIDs(input.SceneIds.Ids, input.SceneIds.Mode)
if err != nil {
return nil, fmt.Errorf("converting scene ids: %w", err)
}
}
ret := []*models.Gallery{}
// Start the transaction and save the galleries
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Gallery()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
for _, galleryIDStr := range input.Ids {
galleryID, _ := strconv.Atoi(galleryIDStr)
updatedGallery.ID = galleryID
gallery, err := qb.UpdatePartial(updatedGallery)
gallery, err := qb.UpdatePartial(ctx, galleryID, updatedGallery)
if err != nil {
return err
}
ret = append(ret, gallery)
// Save the performers
if translator.hasField("performer_ids") {
performerIDs, err := adjustGalleryPerformerIDs(qb, galleryID, *input.PerformerIds)
if err != nil {
return err
}
if err := qb.UpdatePerformers(galleryID, performerIDs); err != nil {
return err
}
}
// Save the tags
if translator.hasField("tag_ids") {
tagIDs, err := adjustGalleryTagIDs(qb, galleryID, *input.TagIds)
if err != nil {
return err
}
if err := qb.UpdateTags(galleryID, tagIDs); err != nil {
return err
}
}
// Save the scenes
if translator.hasField("scene_ids") {
sceneIDs, err := adjustGallerySceneIDs(qb, galleryID, *input.SceneIds)
if err != nil {
return err
}
if err := qb.UpdateScenes(galleryID, sceneIDs); err != nil {
return err
}
}
}
return nil
@@ -367,31 +335,8 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.B
return newRet, nil
}
func adjustGalleryPerformerIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetPerformerIDs(galleryID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func adjustGalleryTagIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetTagIDs(galleryID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func adjustGallerySceneIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetSceneIDs(galleryID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
type GallerySceneGetter interface {
GetSceneIDs(ctx context.Context, galleryID int) ([]int, error)
}
func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.GalleryDestroyInput) (bool, error) {
@@ -403,19 +348,18 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
var galleries []*models.Gallery
var imgsDestroyed []*models.Image
fileDeleter := &image.FileDeleter{
Deleter: *file.NewDeleter(),
Deleter: file.NewDeleter(),
Paths: manager.GetInstance().Paths,
}
deleteGenerated := utils.IsTrue(input.DeleteGenerated)
deleteFile := utils.IsTrue(input.DeleteFile)
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Gallery()
iqb := repo.Image()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
for _, id := range galleryIDs {
gallery, err := qb.Find(id)
gallery, err := qb.Find(ctx, id)
if err != nil {
return err
}
@@ -424,55 +368,14 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
return fmt.Errorf("gallery with id %d not found", id)
}
galleries = append(galleries, gallery)
// if this is a zip-based gallery, delete the images as well first
if gallery.Zip {
imgs, err := iqb.FindByGalleryID(id)
if err != nil {
return err
}
for _, img := range imgs {
if err := image.Destroy(img, iqb, fileDeleter, deleteGenerated, false); err != nil {
return err
}
imgsDestroyed = append(imgsDestroyed, img)
}
if deleteFile {
if err := fileDeleter.Files([]string{gallery.Path.String}); err != nil {
return err
}
}
} else if deleteFile {
// Delete image if it is only attached to this gallery
imgs, err := iqb.FindByGalleryID(id)
if err != nil {
return err
}
for _, img := range imgs {
imgGalleries, err := qb.FindByImageID(img.ID)
if err != nil {
return err
}
if len(imgGalleries) == 1 {
if err := image.Destroy(img, iqb, fileDeleter, deleteGenerated, deleteFile); err != nil {
return err
}
imgsDestroyed = append(imgsDestroyed, img)
}
}
// we only want to delete a folder-based gallery if it is empty.
// don't do this with the file deleter
if err := gallery.LoadFiles(ctx, qb); err != nil {
return fmt.Errorf("loading files for gallery %d", id)
}
if err := qb.Destroy(id); err != nil {
galleries = append(galleries, gallery)
imgsDestroyed, err = r.galleryService.Destroy(ctx, gallery, fileDeleter, deleteGenerated, deleteFile)
if err != nil {
return err
}
}
@@ -488,10 +391,11 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
for _, gallery := range galleries {
// don't delete stash library paths
if utils.IsTrue(input.DeleteFile) && !gallery.Zip && gallery.Path.Valid && !isStashPath(gallery.Path.String) {
path := gallery.Path
if deleteFile && path != "" && !isStashPath(path) {
// try to remove the folder - it is possible that it is not empty
// so swallow the error if present
_ = os.Remove(gallery.Path.String)
_ = os.Remove(path)
}
}
@@ -499,8 +403,8 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
for _, gallery := range galleries {
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
GalleryDestroyInput: input,
Checksum: gallery.Checksum,
Path: gallery.Path.String,
Checksum: gallery.PrimaryChecksum(),
Path: gallery.Path,
}, nil)
}
@@ -526,7 +430,7 @@ func isStashPath(path string) bool {
return false
}
func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.GalleryAddInput) (bool, error) {
func (r *mutationResolver) AddGalleryImages(ctx context.Context, input GalleryAddInput) (bool, error) {
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return false, err
@@ -537,9 +441,9 @@ func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.Ga
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Gallery()
gallery, err := qb.Find(galleryID)
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
gallery, err := qb.Find(ctx, galleryID)
if err != nil {
return err
}
@@ -548,17 +452,7 @@ func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.Ga
return errors.New("gallery not found")
}
if gallery.Zip {
return errors.New("cannot modify zip gallery images")
}
newIDs, err := qb.GetImageIDs(galleryID)
if err != nil {
return err
}
newIDs = intslice.IntAppendUniques(newIDs, imageIDs)
return qb.UpdateImages(galleryID, newIDs)
return r.galleryService.AddImages(ctx, gallery, imageIDs...)
}); err != nil {
return false, err
}
@@ -566,7 +460,7 @@ func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.Ga
return true, nil
}
func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input models.GalleryRemoveInput) (bool, error) {
func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input GalleryRemoveInput) (bool, error) {
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return false, err
@@ -577,9 +471,9 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input models
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Gallery()
gallery, err := qb.Find(galleryID)
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
gallery, err := qb.Find(ctx, galleryID)
if err != nil {
return err
}
@@ -588,17 +482,7 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input models
return errors.New("gallery not found")
}
if gallery.Zip {
return errors.New("cannot modify zip gallery images")
}
newIDs, err := qb.GetImageIDs(galleryID)
if err != nil {
return err
}
newIDs = intslice.IntExclude(newIDs, imageIDs)
return qb.UpdateImages(galleryID, newIDs)
return r.galleryService.RemoveImages(ctx, gallery, imageIDs...)
}); err != nil {
return false, err
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"strconv"
"time"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/file"
@@ -16,8 +15,8 @@ import (
)
func (r *mutationResolver) getImage(ctx context.Context, id int) (ret *models.Image, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Image().Find(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Image.Find(ctx, id)
return err
}); err != nil {
return nil, err
@@ -26,14 +25,14 @@ func (r *mutationResolver) getImage(ctx context.Context, id int) (ret *models.Im
return ret, nil
}
func (r *mutationResolver) ImageUpdate(ctx context.Context, input models.ImageUpdateInput) (ret *models.Image, err error) {
func (r *mutationResolver) ImageUpdate(ctx context.Context, input ImageUpdateInput) (ret *models.Image, err error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Start the transaction and save the image
if err := r.withTxn(ctx, func(repo models.Repository) error {
ret, err = r.imageUpdate(input, translator, repo)
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.imageUpdate(ctx, input, translator)
return err
}); err != nil {
return nil, err
@@ -44,17 +43,17 @@ func (r *mutationResolver) ImageUpdate(ctx context.Context, input models.ImageUp
return r.getImage(ctx, ret.ID)
}
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*models.ImageUpdateInput) (ret []*models.Image, err error) {
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdateInput) (ret []*models.Image, err error) {
inputMaps := getUpdateInputMaps(ctx)
// Start the transaction and save the image
if err := r.withTxn(ctx, func(repo models.Repository) error {
if err := r.withTxn(ctx, func(ctx context.Context) error {
for i, image := range input {
translator := changesetTranslator{
inputMap: inputMaps[i],
}
thisImage, err := r.imageUpdate(*image, translator, repo)
thisImage, err := r.imageUpdate(ctx, *image, translator)
if err != nil {
return err
}
@@ -86,148 +85,169 @@ func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*models.Ima
return newRet, nil
}
func (r *mutationResolver) imageUpdate(input models.ImageUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Image, error) {
func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInput, translator changesetTranslator) (*models.Image, error) {
// Populate image from the input
imageID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
updatedTime := time.Now()
updatedImage := models.ImagePartial{
ID: imageID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
updatedImage.Title = translator.nullString(input.Title, "title")
updatedImage.Rating = translator.nullInt64(input.Rating, "rating")
updatedImage.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedImage.Organized = input.Organized
qb := repo.Image()
image, err := qb.Update(updatedImage)
i, err := r.repository.Image.Find(ctx, imageID)
if err != nil {
return nil, err
}
if i == nil {
return nil, fmt.Errorf("image not found %d", imageID)
}
updatedImage := models.NewImagePartial()
updatedImage.Title = translator.optionalString(input.Title, "title")
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
if input.PrimaryFileID != nil {
primaryFileID, err := strconv.Atoi(*input.PrimaryFileID)
if err != nil {
return nil, fmt.Errorf("converting primary file id: %w", err)
}
converted := file.ID(primaryFileID)
updatedImage.PrimaryFileID = &converted
if err := i.LoadFiles(ctx, r.repository.Image); err != nil {
return nil, err
}
// ensure that new primary file is associated with scene
var f *file.ImageFile
for _, ff := range i.Files.List() {
if ff.ID == converted {
f = ff
}
}
if f == nil {
return nil, fmt.Errorf("file with id %d not associated with image", converted)
}
}
if translator.hasField("gallery_ids") {
if err := r.updateImageGalleries(qb, imageID, input.GalleryIds); err != nil {
updatedImage.GalleryIDs, err = translateUpdateIDs(input.GalleryIds, models.RelationshipUpdateModeSet)
if err != nil {
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
// ensure gallery IDs are loaded
if err := i.LoadGalleryIDs(ctx, r.repository.Image); err != nil {
return nil, err
}
if err := r.galleryService.ValidateImageGalleryChange(ctx, i, *updatedImage.GalleryIDs); err != nil {
return nil, err
}
}
// Save the performers
if translator.hasField("performer_ids") {
if err := r.updateImagePerformers(qb, imageID, input.PerformerIds); err != nil {
return nil, err
updatedImage.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet)
if err != nil {
return nil, fmt.Errorf("converting performer ids: %w", err)
}
}
// Save the tags
if translator.hasField("tag_ids") {
if err := r.updateImageTags(qb, imageID, input.TagIds); err != nil {
return nil, err
updatedImage.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
}
qb := r.repository.Image
image, err := qb.UpdatePartial(ctx, imageID, updatedImage)
if err != nil {
return nil, err
}
return image, nil
}
func (r *mutationResolver) updateImageGalleries(qb models.ImageReaderWriter, imageID int, galleryIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(galleryIDs)
if err != nil {
return err
}
return qb.UpdateGalleries(imageID, ids)
}
func (r *mutationResolver) updateImagePerformers(qb models.ImageReaderWriter, imageID int, performerIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(performerIDs)
if err != nil {
return err
}
return qb.UpdatePerformers(imageID, ids)
}
func (r *mutationResolver) updateImageTags(qb models.ImageReaderWriter, imageID int, tagsIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(tagsIDs)
if err != nil {
return err
}
return qb.UpdateTags(imageID, ids)
}
func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input models.BulkImageUpdateInput) (ret []*models.Image, err error) {
func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageUpdateInput) (ret []*models.Image, err error) {
imageIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, err
}
// Populate image from the input
updatedTime := time.Now()
updatedImage := models.ImagePartial{
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
updatedImage := models.NewImagePartial()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedImage.Title = translator.nullString(input.Title, "title")
updatedImage.Rating = translator.nullInt64(input.Rating, "rating")
updatedImage.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedImage.Organized = input.Organized
updatedImage.Title = translator.optionalString(input.Title, "title")
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
if translator.hasField("gallery_ids") {
updatedImage.GalleryIDs, err = translateUpdateIDs(input.GalleryIds.Ids, input.GalleryIds.Mode)
if err != nil {
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
}
if translator.hasField("performer_ids") {
updatedImage.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode)
if err != nil {
return nil, fmt.Errorf("converting performer ids: %w", err)
}
}
if translator.hasField("tag_ids") {
updatedImage.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
}
// Start the transaction and save the image marker
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
for _, imageID := range imageIDs {
updatedImage.ID = imageID
i, err := r.repository.Image.Find(ctx, imageID)
if err != nil {
return err
}
image, err := qb.Update(updatedImage)
if i == nil {
return fmt.Errorf("image not found %d", imageID)
}
if updatedImage.GalleryIDs != nil {
// ensure gallery IDs are loaded
if err := i.LoadGalleryIDs(ctx, r.repository.Image); err != nil {
return err
}
if err := r.galleryService.ValidateImageGalleryChange(ctx, i, *updatedImage.GalleryIDs); err != nil {
return err
}
}
image, err := qb.UpdatePartial(ctx, imageID, updatedImage)
if err != nil {
return err
}
ret = append(ret, image)
// Save the galleries
if translator.hasField("gallery_ids") {
galleryIDs, err := adjustImageGalleryIDs(qb, imageID, *input.GalleryIds)
if err != nil {
return err
}
if err := qb.UpdateGalleries(imageID, galleryIDs); err != nil {
return err
}
}
// Save the performers
if translator.hasField("performer_ids") {
performerIDs, err := adjustImagePerformerIDs(qb, imageID, *input.PerformerIds)
if err != nil {
return err
}
if err := qb.UpdatePerformers(imageID, performerIDs); err != nil {
return err
}
}
// Save the tags
if translator.hasField("tag_ids") {
tagIDs, err := adjustImageTagIDs(qb, imageID, *input.TagIds)
if err != nil {
return err
}
if err := qb.UpdateTags(imageID, tagIDs); err != nil {
return err
}
}
}
return nil
@@ -251,33 +271,6 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input models.Bul
return newRet, nil
}
func adjustImageGalleryIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetGalleryIDs(imageID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func adjustImagePerformerIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetPerformerIDs(imageID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func adjustImageTagIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetTagIDs(imageID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageDestroyInput) (ret bool, err error) {
imageID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -286,13 +279,11 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD
var i *models.Image
fileDeleter := &image.FileDeleter{
Deleter: *file.NewDeleter(),
Deleter: file.NewDeleter(),
Paths: manager.GetInstance().Paths,
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
i, err = qb.Find(imageID)
if err := r.withTxn(ctx, func(ctx context.Context) error {
i, err = r.repository.Image.Find(ctx, imageID)
if err != nil {
return err
}
@@ -301,7 +292,7 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD
return fmt.Errorf("image with id %d not found", imageID)
}
return image.Destroy(i, qb, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile))
return r.imageService.Destroy(ctx, i, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile))
}); err != nil {
fileDeleter.Rollback()
return false, err
@@ -328,15 +319,14 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image
var images []*models.Image
fileDeleter := &image.FileDeleter{
Deleter: *file.NewDeleter(),
Deleter: file.NewDeleter(),
Paths: manager.GetInstance().Paths,
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
for _, imageID := range imageIDs {
i, err := qb.Find(imageID)
i, err := qb.Find(ctx, imageID)
if err != nil {
return err
}
@@ -347,7 +337,7 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image
images = append(images, i)
if err := image.Destroy(i, qb, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile)); err != nil {
if err := r.imageService.Destroy(ctx, i, fileDeleter, utils.IsTrue(input.DeleteGenerated), utils.IsTrue(input.DeleteFile)); err != nil {
return err
}
}
@@ -379,10 +369,10 @@ func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (ret
return 0, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
ret, err = qb.IncrementOCounter(imageID)
ret, err = qb.IncrementOCounter(ctx, imageID)
return err
}); err != nil {
return 0, err
@@ -397,10 +387,10 @@ func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (ret
return 0, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
ret, err = qb.DecrementOCounter(imageID)
ret, err = qb.DecrementOCounter(ctx, imageID)
return err
}); err != nil {
return 0, err
@@ -415,10 +405,10 @@ func (r *mutationResolver) ImageResetO(ctx context.Context, id string) (ret int,
return 0, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
ret, err = qb.ResetOCounter(imageID)
ret, err = qb.ResetOCounter(ctx, imageID)
return err
}); err != nil {
return 0, err

View File

@@ -9,15 +9,14 @@ import (
"sync"
"time"
"github.com/stashapp/stash/internal/identify"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)
func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
func (r *mutationResolver) MetadataScan(ctx context.Context, input manager.ScanMetadataInput) (string, error) {
jobID, err := manager.GetInstance().Scan(ctx, input)
if err != nil {
@@ -36,7 +35,7 @@ func (r *mutationResolver) MetadataImport(ctx context.Context) (string, error) {
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) ImportObjects(ctx context.Context, input models.ImportObjectsInput) (string, error) {
func (r *mutationResolver) ImportObjects(ctx context.Context, input manager.ImportObjectsInput) (string, error) {
t, err := manager.CreateImportTask(config.GetInstance().GetVideoFileNamingAlgorithm(), input)
if err != nil {
return "", err
@@ -56,12 +55,12 @@ func (r *mutationResolver) MetadataExport(ctx context.Context) (string, error) {
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) ExportObjects(ctx context.Context, input models.ExportObjectsInput) (*string, error) {
func (r *mutationResolver) ExportObjects(ctx context.Context, input manager.ExportObjectsInput) (*string, error) {
t := manager.CreateExportTask(config.GetInstance().GetVideoFileNamingAlgorithm(), input)
var wg sync.WaitGroup
wg.Add(1)
t.Start(&wg)
t.Start(ctx, &wg)
if t.DownloadHash != "" {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
@@ -75,7 +74,7 @@ func (r *mutationResolver) ExportObjects(ctx context.Context, input models.Expor
return nil, nil
}
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input manager.GenerateMetadataInput) (string, error) {
jobID, err := manager.GetInstance().Generate(ctx, input)
if err != nil {
@@ -85,19 +84,19 @@ func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.Ge
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input manager.AutoTagMetadataInput) (string, error) {
jobID := manager.GetInstance().AutoTag(ctx, input)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) MetadataIdentify(ctx context.Context, input models.IdentifyMetadataInput) (string, error) {
func (r *mutationResolver) MetadataIdentify(ctx context.Context, input identify.Options) (string, error) {
t := manager.CreateIdentifyJob(input)
jobID := manager.GetInstance().JobManager.Add(ctx, "Identifying...", t)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) MetadataClean(ctx context.Context, input models.CleanMetadataInput) (string, error) {
func (r *mutationResolver) MetadataClean(ctx context.Context, input manager.CleanMetadataInput) (string, error) {
jobID := manager.GetInstance().Clean(ctx, input)
return strconv.Itoa(jobID), nil
}
@@ -107,10 +106,11 @@ func (r *mutationResolver) MigrateHashNaming(ctx context.Context) (string, error
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.BackupDatabaseInput) (*string, error) {
func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatabaseInput) (*string, error) {
// if download is true, then backup to temporary file and return a link
download := input.Download != nil && *input.Download
mgr := manager.GetInstance()
database := mgr.Database
var backupPath string
if download {
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
@@ -124,10 +124,16 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.Back
backupPath = f.Name()
f.Close()
} else {
backupPath = database.DatabaseBackupPath()
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
if backupDirectoryPath != "" {
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
}
}
backupPath = database.DatabaseBackupPath(backupDirectoryPath)
}
err := database.Backup(database.DB, backupPath)
err := database.Backup(backupPath)
if err != nil {
return nil, err
}
@@ -141,7 +147,7 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.Back
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
fn := filepath.Base(database.DatabaseBackupPath())
fn := filepath.Base(database.DatabaseBackupPath(""))
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
return &ret, nil
} else {

View File

@@ -15,8 +15,8 @@ import (
)
func (r *mutationResolver) getMovie(ctx context.Context, id int) (ret *models.Movie, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Movie().Find(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.Find(ctx, id)
return err
}); err != nil {
return nil, err
@@ -25,7 +25,7 @@ func (r *mutationResolver) getMovie(ctx context.Context, id int) (ret *models.Mo
return ret, nil
}
func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCreateInput) (*models.Movie, error) {
func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInput) (*models.Movie, error) {
// generate checksum from movie name rather than image
checksum := md5.FromString(input.Name)
@@ -76,9 +76,11 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
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 {
@@ -100,16 +102,16 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
// Start the transaction and save the movie
var movie *models.Movie
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Movie()
movie, err = qb.Create(newMovie)
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Movie
movie, err = qb.Create(ctx, newMovie)
if err != nil {
return err
}
// update image table
if len(frontimageData) > 0 {
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {
if err := qb.UpdateImages(ctx, movie.ID, frontimageData, backimageData); err != nil {
return err
}
}
@@ -123,7 +125,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
return r.getMovie(ctx, movie.ID)
}
func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUpdateInput) (*models.Movie, error) {
func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInput) (*models.Movie, error) {
// Populate movie from the input
movieID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -166,7 +168,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
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")
@@ -174,9 +176,9 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
// Start the transaction and save the movie
var movie *models.Movie
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Movie()
movie, err = qb.Update(updatedMovie)
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Movie
movie, err = qb.Update(ctx, updatedMovie)
if err != nil {
return err
}
@@ -184,13 +186,13 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
// update image table
if frontImageIncluded || backImageIncluded {
if !frontImageIncluded {
frontimageData, err = qb.GetFrontImage(updatedMovie.ID)
frontimageData, err = qb.GetFrontImage(ctx, updatedMovie.ID)
if err != nil {
return err
}
}
if !backImageIncluded {
backimageData, err = qb.GetBackImage(updatedMovie.ID)
backimageData, err = qb.GetBackImage(ctx, updatedMovie.ID)
if err != nil {
return err
}
@@ -198,7 +200,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
if len(frontimageData) == 0 && len(backimageData) == 0 {
// both images are being nulled. Destroy them.
if err := qb.DestroyImages(movie.ID); err != nil {
if err := qb.DestroyImages(ctx, movie.ID); err != nil {
return err
}
} else {
@@ -208,7 +210,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
frontimageData, _ = utils.ProcessImageInput(ctx, models.DefaultMovieImage)
}
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {
if err := qb.UpdateImages(ctx, movie.ID, frontimageData, backimageData); err != nil {
return err
}
}
@@ -223,7 +225,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
return r.getMovie(ctx, movie.ID)
}
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input models.BulkMovieUpdateInput) ([]*models.Movie, error) {
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieUpdateInput) ([]*models.Movie, error) {
movieIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, err
@@ -239,19 +241,19 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input models.Bul
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")
ret := []*models.Movie{}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Movie()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Movie
for _, movieID := range movieIDs {
updatedMovie.ID = movieID
existing, err := qb.Find(movieID)
existing, err := qb.Find(ctx, movieID)
if err != nil {
return err
}
@@ -260,7 +262,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input models.Bul
return fmt.Errorf("movie with id %d not found", movieID)
}
movie, err := qb.Update(updatedMovie)
movie, err := qb.Update(ctx, updatedMovie)
if err != nil {
return err
}
@@ -288,14 +290,14 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input models.Bul
return newRet, nil
}
func (r *mutationResolver) MovieDestroy(ctx context.Context, input models.MovieDestroyInput) (bool, error) {
func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyInput) (bool, error) {
id, err := strconv.Atoi(input.ID)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
return repo.Movie().Destroy(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.repository.Movie.Destroy(ctx, id)
}); err != nil {
return false, err
}
@@ -311,10 +313,10 @@ func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string)
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Movie()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Movie
for _, id := range ids {
if err := qb.Destroy(id); err != nil {
if err := qb.Destroy(ctx, id); err != nil {
return err
}
}

View File

@@ -2,7 +2,6 @@ package api
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
@@ -16,8 +15,8 @@ import (
)
func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *models.Performer, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().Find(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Performer.Find(ctx, id)
return err
}); err != nil {
return nil, err
@@ -26,7 +25,17 @@ func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *model
return ret, nil
}
func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.PerformerCreateInput) (*models.Performer, error) {
func stashIDPtrSliceToSlice(v []*models.StashID) []models.StashID {
ret := make([]models.StashID, len(v))
for i, vv := range v {
c := vv
ret[i] = *c
}
return ret
}
func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerCreateInput) (*models.Performer, error) {
// generate checksum from performer name rather than image
checksum := md5.FromString(input.Name)
@@ -44,78 +53,85 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
// Populate a new performer from the input
currentTime := time.Now()
newPerformer := models.Performer{
Name: input.Name,
Checksum: checksum,
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
CreatedAt: currentTime,
UpdatedAt: currentTime,
}
newPerformer.Name = sql.NullString{String: input.Name, Valid: true}
if input.URL != nil {
newPerformer.URL = sql.NullString{String: *input.URL, Valid: true}
newPerformer.URL = *input.URL
}
if input.Gender != nil {
newPerformer.Gender = sql.NullString{String: input.Gender.String(), Valid: true}
newPerformer.Gender = *input.Gender
}
if input.Birthdate != nil {
newPerformer.Birthdate = models.SQLiteDate{String: *input.Birthdate, Valid: true}
d := models.NewDate(*input.Birthdate)
newPerformer.Birthdate = &d
}
if input.Ethnicity != nil {
newPerformer.Ethnicity = sql.NullString{String: *input.Ethnicity, Valid: true}
newPerformer.Ethnicity = *input.Ethnicity
}
if input.Country != nil {
newPerformer.Country = sql.NullString{String: *input.Country, Valid: true}
newPerformer.Country = *input.Country
}
if input.EyeColor != nil {
newPerformer.EyeColor = sql.NullString{String: *input.EyeColor, Valid: true}
newPerformer.EyeColor = *input.EyeColor
}
if input.Height != nil {
newPerformer.Height = sql.NullString{String: *input.Height, Valid: true}
// prefer height_cm over height
if input.HeightCm != nil {
newPerformer.Height = input.HeightCm
} else if input.Height != nil {
h, err := strconv.Atoi(*input.Height)
if err != nil {
return nil, fmt.Errorf("invalid height: %s", *input.Height)
}
newPerformer.Height = &h
}
if input.Measurements != nil {
newPerformer.Measurements = sql.NullString{String: *input.Measurements, Valid: true}
newPerformer.Measurements = *input.Measurements
}
if input.FakeTits != nil {
newPerformer.FakeTits = sql.NullString{String: *input.FakeTits, Valid: true}
newPerformer.FakeTits = *input.FakeTits
}
if input.CareerLength != nil {
newPerformer.CareerLength = sql.NullString{String: *input.CareerLength, Valid: true}
newPerformer.CareerLength = *input.CareerLength
}
if input.Tattoos != nil {
newPerformer.Tattoos = sql.NullString{String: *input.Tattoos, Valid: true}
newPerformer.Tattoos = *input.Tattoos
}
if input.Piercings != nil {
newPerformer.Piercings = sql.NullString{String: *input.Piercings, Valid: true}
newPerformer.Piercings = *input.Piercings
}
if input.Aliases != nil {
newPerformer.Aliases = sql.NullString{String: *input.Aliases, Valid: true}
newPerformer.Aliases = *input.Aliases
}
if input.Twitter != nil {
newPerformer.Twitter = sql.NullString{String: *input.Twitter, Valid: true}
newPerformer.Twitter = *input.Twitter
}
if input.Instagram != nil {
newPerformer.Instagram = sql.NullString{String: *input.Instagram, Valid: true}
newPerformer.Instagram = *input.Instagram
}
if input.Favorite != nil {
newPerformer.Favorite = sql.NullBool{Bool: *input.Favorite, Valid: true}
} else {
newPerformer.Favorite = sql.NullBool{Bool: false, Valid: true}
newPerformer.Favorite = *input.Favorite
}
if input.Rating != nil {
newPerformer.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
newPerformer.Rating = sql.NullInt64{Valid: false}
if input.Rating100 != nil {
newPerformer.Rating = input.Rating100
} else if input.Rating != nil {
rating := models.Rating5To100(*input.Rating)
newPerformer.Rating = &rating
}
if input.Details != nil {
newPerformer.Details = sql.NullString{String: *input.Details, Valid: true}
newPerformer.Details = *input.Details
}
if input.DeathDate != nil {
newPerformer.DeathDate = models.SQLiteDate{String: *input.DeathDate, Valid: true}
d := models.NewDate(*input.DeathDate)
newPerformer.DeathDate = &d
}
if input.HairColor != nil {
newPerformer.HairColor = sql.NullString{String: *input.HairColor, Valid: true}
newPerformer.HairColor = *input.HairColor
}
if input.Weight != nil {
weight := int64(*input.Weight)
newPerformer.Weight = sql.NullInt64{Int64: weight, Valid: true}
newPerformer.Weight = input.Weight
}
if input.IgnoreAutoTag != nil {
newPerformer.IgnoreAutoTag = *input.IgnoreAutoTag
@@ -128,32 +144,31 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
}
// Start the transaction and save the performer
var performer *models.Performer
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Performer()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
performer, err = qb.Create(newPerformer)
err = qb.Create(ctx, &newPerformer)
if err != nil {
return err
}
if len(input.TagIds) > 0 {
if err := r.updatePerformerTags(qb, performer.ID, input.TagIds); err != nil {
if err := r.updatePerformerTags(ctx, newPerformer.ID, input.TagIds); err != nil {
return err
}
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(performer.ID, imageData); err != nil {
if err := qb.UpdateImage(ctx, newPerformer.ID, imageData); err != nil {
return err
}
}
// Save the stash_ids
if input.StashIds != nil {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(performer.ID, stashIDJoins); err != nil {
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, newPerformer.ID, stashIDJoins); err != nil {
return err
}
}
@@ -163,17 +178,14 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
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 models.PerformerUpdateInput) (*models.Performer, error) {
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),
@@ -193,54 +205,62 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
// generate checksum from performer name rather than image
checksum := md5.FromString(*input.Name)
updatedPerformer.Name = &sql.NullString{String: *input.Name, Valid: true}
updatedPerformer.Checksum = &checksum
updatedPerformer.Name = models.NewOptionalString(*input.Name)
updatedPerformer.Checksum = models.NewOptionalString(checksum)
}
updatedPerformer.URL = translator.nullString(input.URL, "url")
updatedPerformer.URL = translator.optionalString(input.URL, "url")
if translator.hasField("gender") {
if input.Gender != nil {
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
updatedPerformer.Gender = models.NewOptionalString(input.Gender.String())
} else {
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
updatedPerformer.Gender = models.NewOptionalStringPtr(nil)
}
}
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
updatedPerformer.Country = translator.nullString(input.Country, "country")
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
updatedPerformer.Height = translator.nullString(input.Height, "height")
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
updatedPerformer.Details = translator.nullString(input.Details, "details")
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = input.IgnoreAutoTag
updatedPerformer.Birthdate = translator.optionalDate(input.Birthdate, "birthdate")
updatedPerformer.Country = translator.optionalString(input.Country, "country")
updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color")
updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements")
// prefer height_cm over height
if translator.hasField("height_cm") {
updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm")
} else if translator.hasField("height") {
updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height")
if err != nil {
return nil, err
}
}
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedPerformer.Details = translator.optionalString(input.Details, "details")
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
// Start the transaction and save the p
var p *models.Performer
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Performer()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
// need to get existing performer
existing, err := qb.Find(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 {
@@ -249,34 +269,34 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
}
}
p, err = qb.Update(updatedPerformer)
_, err = qb.UpdatePartial(ctx, performerID, updatedPerformer)
if err != nil {
return err
}
// Save the tags
if translator.hasField("tag_ids") {
if err := r.updatePerformerTags(qb, p.ID, input.TagIds); err != nil {
if err := r.updatePerformerTags(ctx, performerID, input.TagIds); err != nil {
return err
}
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(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(p.ID); err != nil {
if err := qb.DestroyImage(ctx, performerID); err != nil {
return err
}
}
// Save the stash_ids
if translator.hasField("stash_ids") {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(performerID, stashIDJoins); err != nil {
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, performerID, stashIDJoins); err != nil {
return err
}
}
@@ -286,76 +306,81 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, p.ID, plugin.PerformerUpdatePost, input, translator.getFields())
return r.getPerformer(ctx, p.ID)
r.hookExecutor.ExecutePostHooks(ctx, performerID, plugin.PerformerUpdatePost, input, translator.getFields())
return r.getPerformer(ctx, performerID)
}
func (r *mutationResolver) updatePerformerTags(qb models.PerformerReaderWriter, performerID int, tagsIDs []string) error {
func (r *mutationResolver) updatePerformerTags(ctx context.Context, performerID int, tagsIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(tagsIDs)
if err != nil {
return err
}
return qb.UpdateTags(performerID, ids)
return r.repository.Performer.UpdateTags(ctx, performerID, ids)
}
func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models.BulkPerformerUpdateInput) ([]*models.Performer, error) {
func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPerformerUpdateInput) ([]*models.Performer, error) {
performerIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, err
}
// Populate performer from the input
updatedTime := time.Now()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedPerformer := models.PerformerPartial{
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
updatedPerformer := models.NewPerformerPartial()
updatedPerformer.URL = translator.optionalString(input.URL, "url")
updatedPerformer.Birthdate = translator.optionalDate(input.Birthdate, "birthdate")
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.optionalString(input.Country, "country")
updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color")
// prefer height_cm over height
if translator.hasField("height_cm") {
updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm")
} else if translator.hasField("height") {
updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height")
if err != nil {
return nil, err
}
}
updatedPerformer.URL = translator.nullString(input.URL, "url")
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.nullString(input.Country, "country")
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
updatedPerformer.Height = translator.nullString(input.Height, "height")
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
updatedPerformer.Details = translator.nullString(input.Details, "details")
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = input.IgnoreAutoTag
updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements")
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedPerformer.Details = translator.optionalString(input.Details, "details")
updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
if translator.hasField("gender") {
if input.Gender != nil {
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
updatedPerformer.Gender = models.NewOptionalString(input.Gender.String())
} else {
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
updatedPerformer.Gender = models.NewOptionalStringPtr(nil)
}
}
ret := []*models.Performer{}
// Start the transaction and save the scene marker
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Performer()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
for _, performerID := range performerIDs {
updatedPerformer.ID = performerID
// need to get existing performer
existing, err := qb.Find(performerID)
existing, err := qb.Find(ctx, performerID)
if err != nil {
return err
}
@@ -368,7 +393,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models
return err
}
performer, err := qb.Update(updatedPerformer)
performer, err := qb.UpdatePartial(ctx, performerID, updatedPerformer)
if err != nil {
return err
}
@@ -377,12 +402,12 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models
// Save the tags
if translator.hasField("tag_ids") {
tagIDs, err := adjustTagIDs(qb, performerID, *input.TagIds)
tagIDs, err := adjustTagIDs(ctx, qb, performerID, *input.TagIds)
if err != nil {
return err
}
if err := qb.UpdateTags(performerID, tagIDs); err != nil {
if err := qb.UpdateTags(ctx, performerID, tagIDs); err != nil {
return err
}
}
@@ -409,14 +434,14 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models
return newRet, nil
}
func (r *mutationResolver) PerformerDestroy(ctx context.Context, input models.PerformerDestroyInput) (bool, error) {
func (r *mutationResolver) PerformerDestroy(ctx context.Context, input PerformerDestroyInput) (bool, error) {
id, err := strconv.Atoi(input.ID)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
return repo.Performer().Destroy(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.repository.Performer.Destroy(ctx, id)
}); err != nil {
return false, err
}
@@ -432,10 +457,10 @@ func (r *mutationResolver) PerformersDestroy(ctx context.Context, performerIDs [
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Performer()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
for _, id := range ids {
if err := qb.Destroy(id); err != nil {
if err := qb.Destroy(ctx, id); err != nil {
return err
}
}

View File

@@ -5,10 +5,10 @@ import (
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
)
func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, taskName string, args []*models.PluginArgInput) (string, error) {
func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, taskName string, args []*plugin.PluginArgInput) (string, error) {
m := manager.GetInstance()
m.RunPluginTask(ctx, pluginID, taskName, args)
return "todo", nil

View File

@@ -9,7 +9,7 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *mutationResolver) SaveFilter(ctx context.Context, input models.SaveFilterInput) (ret *models.SavedFilter, err error) {
func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput) (ret *models.SavedFilter, err error) {
if strings.TrimSpace(input.Name) == "" {
return nil, errors.New("name must be non-empty")
}
@@ -23,17 +23,17 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input models.SaveFilt
id = &idv
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
if err := r.withTxn(ctx, func(ctx context.Context) error {
f := models.SavedFilter{
Mode: input.Mode,
Name: input.Name,
Filter: input.Filter,
}
if id == nil {
ret, err = repo.SavedFilter().Create(f)
ret, err = r.repository.SavedFilter.Create(ctx, f)
} else {
f.ID = *id
ret, err = repo.SavedFilter().Update(f)
ret, err = r.repository.SavedFilter.Update(ctx, f)
}
return err
}); err != nil {
@@ -42,14 +42,14 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input models.SaveFilt
return ret, err
}
func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input models.DestroyFilterInput) (bool, error) {
func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input DestroyFilterInput) (bool, error) {
id, err := strconv.Atoi(input.ID)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
return repo.SavedFilter().Destroy(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.repository.SavedFilter.Destroy(ctx, id)
}); err != nil {
return false, err
}
@@ -57,25 +57,25 @@ func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input models.
return true, nil
}
func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input models.SetDefaultFilterInput) (bool, error) {
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.SavedFilter()
func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaultFilterInput) (bool, error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.SavedFilter
if input.Filter == nil {
// clearing
def, err := qb.FindDefault(input.Mode)
def, err := qb.FindDefault(ctx, input.Mode)
if err != nil {
return err
}
if def != nil {
return qb.Destroy(def.ID)
return qb.Destroy(ctx, def.ID)
}
return nil
}
_, err := qb.SetDefault(models.SavedFilter{
_, err := qb.SetDefault(ctx, models.SavedFilter{
Mode: input.Mode,
Filter: *input.Filter,
})

View File

@@ -3,6 +3,7 @@ package api
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"time"
@@ -15,12 +16,86 @@ import (
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/txn"
"github.com/stashapp/stash/pkg/utils"
)
func (r *mutationResolver) getScene(ctx context.Context, id int) (ret *models.Scene, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Scene().Find(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.Find(ctx, id)
return err
}); err != nil {
return nil, err
}
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
@@ -35,8 +110,8 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
}
// Start the transaction and save the scene
if err := r.withTxn(ctx, func(repo models.Repository) error {
ret, err = r.sceneUpdate(ctx, input, translator, repo)
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.sceneUpdate(ctx, input, translator)
return err
}); err != nil {
return nil, err
@@ -50,13 +125,13 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
inputMaps := getUpdateInputMaps(ctx)
// Start the transaction and save the scene
if err := r.withTxn(ctx, func(repo models.Repository) error {
if err := r.withTxn(ctx, func(ctx context.Context) error {
for i, scene := range input {
translator := changesetTranslator{
inputMap: inputMaps[i],
}
thisScene, err := r.sceneUpdate(ctx, *scene, translator, repo)
thisScene, err := r.sceneUpdate(ctx, *scene, translator)
ret = append(ret, thisScene)
if err != nil {
@@ -89,28 +164,133 @@ 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, repo models.Repository) (*models.Scene, error) {
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.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)
}
updatedScene.Organized = translator.optionalBool(input.Organized, "organized")
if input.PrimaryFileID != nil {
primaryFileID, err := strconv.Atoi(*input.PrimaryFileID)
if err != nil {
return nil, fmt.Errorf("converting primary file id: %w", err)
}
converted := file.ID(primaryFileID)
updatedScene.PrimaryFileID = &converted
}
if translator.hasField("performer_ids") {
updatedScene.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet)
if err != nil {
return nil, fmt.Errorf("converting performer ids: %w", err)
}
}
if translator.hasField("tag_ids") {
updatedScene.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
}
if translator.hasField("gallery_ids") {
updatedScene.GalleryIDs, err = translateUpdateIDs(input.GalleryIds, models.RelationshipUpdateModeSet)
if err != nil {
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
}
// Save the movies
if translator.hasField("movies") {
updatedScene.MovieIDs, err = models.UpdateMovieIDsFromInput(input.Movies)
if err != nil {
return nil, fmt.Errorf("converting movie ids: %w", err)
}
}
// Save the stash_ids
if translator.hasField("stash_ids") {
updatedScene.StashIDs = &models.UpdateStashIDs{
StashIDs: input.StashIds,
Mode: models.RelationshipUpdateModeSet,
}
}
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
}
var coverImageData []byte
qb := r.repository.Scene
updatedTime := time.Now()
updatedScene := models.ScenePartial{
ID: sceneID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
s, err := qb.Find(ctx, sceneID)
if err != nil {
return nil, err
}
updatedScene.Title = translator.nullString(input.Title, "title")
updatedScene.Details = translator.nullString(input.Details, "details")
updatedScene.URL = translator.nullString(input.URL, "url")
updatedScene.Date = translator.sqliteDate(input.Date, "date")
updatedScene.Rating = translator.nullInt64(input.Rating, "rating")
updatedScene.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedScene.Organized = input.Organized
if 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
@@ -118,208 +298,108 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp
if err != nil {
return nil, err
}
// update the cover after updating the scene
}
qb := repo.Scene()
s, err := qb.Update(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(sceneID, coverImageData); err != nil {
return nil, err
}
}
// Save the performers
if translator.hasField("performer_ids") {
if err := r.updateScenePerformers(qb, sceneID, input.PerformerIds); err != nil {
return nil, err
}
}
// Save the movies
if translator.hasField("movies") {
if err := r.updateSceneMovies(qb, sceneID, input.Movies); err != nil {
return nil, err
}
}
// Save the tags
if translator.hasField("tag_ids") {
if err := r.updateSceneTags(qb, sceneID, input.TagIds); err != nil {
return nil, err
}
}
// Save the galleries
if translator.hasField("gallery_ids") {
if err := r.updateSceneGalleries(qb, sceneID, input.GalleryIds); err != nil {
return nil, err
}
}
// Save the stash_ids
if translator.hasField("stash_ids") {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(sceneID, stashIDJoins); err != nil {
return nil, err
}
}
// only update the cover image if provided and everything else was successful
if coverImageData != nil {
err = 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) updateScenePerformers(qb models.SceneReaderWriter, sceneID int, performerIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(performerIDs)
if err != nil {
return err
}
return qb.UpdatePerformers(sceneID, ids)
}
func (r *mutationResolver) sceneUpdateCoverImage(ctx context.Context, s *models.Scene, coverImageData []byte) error {
if len(coverImageData) > 0 {
qb := r.repository.Scene
func (r *mutationResolver) updateSceneMovies(qb models.SceneReaderWriter, sceneID int, movies []*models.SceneMovieInput) error {
var movieJoins []models.MoviesScenes
for _, movie := range movies {
movieID, err := strconv.Atoi(movie.MovieID)
if err != nil {
// update cover table
if err := qb.UpdateCover(ctx, s.ID, coverImageData); err != nil {
return err
}
movieJoin := models.MoviesScenes{
MovieID: movieID,
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)
})
}
if movie.SceneIndex != nil {
movieJoin.SceneIndex = sql.NullInt64{
Int64: int64(*movie.SceneIndex),
Valid: true,
}
}
movieJoins = append(movieJoins, movieJoin)
}
return qb.UpdateMovies(sceneID, movieJoins)
return nil
}
func (r *mutationResolver) updateSceneTags(qb models.SceneReaderWriter, sceneID int, tagsIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(tagsIDs)
if err != nil {
return err
}
return qb.UpdateTags(sceneID, ids)
}
func (r *mutationResolver) updateSceneGalleries(qb models.SceneReaderWriter, sceneID int, galleryIDs []string) error {
ids, err := stringslice.StringSliceToIntSlice(galleryIDs)
if err != nil {
return err
}
return qb.UpdateGalleries(sceneID, ids)
}
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.BulkSceneUpdateInput) ([]*models.Scene, error) {
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneUpdateInput) ([]*models.Scene, error) {
sceneIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, err
}
// Populate scene from the input
updatedTime := time.Now()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedScene := models.ScenePartial{
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
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.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)
}
updatedScene.Title = translator.nullString(input.Title, "title")
updatedScene.Details = translator.nullString(input.Details, "details")
updatedScene.URL = translator.nullString(input.URL, "url")
updatedScene.Date = translator.sqliteDate(input.Date, "date")
updatedScene.Rating = translator.nullInt64(input.Rating, "rating")
updatedScene.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedScene.Organized = input.Organized
updatedScene.Organized = translator.optionalBool(input.Organized, "organized")
if translator.hasField("performer_ids") {
updatedScene.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode)
if err != nil {
return nil, fmt.Errorf("converting performer ids: %w", err)
}
}
if translator.hasField("tag_ids") {
updatedScene.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
}
if translator.hasField("gallery_ids") {
updatedScene.GalleryIDs, err = translateUpdateIDs(input.GalleryIds.Ids, input.GalleryIds.Mode)
if err != nil {
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
}
// Save the movies
if translator.hasField("movie_ids") {
updatedScene.MovieIDs, err = translateSceneMovieIDs(*input.MovieIds)
if err != nil {
return nil, fmt.Errorf("converting movie ids: %w", err)
}
}
ret := []*models.Scene{}
// Start the transaction and save the scene marker
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
for _, sceneID := range sceneIDs {
updatedScene.ID = sceneID
scene, err := qb.Update(updatedScene)
scene, err := qb.UpdatePartial(ctx, sceneID, updatedScene)
if err != nil {
return err
}
ret = append(ret, scene)
// Save the performers
if translator.hasField("performer_ids") {
performerIDs, err := adjustScenePerformerIDs(qb, sceneID, *input.PerformerIds)
if err != nil {
return err
}
if err := qb.UpdatePerformers(sceneID, performerIDs); err != nil {
return err
}
}
// Save the tags
if translator.hasField("tag_ids") {
tagIDs, err := adjustTagIDs(qb, sceneID, *input.TagIds)
if err != nil {
return err
}
if err := qb.UpdateTags(sceneID, tagIDs); err != nil {
return err
}
}
// Save the galleries
if translator.hasField("gallery_ids") {
galleryIDs, err := adjustSceneGalleryIDs(qb, sceneID, *input.GalleryIds)
if err != nil {
return err
}
if err := qb.UpdateGalleries(sceneID, galleryIDs); err != nil {
return err
}
}
// Save the movies
if translator.hasField("movie_ids") {
movies, err := adjustSceneMovieIDs(qb, sceneID, *input.MovieIds)
if err != nil {
return err
}
if err := qb.UpdateMovies(sceneID, movies); err != nil {
return err
}
}
}
return nil
@@ -343,9 +423,9 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
return newRet, nil
}
func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
func adjustIDs(existingIDs []int, updateIDs BulkUpdateIds) []int {
// if we are setting the ids, just return the ids
if updateIDs.Mode == models.BulkUpdateIDModeSet {
if updateIDs.Mode == models.RelationshipUpdateModeSet {
existingIDs = []int{}
for _, idStr := range updateIDs.Ids {
id, _ := strconv.Atoi(idStr)
@@ -362,7 +442,7 @@ func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
foundExisting := false
for idx, existingID := range existingIDs {
if existingID == id {
if updateIDs.Mode == models.BulkUpdateIDModeRemove {
if updateIDs.Mode == models.RelationshipUpdateModeRemove {
// remove from the list
existingIDs = append(existingIDs[:idx], existingIDs[idx+1:]...)
}
@@ -372,7 +452,7 @@ func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
}
}
if !foundExisting && updateIDs.Mode != models.BulkUpdateIDModeRemove {
if !foundExisting && updateIDs.Mode != models.RelationshipUpdateModeRemove {
existingIDs = append(existingIDs, id)
}
}
@@ -380,21 +460,12 @@ func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
return existingIDs
}
func adjustScenePerformerIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetPerformerIDs(sceneID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
type tagIDsGetter interface {
GetTagIDs(id int) ([]int, error)
GetTagIDs(ctx context.Context, id int) ([]int, error)
}
func adjustTagIDs(qb tagIDsGetter, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetTagIDs(sceneID)
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
}
@@ -402,57 +473,6 @@ func adjustTagIDs(qb tagIDsGetter, sceneID int, ids models.BulkUpdateIds) (ret [
return adjustIDs(ret, ids), nil
}
func adjustSceneGalleryIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetGalleryIDs(sceneID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func adjustSceneMovieIDs(qb models.SceneReader, sceneID int, updateIDs models.BulkUpdateIds) ([]models.MoviesScenes, error) {
existingMovies, err := qb.GetMovies(sceneID)
if err != nil {
return nil, err
}
// if we are setting the ids, just return the ids
if updateIDs.Mode == models.BulkUpdateIDModeSet {
existingMovies = []models.MoviesScenes{}
for _, idStr := range updateIDs.Ids {
id, _ := strconv.Atoi(idStr)
existingMovies = append(existingMovies, models.MoviesScenes{MovieID: id})
}
return existingMovies, nil
}
for _, idStr := range updateIDs.Ids {
id, _ := strconv.Atoi(idStr)
// look for the id in the list
foundExisting := false
for idx, existingMovie := range existingMovies {
if existingMovie.MovieID == id {
if updateIDs.Mode == models.BulkUpdateIDModeRemove {
// remove from the list
existingMovies = append(existingMovies[:idx], existingMovies[idx+1:]...)
}
foundExisting = true
break
}
}
if !foundExisting && updateIDs.Mode != models.BulkUpdateIDModeRemove {
existingMovies = append(existingMovies, models.MoviesScenes{MovieID: id})
}
}
return existingMovies, err
}
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
sceneID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -463,7 +483,7 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
var s *models.Scene
fileDeleter := &scene.FileDeleter{
Deleter: *file.NewDeleter(),
Deleter: file.NewDeleter(),
FileNamingAlgo: fileNamingAlgo,
Paths: manager.GetInstance().Paths,
}
@@ -471,10 +491,10 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
deleteGenerated := utils.IsTrue(input.DeleteGenerated)
deleteFile := utils.IsTrue(input.DeleteFile)
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
var err error
s, err = qb.Find(sceneID)
s, err = qb.Find(ctx, sceneID)
if err != nil {
return err
}
@@ -486,7 +506,7 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
// kill any running encoders
manager.KillRunningStreams(s, fileNamingAlgo)
return scene.Destroy(s, repo, fileDeleter, deleteGenerated, deleteFile)
return r.sceneService.Destroy(ctx, s, fileDeleter, deleteGenerated, deleteFile)
}); err != nil {
fileDeleter.Rollback()
return false, err
@@ -498,8 +518,8 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
// call post hook after performing the other actions
r.hookExecutor.ExecutePostHooks(ctx, s.ID, plugin.SceneDestroyPost, plugin.SceneDestroyInput{
SceneDestroyInput: input,
Checksum: s.Checksum.String,
OSHash: s.OSHash.String,
Checksum: s.Checksum,
OSHash: s.OSHash,
Path: s.Path,
}, nil)
@@ -511,7 +531,7 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
fileDeleter := &scene.FileDeleter{
Deleter: *file.NewDeleter(),
Deleter: file.NewDeleter(),
FileNamingAlgo: fileNamingAlgo,
Paths: manager.GetInstance().Paths,
}
@@ -519,13 +539,13 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
deleteGenerated := utils.IsTrue(input.DeleteGenerated)
deleteFile := utils.IsTrue(input.DeleteFile)
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
for _, id := range input.Ids {
sceneID, _ := strconv.Atoi(id)
s, err := qb.Find(sceneID)
s, err := qb.Find(ctx, sceneID)
if err != nil {
return err
}
@@ -536,7 +556,7 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
// kill any running encoders
manager.KillRunningStreams(s, fileNamingAlgo)
if err := scene.Destroy(s, repo, fileDeleter, deleteGenerated, deleteFile); err != nil {
if err := r.sceneService.Destroy(ctx, s, fileDeleter, deleteGenerated, deleteFile); err != nil {
return err
}
}
@@ -554,8 +574,8 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
// call post hook after performing the other actions
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, plugin.SceneDestroyPost, plugin.ScenesDestroyInput{
ScenesDestroyInput: input,
Checksum: scene.Checksum.String,
OSHash: scene.OSHash.String,
Checksum: scene.Checksum,
OSHash: scene.OSHash,
Path: scene.Path,
}, nil)
}
@@ -563,9 +583,76 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
return true, nil
}
func (r *mutationResolver) getSceneMarker(ctx context.Context, id int) (ret *models.SceneMarker, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.SceneMarker().Find(id)
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
@@ -574,7 +661,18 @@ func (r *mutationResolver) getSceneMarker(ctx context.Context, id int) (ret *mod
return ret, nil
}
func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.SceneMarkerCreateInput) (*models.SceneMarker, error) {
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)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMarkerCreateInput) (*models.SceneMarker, error) {
primaryTagID, err := strconv.Atoi(input.PrimaryTagID)
if err != nil {
return nil, err
@@ -609,7 +707,7 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.S
return r.getSceneMarker(ctx, ret.ID)
}
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input models.SceneMarkerUpdateInput) (*models.SceneMarker, error) {
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) {
// Populate scene marker from the input
sceneMarkerID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -661,16 +759,16 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
fileDeleter := &scene.FileDeleter{
Deleter: *file.NewDeleter(),
Deleter: file.NewDeleter(),
FileNamingAlgo: fileNamingAlgo,
Paths: manager.GetInstance().Paths,
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.SceneMarker()
sqb := repo.Scene()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.SceneMarker
sqb := r.repository.Scene
marker, err := qb.Find(markerID)
marker, err := qb.Find(ctx, markerID)
if err != nil {
return err
@@ -680,12 +778,12 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
return fmt.Errorf("scene marker with id %d not found", markerID)
}
s, err := sqb.Find(int(marker.SceneID.Int64))
s, err := sqb.Find(ctx, int(marker.SceneID.Int64))
if err != nil {
return err
}
return scene.DestroyMarker(s, marker, qb, fileDeleter)
return scene.DestroyMarker(ctx, s, marker, qb, fileDeleter)
}); err != nil {
fileDeleter.Rollback()
return false, err
@@ -707,32 +805,32 @@ func (r *mutationResolver) changeMarker(ctx context.Context, changeType int, cha
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
fileDeleter := &scene.FileDeleter{
Deleter: *file.NewDeleter(),
Deleter: file.NewDeleter(),
FileNamingAlgo: fileNamingAlgo,
Paths: manager.GetInstance().Paths,
}
// Start the transaction and save the scene marker
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.SceneMarker()
sqb := repo.Scene()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.SceneMarker
sqb := r.repository.Scene
var err error
switch changeType {
case create:
sceneMarker, err = qb.Create(changedMarker)
sceneMarker, err = qb.Create(ctx, changedMarker)
case update:
// check to see if timestamp was changed
existingMarker, err = qb.Find(changedMarker.ID)
existingMarker, err = qb.Find(ctx, changedMarker.ID)
if err != nil {
return err
}
sceneMarker, err = qb.Update(changedMarker)
sceneMarker, err = qb.Update(ctx, changedMarker)
if err != nil {
return err
}
s, err = sqb.Find(int(existingMarker.SceneID.Int64))
s, err = sqb.Find(ctx, int(existingMarker.SceneID.Int64))
}
if err != nil {
return err
@@ -749,7 +847,7 @@ func (r *mutationResolver) changeMarker(ctx context.Context, changeType int, cha
// Save the marker tags
// If this tag is the primary tag, then let's not add it.
tagIDs = intslice.IntExclude(tagIDs, []int{changedMarker.PrimaryTagID})
return qb.UpdateTags(sceneMarker.ID, tagIDs)
return qb.UpdateTags(ctx, sceneMarker.ID, tagIDs)
}); err != nil {
fileDeleter.Rollback()
return nil, err
@@ -760,16 +858,52 @@ 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 {
return 0, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.IncrementOCounter(sceneID)
ret, err = qb.IncrementOCounter(ctx, sceneID)
return err
}); err != nil {
return 0, err
@@ -784,10 +918,10 @@ func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (ret
return 0, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.DecrementOCounter(sceneID)
ret, err = qb.DecrementOCounter(ctx, sceneID)
return err
}); err != nil {
return 0, err
@@ -802,10 +936,10 @@ func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int,
return 0, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.ResetOCounter(sceneID)
ret, err = qb.ResetOCounter(ctx, sceneID)
return err
}); err != nil {
return 0, err

View File

@@ -7,35 +7,43 @@ import (
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scraper/stashbox"
)
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input models.StashBoxFingerprintSubmissionInput) (bool, error) {
func (r *Resolver) stashboxRepository() stashbox.Repository {
return stashbox.Repository{
Scene: r.repository.Scene,
Performer: r.repository.Performer,
Tag: r.repository.Tag,
Studio: r.repository.Studio,
}
}
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input StashBoxFingerprintSubmissionInput) (bool, error) {
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return false, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
return client.SubmitStashBoxFingerprints(ctx, input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
}
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input models.StashBoxBatchPerformerTagInput) (string, error) {
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input manager.StashBoxBatchPerformerTagInput) (string, error) {
jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, input)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input models.StashBoxDraftSubmissionInput) (*string, error) {
func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
id, err := strconv.Atoi(input.ID)
if err != nil {
@@ -43,29 +51,30 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input m
}
var res *string
err = r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Scene()
scene, err := qb.Find(id)
err = r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
scene, err := qb.Find(ctx, id)
if err != nil {
return err
}
filepath := manager.GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
res, err = client.SubmitSceneDraft(ctx, id, boxes[input.StashBoxIndex].Endpoint, filepath)
res, err = client.SubmitSceneDraft(ctx, scene, boxes[input.StashBoxIndex].Endpoint, filepath)
return err
})
return res, err
}
func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, input models.StashBoxDraftSubmissionInput) (*string, error) {
func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
id, err := strconv.Atoi(input.ID)
if err != nil {
@@ -73,9 +82,9 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
}
var res *string
err = r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Performer()
performer, err := qb.Find(id)
err = r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
performer, err := qb.Find(ctx, id)
if err != nil {
return err
}

View File

@@ -17,8 +17,8 @@ import (
)
func (r *mutationResolver) getStudio(ctx context.Context, id int) (ret *models.Studio, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().Find(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.Find(ctx, id)
return err
}); err != nil {
return nil, err
@@ -27,7 +27,7 @@ func (r *mutationResolver) getStudio(ctx context.Context, id int) (ret *models.S
return ret, nil
}
func (r *mutationResolver) StudioCreate(ctx context.Context, input models.StudioCreateInput) (*models.Studio, error) {
func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateInput) (*models.Studio, error) {
// generate checksum from studio name rather than image
checksum := md5.FromString(input.Name)
@@ -58,11 +58,18 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true}
}
if input.Rating != nil {
newStudio.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
newStudio.Rating = sql.NullInt64{Valid: false}
if input.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}
}
@@ -72,36 +79,36 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
// Start the transaction and save the studio
var s *models.Studio
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Studio()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Studio
var err error
s, err = qb.Create(newStudio)
s, err = qb.Create(ctx, newStudio)
if err != nil {
return err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(s.ID, imageData); err != nil {
if err := qb.UpdateImage(ctx, s.ID, imageData); err != nil {
return err
}
}
// Save the stash_ids
if input.StashIds != nil {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(s.ID, stashIDJoins); err != nil {
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, s.ID, stashIDJoins); err != nil {
return err
}
}
if len(input.Aliases) > 0 {
if err := studio.EnsureAliasesUnique(s.ID, input.Aliases, qb); err != nil {
if err := studio.EnsureAliasesUnique(ctx, s.ID, input.Aliases, qb); err != nil {
return err
}
if err := qb.UpdateAliases(s.ID, input.Aliases); err != nil {
if err := qb.UpdateAliases(ctx, s.ID, input.Aliases); err != nil {
return err
}
}
@@ -115,7 +122,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
return r.getStudio(ctx, s.ID)
}
func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.StudioUpdateInput) (*models.Studio, error) {
func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateInput) (*models.Studio, error) {
// Populate studio from the input
studioID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -150,50 +157,50 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
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
var s *models.Studio
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Studio()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Studio
if err := manager.ValidateModifyStudio(updatedStudio, qb); err != nil {
if err := manager.ValidateModifyStudio(ctx, updatedStudio, qb); err != nil {
return err
}
var err error
s, err = qb.Update(updatedStudio)
s, err = qb.Update(ctx, updatedStudio)
if err != nil {
return err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(s.ID, imageData); err != nil {
if err := qb.UpdateImage(ctx, s.ID, imageData); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyImage(s.ID); err != nil {
if err := qb.DestroyImage(ctx, s.ID); err != nil {
return err
}
}
// Save the stash_ids
if translator.hasField("stash_ids") {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(studioID, stashIDJoins); err != nil {
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, studioID, stashIDJoins); err != nil {
return err
}
}
if translator.hasField("aliases") {
if err := studio.EnsureAliasesUnique(studioID, input.Aliases, qb); err != nil {
if err := studio.EnsureAliasesUnique(ctx, studioID, input.Aliases, qb); err != nil {
return err
}
if err := qb.UpdateAliases(studioID, input.Aliases); err != nil {
if err := qb.UpdateAliases(ctx, studioID, input.Aliases); err != nil {
return err
}
}
@@ -207,14 +214,14 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
return r.getStudio(ctx, s.ID)
}
func (r *mutationResolver) StudioDestroy(ctx context.Context, input models.StudioDestroyInput) (bool, error) {
func (r *mutationResolver) StudioDestroy(ctx context.Context, input StudioDestroyInput) (bool, error) {
id, err := strconv.Atoi(input.ID)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
return repo.Studio().Destroy(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.repository.Studio.Destroy(ctx, id)
}); err != nil {
return false, err
}
@@ -230,10 +237,10 @@ func (r *mutationResolver) StudiosDestroy(ctx context.Context, studioIDs []strin
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Studio()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Studio
for _, id := range ids {
if err := qb.Destroy(id); err != nil {
if err := qb.Destroy(ctx, id); err != nil {
return err
}
}

View File

@@ -2,6 +2,7 @@ package api
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
@@ -15,8 +16,8 @@ import (
)
func (r *mutationResolver) getTag(ctx context.Context, id int) (ret *models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().Find(id)
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.Find(ctx, id)
return err
}); err != nil {
return nil, err
@@ -25,7 +26,7 @@ func (r *mutationResolver) getTag(ctx context.Context, id int) (ret *models.Tag,
return ret, nil
}
func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreateInput) (*models.Tag, error) {
func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput) (*models.Tag, error) {
// Populate a new tag from the input
currentTime := time.Now()
newTag := models.Tag{
@@ -34,6 +35,10 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
}
if input.Description != nil {
newTag.Description = sql.NullString{String: *input.Description, Valid: true}
}
if input.IgnoreAutoTag != nil {
newTag.IgnoreAutoTag = *input.IgnoreAutoTag
}
@@ -68,44 +73,44 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
// Start the transaction and save the tag
var t *models.Tag
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Tag()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Tag
// ensure name is unique
if err := tag.EnsureTagNameUnique(0, newTag.Name, qb); err != nil {
if err := tag.EnsureTagNameUnique(ctx, 0, newTag.Name, qb); err != nil {
return err
}
t, err = qb.Create(newTag)
t, err = qb.Create(ctx, newTag)
if err != nil {
return err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(t.ID, imageData); err != nil {
if err := qb.UpdateImage(ctx, t.ID, imageData); err != nil {
return err
}
}
if len(input.Aliases) > 0 {
if err := tag.EnsureAliasesUnique(t.ID, input.Aliases, qb); err != nil {
if err := tag.EnsureAliasesUnique(ctx, t.ID, input.Aliases, qb); err != nil {
return err
}
if err := qb.UpdateAliases(t.ID, input.Aliases); err != nil {
if err := qb.UpdateAliases(ctx, t.ID, input.Aliases); err != nil {
return err
}
}
if len(parentIDs) > 0 {
if err := qb.UpdateParentTags(t.ID, parentIDs); err != nil {
if err := qb.UpdateParentTags(ctx, t.ID, parentIDs); err != nil {
return err
}
}
if len(childIDs) > 0 {
if err := qb.UpdateChildTags(t.ID, childIDs); err != nil {
if err := qb.UpdateChildTags(ctx, t.ID, childIDs); err != nil {
return err
}
}
@@ -113,7 +118,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
// FIXME: This should be called before any changes are made, but
// requires a rewrite of ValidateHierarchy.
if len(parentIDs) > 0 || len(childIDs) > 0 {
if err := tag.ValidateHierarchy(t, parentIDs, childIDs, qb); err != nil {
if err := tag.ValidateHierarchy(ctx, t, parentIDs, childIDs, qb); err != nil {
return err
}
}
@@ -127,7 +132,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
return r.getTag(ctx, t.ID)
}
func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdateInput) (*models.Tag, error) {
func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) (*models.Tag, error) {
// Populate tag from the input
tagID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -168,11 +173,11 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
// Start the transaction and save the tag
var t *models.Tag
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Tag()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Tag
// ensure name is unique
t, err = qb.Find(tagID)
t, err = qb.Find(ctx, tagID)
if err != nil {
return err
}
@@ -188,48 +193,50 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
}
if input.Name != nil && t.Name != *input.Name {
if err := tag.EnsureTagNameUnique(tagID, *input.Name, qb); err != nil {
if err := tag.EnsureTagNameUnique(ctx, tagID, *input.Name, qb); err != nil {
return err
}
updatedTag.Name = input.Name
}
t, err = qb.Update(updatedTag)
updatedTag.Description = translator.nullString(input.Description, "description")
t, err = qb.Update(ctx, updatedTag)
if err != nil {
return err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(tagID, imageData); err != nil {
if err := qb.UpdateImage(ctx, tagID, imageData); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyImage(tagID); err != nil {
if err := qb.DestroyImage(ctx, tagID); err != nil {
return err
}
}
if translator.hasField("aliases") {
if err := tag.EnsureAliasesUnique(tagID, input.Aliases, qb); err != nil {
if err := tag.EnsureAliasesUnique(ctx, tagID, input.Aliases, qb); err != nil {
return err
}
if err := qb.UpdateAliases(tagID, input.Aliases); err != nil {
if err := qb.UpdateAliases(ctx, tagID, input.Aliases); err != nil {
return err
}
}
if parentIDs != nil {
if err := qb.UpdateParentTags(tagID, parentIDs); err != nil {
if err := qb.UpdateParentTags(ctx, tagID, parentIDs); err != nil {
return err
}
}
if childIDs != nil {
if err := qb.UpdateChildTags(tagID, childIDs); err != nil {
if err := qb.UpdateChildTags(ctx, tagID, childIDs); err != nil {
return err
}
}
@@ -237,7 +244,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
// FIXME: This should be called before any changes are made, but
// requires a rewrite of ValidateHierarchy.
if parentIDs != nil || childIDs != nil {
if err := tag.ValidateHierarchy(t, parentIDs, childIDs, qb); err != nil {
if err := tag.ValidateHierarchy(ctx, t, parentIDs, childIDs, qb); err != nil {
logger.Errorf("Error saving tag: %s", err)
return err
}
@@ -252,14 +259,14 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
return r.getTag(ctx, t.ID)
}
func (r *mutationResolver) TagDestroy(ctx context.Context, input models.TagDestroyInput) (bool, error) {
func (r *mutationResolver) TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error) {
tagID, err := strconv.Atoi(input.ID)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
return repo.Tag().Destroy(tagID)
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.repository.Tag.Destroy(ctx, tagID)
}); err != nil {
return false, err
}
@@ -275,10 +282,10 @@ func (r *mutationResolver) TagsDestroy(ctx context.Context, tagIDs []string) (bo
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Tag()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Tag
for _, id := range ids {
if err := qb.Destroy(id); err != nil {
if err := qb.Destroy(ctx, id); err != nil {
return err
}
}
@@ -295,7 +302,7 @@ func (r *mutationResolver) TagsDestroy(ctx context.Context, tagIDs []string) (bo
return true, nil
}
func (r *mutationResolver) TagsMerge(ctx context.Context, input models.TagsMergeInput) (*models.Tag, error) {
func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput) (*models.Tag, error) {
source, err := stringslice.StringSliceToIntSlice(input.Source)
if err != nil {
return nil, err
@@ -311,11 +318,11 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input models.TagsMerge
}
var t *models.Tag
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Tag()
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Tag
var err error
t, err = qb.Find(destination)
t, err = qb.Find(ctx, destination)
if err != nil {
return err
}
@@ -324,25 +331,25 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input models.TagsMerge
return fmt.Errorf("Tag with ID %d not found", destination)
}
parents, children, err := tag.MergeHierarchy(destination, source, qb)
parents, children, err := tag.MergeHierarchy(ctx, destination, source, qb)
if err != nil {
return err
}
if err = qb.Merge(source, destination); err != nil {
if err = qb.Merge(ctx, source, destination); err != nil {
return err
}
err = qb.UpdateParentTags(destination, parents)
err = qb.UpdateParentTags(ctx, destination, parents)
if err != nil {
return err
}
err = qb.UpdateChildTags(destination, children)
err = qb.UpdateChildTags(ctx, destination, children)
if err != nil {
return err
}
err = tag.ValidateHierarchy(t, parents, children, qb)
err = tag.ValidateHierarchy(ctx, t, parents, children, qb)
if err != nil {
logger.Errorf("Error merging tag: %s", err)
return err

View File

@@ -5,6 +5,7 @@ import (
"errors"
"testing"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/plugin"
@@ -15,18 +16,28 @@ import (
// TODO - move this into a common area
func newResolver() *Resolver {
txnMgr := &mocks.TxnManager{}
return &Resolver{
txnManager: mocks.NewTransactionManager(),
txnManager: txnMgr,
repository: manager.Repository{
TxnManager: txnMgr,
Tag: &mocks.TagReaderWriter{},
},
hookExecutor: &mockHookExecutor{},
}
}
const tagName = "tagName"
const errTagName = "errTagName"
const (
tagName = "tagName"
errTagName = "errTagName"
const existingTagID = 1
const existingTagName = "existingTagName"
const newTagID = 2
existingTagID = 1
existingTagName = "existingTagName"
newTagID = 2
)
var testCtx = context.Background()
type mockHookExecutor struct{}
@@ -36,7 +47,7 @@ func (*mockHookExecutor) ExecutePostHooks(ctx context.Context, id int, hookType
func TestTagCreate(t *testing.T) {
r := newResolver()
tagRW := r.txnManager.(*mocks.TransactionManager).Tag().(*mocks.TagReaderWriter)
tagRW := r.repository.Tag.(*mocks.TagReaderWriter)
pp := 1
findFilter := &models.FindFilterType{
@@ -61,25 +72,25 @@ func TestTagCreate(t *testing.T) {
}
}
tagRW.On("Query", tagFilterForName(existingTagName), findFilter).Return([]*models.Tag{
tagRW.On("Query", mock.Anything, tagFilterForName(existingTagName), findFilter).Return([]*models.Tag{
{
ID: existingTagID,
Name: existingTagName,
},
}, 1, nil).Once()
tagRW.On("Query", tagFilterForName(errTagName), findFilter).Return(nil, 0, nil).Once()
tagRW.On("Query", tagFilterForAlias(errTagName), findFilter).Return(nil, 0, nil).Once()
tagRW.On("Query", mock.Anything, tagFilterForName(errTagName), findFilter).Return(nil, 0, nil).Once()
tagRW.On("Query", mock.Anything, tagFilterForAlias(errTagName), findFilter).Return(nil, 0, nil).Once()
expectedErr := errors.New("TagCreate error")
tagRW.On("Create", mock.AnythingOfType("models.Tag")).Return(nil, expectedErr)
tagRW.On("Create", mock.Anything, mock.AnythingOfType("models.Tag")).Return(nil, expectedErr)
_, err := r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
_, err := r.Mutation().TagCreate(testCtx, TagCreateInput{
Name: existingTagName,
})
assert.NotNil(t, err)
_, err = r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
_, err = r.Mutation().TagCreate(testCtx, TagCreateInput{
Name: errTagName,
})
@@ -87,18 +98,18 @@ func TestTagCreate(t *testing.T) {
tagRW.AssertExpectations(t)
r = newResolver()
tagRW = r.txnManager.(*mocks.TransactionManager).Tag().(*mocks.TagReaderWriter)
tagRW = r.repository.Tag.(*mocks.TagReaderWriter)
tagRW.On("Query", tagFilterForName(tagName), findFilter).Return(nil, 0, nil).Once()
tagRW.On("Query", tagFilterForAlias(tagName), findFilter).Return(nil, 0, nil).Once()
tagRW.On("Query", mock.Anything, tagFilterForName(tagName), findFilter).Return(nil, 0, nil).Once()
tagRW.On("Query", mock.Anything, tagFilterForAlias(tagName), findFilter).Return(nil, 0, nil).Once()
newTag := &models.Tag{
ID: newTagID,
Name: tagName,
}
tagRW.On("Create", mock.AnythingOfType("models.Tag")).Return(newTag, nil)
tagRW.On("Find", newTagID).Return(newTag, nil)
tagRW.On("Create", mock.Anything, mock.AnythingOfType("models.Tag")).Return(newTag, nil)
tagRW.On("Find", mock.Anything, newTagID).Return(newTag, nil)
tag, err := r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
tag, err := r.Mutation().TagCreate(testCtx, TagCreateInput{
Name: tagName,
})

View File

@@ -13,13 +13,13 @@ import (
"golang.org/x/text/collate"
)
func (r *queryResolver) Configuration(ctx context.Context) (*models.ConfigResult, error) {
func (r *queryResolver) Configuration(ctx context.Context) (*ConfigResult, error) {
return makeConfigResult(), nil
}
func (r *queryResolver) Directory(ctx context.Context, path, locale *string) (*models.Directory, error) {
func (r *queryResolver) Directory(ctx context.Context, path, locale *string) (*Directory, error) {
directory := &models.Directory{}
directory := &Directory{}
var err error
col := newCollator(locale, collate.IgnoreCase, collate.Numeric)
@@ -59,17 +59,18 @@ func getParent(path string) *string {
}
}
func makeConfigResult() *models.ConfigResult {
return &models.ConfigResult{
func makeConfigResult() *ConfigResult {
return &ConfigResult{
General: makeConfigGeneralResult(),
Interface: makeConfigInterfaceResult(),
Dlna: makeConfigDLNAResult(),
Scraping: makeConfigScrapingResult(),
Defaults: makeConfigDefaultsResult(),
UI: makeConfigUIResult(),
}
}
func makeConfigGeneralResult() *models.ConfigGeneralResult {
func makeConfigGeneralResult() *ConfigGeneralResult {
config := config.GetInstance()
logFile := config.GetLogFile()
@@ -81,9 +82,10 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
scraperUserAgent := config.GetScraperUserAgent()
scraperCDPPath := config.GetScraperCDPPath()
return &models.ConfigGeneralResult{
return &ConfigGeneralResult{
Stashes: config.GetStashPaths(),
DatabasePath: config.GetDatabasePath(),
BackupDirectoryPath: config.GetBackupDirectoryPath(),
GeneratedPath: config.GetGeneratedPath(),
MetadataPath: config.GetMetadataPath(),
ConfigFilePath: config.GetConfigFile(),
@@ -124,7 +126,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
}
}
func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
func makeConfigInterfaceResult() *ConfigInterfaceResult {
config := config.GetInstance()
menuItems := config.GetMenuItems()
soundOnPreview := config.GetSoundOnPreview()
@@ -140,15 +142,18 @@ func makeConfigInterfaceResult() *models.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()
handyKey := config.GetHandyKey()
scriptOffset := config.GetFunscriptOffset()
imageLightboxOptions := config.GetImageLightboxOptions()
// FIXME - misnamed output field means we have redundant fields
disableDropdownCreate := config.GetDisableDropdownCreate()
return &models.ConfigInterfaceResult{
return &ConfigInterfaceResult{
MenuItems: menuItems,
SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle,
@@ -163,6 +168,10 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
ContinuePlaylistDefault: &continuePlaylistDefault,
CSS: &css,
CSSEnabled: &cssEnabled,
Javascript: &javascript,
JavascriptEnabled: &javascriptEnabled,
CustomLocales: &customLocales,
CustomLocalesEnabled: &customLocalesEnabled,
Language: &language,
ImageLightbox: &imageLightboxOptions,
@@ -176,10 +185,10 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
}
}
func makeConfigDLNAResult() *models.ConfigDLNAResult {
func makeConfigDLNAResult() *ConfigDLNAResult {
config := config.GetInstance()
return &models.ConfigDLNAResult{
return &ConfigDLNAResult{
ServerName: config.GetDLNAServerName(),
Enabled: config.GetDLNADefaultEnabled(),
WhitelistedIPs: config.GetDLNADefaultIPWhitelist(),
@@ -187,13 +196,13 @@ func makeConfigDLNAResult() *models.ConfigDLNAResult {
}
}
func makeConfigScrapingResult() *models.ConfigScrapingResult {
func makeConfigScrapingResult() *ConfigScrapingResult {
config := config.GetInstance()
scraperUserAgent := config.GetScraperUserAgent()
scraperCDPPath := config.GetScraperCDPPath()
return &models.ConfigScrapingResult{
return &ConfigScrapingResult{
ScraperUserAgent: &scraperUserAgent,
ScraperCertCheck: config.GetScraperCertCheck(),
ScraperCDPPath: &scraperCDPPath,
@@ -201,12 +210,12 @@ func makeConfigScrapingResult() *models.ConfigScrapingResult {
}
}
func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult {
func makeConfigDefaultsResult() *ConfigDefaultSettingsResult {
config := config.GetInstance()
deleteFileDefault := config.GetDeleteFileDefault()
deleteGeneratedDefault := config.GetDeleteGeneratedDefault()
return &models.ConfigDefaultSettingsResult{
return &ConfigDefaultSettingsResult{
Identify: config.GetDefaultIdentifySettings(),
Scan: config.GetDefaultScanSettings(),
AutoTag: config.GetDefaultAutoTagSettings(),
@@ -216,8 +225,12 @@ func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult {
}
}
func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input models.StashBoxInput) (*models.StashBoxValidationResult, error) {
client := stashbox.NewClient(models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}, r.txnManager)
func makeConfigUIResult() map[string]interface{} {
return config.GetInstance().GetUIConfiguration()
}
func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input config.StashBoxInput) (*StashBoxValidationResult, error) {
client := stashbox.NewClient(models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}, r.txnManager, r.stashboxRepository())
user, err := client.GetUser(ctx)
valid := user != nil && user.Me != nil
@@ -243,7 +256,7 @@ func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input m
}
}
result := models.StashBoxValidationResult{
result := StashBoxValidationResult{
Valid: valid,
Status: status,
}

View File

@@ -3,10 +3,10 @@ package api
import (
"context"
"github.com/stashapp/stash/internal/dlna"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) DlnaStatus(ctx context.Context) (*models.DLNAStatus, error) {
func (r *queryResolver) DlnaStatus(ctx context.Context) (*dlna.Status, error) {
return manager.GetInstance().DLNAService.Status(), nil
}

View File

@@ -13,8 +13,8 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
return nil, err
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Gallery().Find(idInt)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Gallery.Find(ctx, idInt)
return err
}); err != nil {
return nil, err
@@ -23,14 +23,14 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
return ret, nil
}
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *models.FindGalleriesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
galleries, total, err := repo.Gallery().Query(galleryFilter, filter)
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *FindGalleriesResultType, err 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
}
ret = &models.FindGalleriesResultType{
ret = &FindGalleriesResultType{
Count: total,
Galleries: galleries,
}

View File

@@ -12,8 +12,8 @@ import (
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
var image *models.Image
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Image()
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
var err error
if id != nil {
@@ -22,12 +22,20 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
return err
}
image, err = qb.Find(idInt)
image, err = qb.Find(ctx, idInt)
if err != nil {
return err
}
} else if checksum != nil {
image, err = qb.FindByChecksum(*checksum)
var images []*models.Image
images, err = qb.FindByChecksum(ctx, *checksum)
if err != nil {
return err
}
if len(images) > 0 {
image = images[0]
}
}
return err
@@ -38,13 +46,13 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
return image, nil
}
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *models.FindImagesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Image()
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *FindImagesResultType, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
fields := graphql.CollectAllFields(ctx)
result, err := qb.Query(models.ImageQueryOptions{
result, err := qb.Query(ctx, models.ImageQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,
Count: stringslice.StrInclude(fields, "count"),
@@ -57,12 +65,12 @@ func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.Imag
return err
}
images, err := result.Resolve()
images, err := result.Resolve(ctx)
if err != nil {
return err
}
ret = &models.FindImagesResultType{
ret = &FindImagesResultType{
Count: result.Count,
Images: images,
Megapixels: result.Megapixels,

View File

@@ -13,8 +13,8 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
return nil, err
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Movie().Find(idInt)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.Find(ctx, idInt)
return err
}); err != nil {
return nil, err
@@ -23,14 +23,14 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
return ret, nil
}
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *models.FindMoviesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
movies, total, err := repo.Movie().Query(movieFilter, filter)
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *FindMoviesResultType, err 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
}
ret = &models.FindMoviesResultType{
ret = &FindMoviesResultType{
Count: total,
Movies: movies,
}
@@ -44,8 +44,8 @@ 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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Movie().All()
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.All(ctx)
return err
}); err != nil {
return nil, err

View File

@@ -13,8 +13,8 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
return nil, err
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().Find(idInt)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Performer.Find(ctx, idInt)
return err
}); err != nil {
return nil, err
@@ -23,14 +23,14 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
return ret, nil
}
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *models.FindPerformersResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
performers, total, err := repo.Performer().Query(performerFilter, filter)
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *FindPerformersResultType, err 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
}
ret = &models.FindPerformersResultType{
ret = &FindPerformersResultType{
Count: total,
Performers: performers,
}
@@ -43,8 +43,8 @@ func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *mod
}
func (r *queryResolver) AllPerformers(ctx context.Context) (ret []*models.Performer, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().All()
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Performer.All(ctx)
return err
}); err != nil {
return nil, err

View File

@@ -2,13 +2,33 @@ package api
import (
"context"
"strconv"
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) FindSavedFilters(ctx context.Context, mode models.FilterMode) (ret []*models.SavedFilter, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.SavedFilter().FindByMode(mode)
func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *models.SavedFilter, err error) {
idInt, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SavedFilter.Find(ctx, idInt)
return err
}); err != nil {
return nil, err
}
return ret, err
}
func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.FilterMode) (ret []*models.SavedFilter, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
if mode != nil {
ret, err = r.repository.SavedFilter.FindByMode(ctx, *mode)
} else {
ret, err = r.repository.SavedFilter.All(ctx)
}
return err
}); err != nil {
return nil, err
@@ -17,8 +37,8 @@ func (r *queryResolver) FindSavedFilters(ctx context.Context, mode models.Filter
}
func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.SavedFilter().FindDefault(mode)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
return err
}); err != nil {
return nil, err

View File

@@ -12,20 +12,24 @@ import (
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
var scene *models.Scene
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Scene()
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
var err error
if id != nil {
idInt, err := strconv.Atoi(*id)
if err != nil {
return err
}
scene, err = qb.Find(idInt)
scene, err = qb.Find(ctx, idInt)
if err != nil {
return err
}
} else if checksum != nil {
scene, err = qb.FindByChecksum(*checksum)
var scenes []*models.Scene
scenes, err = qb.FindByChecksum(ctx, *checksum)
if len(scenes) > 0 {
scene = scenes[0]
}
}
return err
@@ -36,24 +40,29 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str
return scene, nil
}
func (r *queryResolver) FindSceneByHash(ctx context.Context, input models.SceneHashInput) (*models.Scene, error) {
func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInput) (*models.Scene, error) {
var scene *models.Scene
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Scene()
var err error
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
if input.Checksum != nil {
scene, err = qb.FindByChecksum(*input.Checksum)
scenes, err := qb.FindByChecksum(ctx, *input.Checksum)
if err != nil {
return err
}
if len(scenes) > 0 {
scene = scenes[0]
}
}
if scene == nil && input.Oshash != nil {
scene, err = qb.FindByOSHash(*input.Oshash)
scenes, err := qb.FindByOSHash(ctx, *input.Oshash)
if err != nil {
return err
}
if len(scenes) > 0 {
scene = scenes[0]
}
}
return nil
@@ -64,8 +73,8 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input models.SceneH
return scene, nil
}
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var scenes []*models.Scene
var err error
@@ -73,17 +82,26 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
result := &models.SceneQueryResult{}
if len(sceneIDs) > 0 {
scenes, err = repo.Scene().FindMany(sceneIDs)
scenes, err = r.repository.Scene.FindMany(ctx, sceneIDs)
if err == nil {
result.Count = len(scenes)
for _, s := range scenes {
result.TotalDuration += s.Duration.Float64
size, _ := strconv.ParseFloat(s.Size.String, 64)
result.TotalSize += size
if err = s.LoadPrimaryFile(ctx, r.repository.File); err != nil {
break
}
f := s.Files.Primary()
if f == nil {
continue
}
result.TotalDuration += f.Duration
result.TotalSize += float64(f.Size)
}
}
} else {
result, err = repo.Scene().Query(models.SceneQueryOptions{
result, err = r.repository.Scene.Query(ctx, models.SceneQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,
Count: stringslice.StrInclude(fields, "count"),
@@ -93,7 +111,7 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
TotalSize: stringslice.StrInclude(fields, "filesize"),
})
if err == nil {
scenes, err = result.Resolve()
scenes, err = result.Resolve(ctx)
}
}
@@ -101,7 +119,7 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
return err
}
ret = &models.FindScenesResultType{
ret = &FindScenesResultType{
Count: result.Count,
Scenes: scenes,
Duration: result.TotalDuration,
@@ -116,8 +134,8 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
return ret, nil
}
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneFilter := &models.SceneFilterType{}
@@ -138,7 +156,7 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
fields := graphql.CollectAllFields(ctx)
result, err := repo.Scene().Query(models.SceneQueryOptions{
result, err := r.repository.Scene.Query(ctx, models.SceneQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: queryFilter,
Count: stringslice.StrInclude(fields, "count"),
@@ -151,12 +169,12 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
return err
}
scenes, err := result.Resolve()
scenes, err := result.Resolve(ctx)
if err != nil {
return err
}
ret = &models.FindScenesResultType{
ret = &FindScenesResultType{
Count: result.Count,
Scenes: scenes,
Duration: result.TotalDuration,
@@ -171,17 +189,23 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
return ret, nil
}
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config models.SceneParserInput) (ret *models.SceneParserResultType, err error) {
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.withReadTxn(ctx, func(repo models.ReaderRepository) error {
result, count, err := parser.Parse(repo)
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,
Studio: r.repository.Studio,
Movie: r.repository.Movie,
Tag: r.repository.Tag,
})
if err != nil {
return err
}
ret = &models.SceneParserResultType{
ret = &SceneParserResultType{
Count: count,
Results: result,
}
@@ -199,8 +223,8 @@ func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int)
if distance != nil {
dist = *distance
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Scene().FindDuplicates(dist)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.FindDuplicates(ctx, dist)
return err
}); err != nil {
return nil, err

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