Compare commits

..

241 Commits

Author SHA1 Message Date
WithoutPants
e685f80e3d Update changelog 2023-04-08 08:17:21 +10:00
WithoutPants
9b8d124ac8 Fix empty strings overwriting during scrape (#3647) 2023-04-08 08:15:09 +10:00
WithoutPants
0cd0151251 Don't regenerate covers if present during scan (#3646)
* Don't regenerate covers if present during scan
* Fix performer unit test (unrelated)
2023-04-07 11:57:10 +10:00
WithoutPants
a6ef924d06 Update changelog for hotfix 2023-03-31 10:50:46 +11:00
DingDongSoLong4
3ab8f4aca6 Fix scrape dialog null values (#3621) 2023-03-31 10:12:50 +11:00
ChilledSlim
2d8b6e1722 Add default blob data location (#3613)
On a new installation, Stash will default to 'blobs' (which relative to the CWD of /, becomes '/blobs').
In our recommended installation, this is NOT mapped and as a result will NOT be persistent across the container being removed or updated.

So by default, we are populating blob data which will be deleted, likely leaving to some confused new users.
This should also be noted for existing installations to make this a persistent path.
2023-03-29 16:01:36 +11:00
stash-translation-bot
cb8fc3788a Translations update from Stash (#3591)
* Translated using Weblate (French)

Currently translated at 94.5% (905 of 957 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 94.1% (901 of 957 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 (English (United Kingdom))

Currently translated at 100.0% (957 of 957 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (957 of 957 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 84.0% (804 of 957 strings)

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

* Translated using Weblate (French)

Currently translated at 97.8% (936 of 957 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 92.3% (884 of 957 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 85.0% (814 of 957 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 81.6% (781 of 957 strings)

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

* Translated using Weblate (Croatian)

Currently translated at 9.9% (95 of 957 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 73.7% (706 of 957 strings)

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

* Translated using Weblate (Russian)

Currently translated at 81.6% (781 of 957 strings)

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

* Translated using Weblate (Danish)

Currently translated at 85.9% (823 of 957 strings)

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

* Translated using Weblate (Czech)

Currently translated at 54.0% (517 of 957 strings)

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

* Translated using Weblate (Romanian)

Currently translated at 38.8% (372 of 957 strings)

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

* Translated using Weblate (Korean)

Currently translated at 90.8% (869 of 957 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 40.9% (392 of 957 strings)

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

* Translated using Weblate (Thai)

Currently translated at 31.5% (302 of 957 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 17.5% (168 of 957 strings)

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

* Translated using Weblate (Bengali (Bangladesh))

Currently translated at 23.4% (224 of 957 strings)

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

* Translated using Weblate (Polish)

Currently translated at 98.2% (940 of 957 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% (957 of 957 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% (957 of 957 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% (957 of 957 strings)

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

* Translated using Weblate (Russian)

Currently translated at 82.5% (790 of 957 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (957 of 957 strings)

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

---------

Co-authored-by: MrOV3RDOSE <mrov3rdose@gmail.com>
Co-authored-by: Still <dev@stillu.cc>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Phasetime <phasetime@protonmail.com>
Co-authored-by: Wasylq <Wasylq@protonmail.com>
Co-authored-by: Coscosname <coscosname@gmail.com>
Co-authored-by: Alpaca Serious <srhsgsef@gmail.com>
Co-authored-by: Kwilz <avyriapo@mail.ru>
Co-authored-by: Lauri <stashapp.pot3l@8shield.net>
2023-03-28 21:16:48 -04:00
DingDongSoLong4
a8f9310c0f Fix moveFiles for zip files (#3608) 2023-03-29 09:28:11 +11:00
WithoutPants
046fd1c0be Blob fixes (#3599)
* Fix error if movie back image blob was not found
* Don't error out if scene cover get fails
* Don't error out on image get fails
* Add debug logging for fs blobs
* Remove old blob data when no longer referenced
2023-03-26 10:56:32 +11:00
WithoutPants
0050e4abbf Fix path filtering when directory has whitespace in it (#3600)
* Add quotes for path with space in path filter
* Hide directory errors in path filter
2023-03-26 07:27:44 +11:00
DingDongSoLong4
2bcab7b0be Add busy timeout to database connection (#3593) 2023-03-25 12:37:17 +11:00
PhuriousGeorge
806964086b Add tzdata package for localtime pass-through (#3372)
Current docker variable to pass localtime to container does not function (`/etc/localtime` does not exist within a running container).  I've never built a container, nor personally use Alpine, but based on my digging it appears the package `tzdata` is required for this functionality
2023-03-24 16:57:31 -04:00
DingDongSoLong4
a8816e0635 Fix Arch dependency install command (#3592) 2023-03-24 13:58:57 -04:00
WithoutPants
1f578db2d6 Warn on charset decoding issues (#3568) 2023-03-24 09:04:48 +11:00
stash-translation-bot
7e66741998 Translations update from Stash (#3387)
* Translated using Weblate (French)

Currently translated at 100.0% (899 of 899 strings)

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

* Translated using Weblate (Swedish)

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

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

* Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (899 of 899 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (899 of 899 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (899 of 899 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 87.5% (787 of 899 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 98.8% (890 of 900 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% (900 of 900 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 100.0% (900 of 900 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 100.0% (900 of 900 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 100.0% (900 of 900 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 100.0% (900 of 900 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (900 of 900 strings)

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

* Translated using Weblate (Estonian)

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

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Swedish)

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

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (901 of 901 strings)

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

* Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (901 of 901 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (903 of 903 strings)

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

* Translated using Weblate (Swedish)

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

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

---------

Co-authored-by: MrOV3RDOSE <mrov3rdose@gmail.com>
Co-authored-by: Alpaca Serious <srhsgsef@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Lauri <stashapp.pot3l@8shield.net>
Co-authored-by: Phasetime <phasetime@protonmail.com>
Co-authored-by: Philip Wang <philpw99@gmail.com>
Co-authored-by: ApxuBbI <nick1232@inbox.lv>
Co-authored-by: Coscosname <coscosname@gmail.com>
Co-authored-by: Asdepique777 <asdepique777@gmail.com>
Co-authored-by: Jean Dupont <ph.bauwens@gmail.com>
Co-authored-by: 風林火山 <nezoko@digdig.org>
Co-authored-by: Aa <jarruraita@outlook.com>
Co-authored-by: kermieisinthehouse <kermie@isinthe.house>
2023-03-23 15:25:02 -04:00
WithoutPants
6a1458fb2c Don't use folder select for regex (#3586) 2023-03-23 13:01:28 +11:00
WithoutPants
0841d6877a Fix scan defaults not set correctly 2023-03-23 11:36:50 +11:00
myalow
b967b63288 Add linux package lists (#3571)
Added dependency installs for Arch & Ubuntu. There may be a more straightforward way to install yarn on Ubuntu, but I'm not certain.
2023-03-22 11:27:37 +11:00
WithoutPants
b6b275edc8 Date picker (#3572)
* Add date picker dependency
* Add DateInput component
* Add DateInput to edit panels
* Add DateInput to DateFilter
* Add time to DateInput and add to Timestamp filter
* Use calendar icon for button
2023-03-22 11:25:50 +11:00
WithoutPants
b602ed2381 Add changelog entry 2023-03-22 11:18:19 +11:00
WithoutPants
b1608128d6 Add folder browser to path filter field (#3570) 2023-03-22 11:17:31 +11:00
WithoutPants
cf0e7a4574 Update changelog 2023-03-22 10:56:00 +11:00
WithoutPants
fcfbdc47bc Include organized field in merge dialog (#3565) 2023-03-22 10:54:45 +11:00
WithoutPants
fc7c3f588e Update changelog 2023-03-22 08:13:02 +11:00
WithoutPants
09c724b8d5 Add move files external interface (#3557)
* Add moveFiles graphql mutation
* Move library resolution code into config
* Implement file moving
* Log if old file not removed in SafeMove
* Ensure extensions are consistent
* Don't allow overwriting existing files
2023-03-22 07:57:26 +11:00
WithoutPants
f6387c1018 Fix scene scraping (#3569) 2023-03-21 12:42:55 +11:00
WithoutPants
496c36493b Fix scan cover defaults (#3564)
* Set generate covers to true during migration
* Default generate cover to true
2023-03-20 12:20:51 +11:00
WithoutPants
44c58d6e3c Fix missing studio image filter 2023-03-20 11:22:13 +11:00
WithoutPants
c1ca34303e Fix incorrect performer data being sent and submitted when multiple stash-box endpoints configured (#3543)
* Show performer stash id for selected instance
* Fix incorrect select value in SubmitDraft modal
2023-03-20 10:30:41 +11:00
NodudeWasTaken
4f909d2457 Added Intel QSV drivers to docker (#3540)
* Alpine docker remove QSV, CUDA docker switch to base build to save space
2023-03-20 10:08:44 +11:00
WithoutPants
2b9f573b30 Login fixes (#3555)
* Don't redirect /logout
* Improve login page styling on mobile
2023-03-20 09:42:48 +11:00
WithoutPants
88b3b87f01 Update changelog 2023-03-17 15:09:42 +11:00
WithoutPants
9a24e6356e Filter edit fixes (#3556)
* Fix country selector dropdown
* Don't include search term in filter count
* Use info colour for filter button count pill
2023-03-17 15:08:20 +11:00
WithoutPants
5a41001246 Fix batch performer tagging with multiple endpoints (#3548)
* Set stash ids correctly during performer batch add
* Refactor performer tagger dialogs
2023-03-17 15:07:53 +11:00
WithoutPants
7cff71c35f Add filesystem based blob storage (#3187)
* Refactor transaction hooks. Add preCommit
* Add BlobStore
* Use blobStore for tag images
* Use blobStore for studio images
* Use blobStore for performer images
* Use blobStore for scene covers
* Don't generate screenshots in legacy directory
* Run post-hooks outside original transaction
* Use blobStore for movie images
* Remove unnecessary DestroyImage methods
* Add missing filter for scene cover
* Add covers to generate options
* Add generate cover option to UI
* Add screenshot migration
* Delete thumb files as part of screenshot migration
2023-03-17 10:52:49 +11:00
WithoutPants
c3081700c0 Update changelog 2023-03-16 15:46:35 +11:00
WithoutPants
9bae98cf57 Fix /stream endpoint serving directory for fileless scenes (#3541)
* Fix stream endpoint serving folder
* Hide stream link for fileless scenes
2023-03-16 15:45:25 +11:00
WithoutPants
943a6d3be7 UI filter builder (#3515)
* Add clear criteria button
* Add count to filter button
2023-03-16 15:44:46 +11:00
yoshnopa
7e8f941155 Add Chapters for Galleries (#3289)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-03-16 15:04:54 +11:00
WithoutPants
32c91c4855 Update changelog 2023-03-16 09:11:07 +11:00
WithoutPants
58852f86fe Handle large and all entity queries (#3544)
* Remove upper page size limit
* Batch GetMany function
* Remove upper query limit from UI
2023-03-16 09:08:21 +11:00
WithoutPants
ac67d640db Anonymise marker titles (#3542) 2023-03-16 09:07:33 +11:00
Flashy78
e2b52a4bf6 Add show sprite button to scene tagger list (#3536)
* Add show sprite button to scene tagger list
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-03-14 15:44:39 +11:00
DingDongSoLong4
e22c938d74 Clear image on cancel (#3528) 2023-03-13 13:47:09 +11:00
DingDongSoLong4
99b6d316c3 Automatic database optimization and performance tweaks (#3527)
* Automatic database optimize
* Tweak connection pooling
2023-03-13 13:45:13 +11:00
DingDongSoLong4
bc3730d49f Add debounce hook (#3524)
* Remove noop
* Add debounce hook
2023-03-13 13:24:37 +11:00
DingDongSoLong4
798b3e6dd7 Fix lint error on macOS (#3533) 2023-03-13 11:26:19 +11:00
CJ
c4d08c5225 Address additional overflow on performer page (#3532) 2023-03-13 11:25:52 +11:00
CJ
9aa7ec575a Fix undefined containerHeight error with imageWall columns (#3525) 2023-03-11 09:21:47 +11:00
CJ
b7183900ac add divider on more details pages (#3514) 2023-03-10 20:17:20 +11:00
DingDongSoLong4
579c5ad8b9 Fix unit conversion rounding (#3519) 2023-03-10 12:21:00 +11:00
DingDongSoLong4
1c8aa46da5 Add DASH to docs and log messages (#3521) 2023-03-10 12:19:36 +11:00
CJ
e90b00d3bd improve image wasll column scaling (#3516) 2023-03-10 12:03:08 +11:00
DingDongSoLong4
57951fe6a0 Fix performer height/weight clearing (#3520) 2023-03-10 12:02:07 +11:00
DingDongSoLong4
7a2ee7cdda Fix VTT thumbnails (#3513)
* Fix VTT thumbnails
* Add API key to sceneStreams query
* Add scene ID routes
2023-03-10 11:54:18 +11:00
NodudeWasTaken
0c1b02380e Simple hardware encoding (#3419)
* HW Accel
* CUDA Docker build and adjust the NVENC encoder
* Removed NVENC preset

Using legacy presets is removed in SDK 12 and deprecated since SDK 10.
This commit removed the preset to allow ffmpeg to select the default one.

---------

Co-authored-by: Nodude <>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-03-10 11:25:55 +11:00
CJ
d4fb6b2acf Improved wall view for images (#3511)
* Proper masonry wall view for images
* allow user to configure margin and direction
2023-03-08 12:36:47 +11:00
DingDongSoLong4
9ede271c05 Fix yup schemas (#3509)
* Fix yup schemas
* Add internationalization
2023-03-07 16:19:56 +11:00
DingDongSoLong4
6b59b9643c Item list refactor and related bug fixes (#3474)
* Replace ListHook with ItemList
* Cache ItemList pagination
* Fix SceneMarkerList Helmet
* Prevent ItemList query string conflicts
* Tweak saved filter clearing search term
* Hide pagination on filter changes
* Fix debounce of query term
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-03-07 13:42:51 +11:00
DingDongSoLong4
2d4384169a Add DASH streams for VP9 transcoding (#3275) 2023-03-07 12:57:27 +11:00
DingDongSoLong4
71e1451c94 Setup improvements (#3504)
* Improve setup redirects
* Add network database warning
* Add cache directory to setup
2023-03-07 08:28:19 +11:00
CJ
42fde9bc9f Add divider to gallery tab (#3508) 2023-03-07 08:21:29 +11:00
DingDongSoLong4
381486904b Improve dynamic import error message (#3500)
* Improve dynamic import error message
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-03-03 12:18:46 +11:00
DingDongSoLong4
7b07810c12 Replace react-markdown with react-remark (#3093) 2023-03-02 16:45:09 +11:00
DingDongSoLong4
7c0f4763ad Improve legacy browser support (#3490) 2023-03-02 15:07:28 +11:00
WithoutPants
3dcc23c001 Fix arm32v6 build (#3501)
* Downgrade gqlgen to v0.17.2

The current version of gqlgen (v0.17.24) gives SIGILL crashes on armv7.

* Update gqlparser
2023-03-02 14:25:59 +11:00
mnh
a081b62823 [Feature] Development quickstart guide and Makefile additions (#3495)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-03-01 15:09:28 +11:00
DingDongSoLong4
b1325ce03f Improve release notes dialog (#3497)
* Fix multiple release notes
* Improve release notes dialog
* Hide release notes dialog after setup
2023-03-01 15:08:56 +11:00
DingDongSoLong4
f992b9a0de Fix filter hooks (#3482) 2023-02-28 10:12:14 +11:00
CJ
1d13f46e23 Use matched stash id boolean as the name suggests (#3488) 2023-02-28 10:09:41 +11:00
dependabot[bot]
078f99a7ec Bump golang.org/x/image from 0.0.0-20210220032944-ac19c3e999fb to 0.5.0 (#3494)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.0.0-20210220032944-ac19c3e999fb to 0.5.0.
- [Release notes](https://github.com/golang/image/releases)
- [Commits](https://github.com/golang/image/commits/v0.5.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-28 09:29:14 +11:00
WithoutPants
30809e16fa Update go dependencies (#3480)
* Bump golang.org/x/text from 0.3.7 to 0.3.8

Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.7 to 0.3.8.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.7...v0.3.8)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
...

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

* Update go dependencies

* Update x/net

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-28 08:26:14 +11:00
DingDongSoLong4
445e0a7311 Fix custom transcode arguments (#3491) 2023-02-28 08:15:52 +11:00
WithoutPants
967a25f64a Include database cover in draft submission if filesystem version not present (#3465)
* Fix styling of stash-box dropdown
* Use cover blob in draft submission if present
2023-02-27 10:28:00 +11:00
WithoutPants
dc934d73fa Add setting for dropdown limit (#3459) 2023-02-25 09:31:01 +11:00
DingDongSoLong4
05669f5503 Overhaul HLS streaming (#3274)
* Overhaul HLS streaming
* Fix streaming transcode ffmpeg zombie processes
* Add changelog and release notes
* Documentation
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-02-24 14:55:46 +11:00
JoeSmithStarkers
f767635080 Added the ability to do Sequential Scans (#3378)
* Added the ability to do Seqential Scans
* Modify pkg/txn to run hooks with the outer context, instead of the context that was in a transaction
* update in application manual
2023-02-23 14:38:02 +11:00
pornstasche
75a8d572cc Add ranges to funscript heatmaps (#3373)
* Add range to funscript heatmaps
* Add config to draw heatmap range
* Add setting to UI
* Apply draw range setting

Includes some refactoring

---------

Co-authored-by: kermieisinthehouse <kermie@isinthe.house>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-02-23 12:33:22 +11:00
pornstasche
2b84392df7 Show funscript heatmaps in player (#3374)
* Make the indicator transparent and add a background-size class
* Show the heatmap if available
* Styling tweaks

---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-02-23 09:38:14 +11:00
Tildaar
edc22629b6 add missing brew packages to build instructions (#3475) 2023-02-23 09:37:09 +11:00
Ksrx01
2d528733ff Improved gallery cover lookup (#3391)
* Gallery cover lookup by regex in config.yml
* Added regex validation and an in-app manual entry
* Improved settings description + some translations
* Add changelog entry
2023-02-22 18:50:12 +11:00
WithoutPants
8b6f7db4ef Add changelog (#3467) 2023-02-22 16:10:46 +11:00
WithoutPants
d0f30ebf39 Fix scene merge dialog (#3466)
* Handle bad savedFilterIds
* Fix scene merge dialog
2023-02-22 14:33:18 +11:00
DingDongSoLong4
87e74d1171 Fix latest version (#3464) 2023-02-22 10:00:34 +11:00
WithoutPants
8f17721d54 Merge pull request #3462 from stashapp/releases/0.19.1
Merge 0.19.1 branch
2023-02-21 18:05:39 +11:00
WithoutPants
066e0b3d5f Merge branch 'develop' into releases/0.19.1 2023-02-21 15:56:42 +11:00
WithoutPants
e9fa7d071e Add changelog entries 2023-02-21 12:06:17 +11:00
DingDongSoLong4
dd5cff2aec Minor gallery-related fixes (#3448)
* Fix gallery titles
* Fix SceneListTable
2023-02-21 12:00:33 +11:00
DingDongSoLong4
c7c4d5b126 Optimize allData queries (#3452)
* Add specific fields to allData queries
* Add additional allData endpoints
2023-02-21 11:53:08 +11:00
DingDongSoLong4
ccbe3c4e92 Fix batch performer panic (#3456) 2023-02-21 11:45:58 +11:00
DingDongSoLong4
96ce260a40 Fix allTags cache reset (#3444) 2023-02-21 11:45:13 +11:00
DingDongSoLong4
28b8473f2d Minor gallery-related fixes (#3448)
* Fix gallery titles
* Fix SceneListTable
2023-02-20 09:25:48 +11:00
DingDongSoLong4
51469cfc7f Optimize allData queries (#3452)
* Add specific fields to allData queries
* Add additional allData endpoints
2023-02-20 09:24:47 +11:00
DingDongSoLong4
b3c23950e2 Fix batch performer panic (#3456) 2023-02-20 09:12:22 +11:00
CrookedDuck
bb6fa04654 Added favorite button in Performer grid view (#3369)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-02-17 14:47:58 +11:00
JoeSmithStarkers
390f72207c added vfr detection, to improve preview generation performance (#3376) 2023-02-17 13:59:36 +11:00
DingDongSoLong4
f92ba7ba53 Fix allTags cache reset (#3444) 2023-02-17 13:51:03 +11:00
DingDongSoLong4
4e34de4c1e Fix yup schemas (#3445) 2023-02-17 12:05:16 +11:00
DingDongSoLong4
bd747317d4 Update dependencies (again) (#3442)
* Update dependencies
* Upgrade rollup
* Remove all index.ts reexport files
2023-02-17 09:42:44 +11:00
DingDongSoLong4
a1851b3713 Update dependencies (#3123)
* Update localforage, remove query-string
* Update fontawesome and flag-icons
* Update formatjs
* Update axios and videojs
* Update apollo client and graphql
* Update bootstrap and react
* Update polyfills
* Update vite
* Update ESLint
* Update stylelint
* Update configs
* Rebuild yarn.lock
2023-02-16 14:06:44 +11:00
DingDongSoLong4
0c9eeef143 Add Cache-Control header to custom css/js (#3434) 2023-02-16 10:29:04 +11:00
alexandra-3
8ab095f675 Sort duplicate scenes by path (#3157) 2023-02-16 10:20:14 +11:00
CJ
ebf3a4ba8e Add tenth place precision star rating (#3343)
* Add tenth place precision star rating
* include rating number by stars
2023-02-16 10:18:10 +11:00
Flashy78
2f312ac651 Use first url available if no studio url (#3439) 2023-02-16 10:17:20 +11:00
DingDongSoLong4
7d1a565803 Fix golangci-lint error (#3425) 2023-02-16 10:15:58 +11:00
DingDongSoLong4
8437e10027 Improve 'item not found' pages (#3438)
* Make scene 'not found' page consistent
* Make backend response for no result consistent
2023-02-16 10:12:01 +11:00
DogmaDragon
a1e7f8940b [Documentation] Add a few more badges (#3398) 2023-02-16 10:08:58 +11:00
xWTF
6a5a2060bf Fix gallery zip scan context (#3433)
* fix zip scan context
* move ValueOnlyContext to utils, use it for zipCtx
2023-02-16 10:07:52 +11:00
xWTF
d00966c335 Support non-utf8 encoding for zip files (#3389)
* detect & decode zip file names in newZipFS
2023-02-16 10:06:12 +11:00
WithoutPants
8bd5f91e58 Disable tag popover for create tag option (#3429) 2023-02-13 12:15:06 +11:00
DingDongSoLong4
83cb51ec47 Error reliably on invalid filter (#3428) 2023-02-13 12:14:41 +11:00
DingDongSoLong4
ca38a355d2 Add regex validation (#3424) 2023-02-13 10:48:10 +11:00
WithoutPants
d2865b0796 Use absolute paths when creating config file (#3417) 2023-02-11 09:22:17 +11:00
WithoutPants
3cf97f6e27 Handle folder symlinks correctly during clean (#3415) 2023-02-11 09:21:18 +11:00
WithoutPants
f0988817c8 Fix multi scrape bug with fileless scenes (#3414) 2023-02-11 09:19:15 +11:00
WithoutPants
692c1e55ac Fix save disabled when creating new object with name (#3409)
* Dirty when creating with name
* Fix prompt on tag save
2023-02-11 08:59:08 +11:00
WithoutPants
18b44e9381 Set performer disambiguation for names with parentheses (#3406) 2023-02-10 09:30:23 +11:00
CJ
7761ac19de Add count attribute to badges (#3405) 2023-02-08 15:08:38 +11:00
dependabot[bot]
deb5110623 Bump http-cache-semantics from 4.1.0 to 4.1.1 in /ui/v2.5 (#3393)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

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

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

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

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

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

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

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

Currently translated at 98.0% (868 of 885 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 97.8% (866 of 885 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (886 of 886 strings)

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

* Translated using Weblate (Korean)

Currently translated at 98.9% (877 of 886 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (886 of 886 strings)

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

* Translated using Weblate (German)

Currently translated at 98.9% (877 of 886 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (886 of 886 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (886 of 886 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (886 of 886 strings)

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

* Translated using Weblate (Russian)

Currently translated at 88.9% (788 of 886 strings)

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

* Translated using Weblate (Korean)

Currently translated at 99.0% (878 of 886 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (886 of 886 strings)

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

* Translated using Weblate (German)

Currently translated at 99.6% (883 of 886 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (886 of 886 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (886 of 886 strings)

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

* Translated using Weblate (Croatian)

Currently translated at 10.7% (95 of 886 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (886 of 886 strings)

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

* Translated using Weblate (Danish)

Currently translated at 93.7% (831 of 886 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Russian)

Currently translated at 88.4% (788 of 891 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 91.0% (811 of 891 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 91.5% (816 of 891 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.7% (889 of 891 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (891 of 891 strings)

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

---------

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

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

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

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

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

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

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

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

Currently translated at 100.0% (884 of 884 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 97.8% (865 of 884 strings)

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

* Translated using Weblate (Polish)

Currently translated at 98.5% (871 of 884 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (Finnish)

Currently translated at 91.2% (808 of 885 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (Russian)

Currently translated at 89.0% (788 of 885 strings)

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

* Translated using Weblate (Korean)

Currently translated at 97.5% (863 of 885 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 95.4% (845 of 885 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 97.1% (860 of 885 strings)

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

* Translated using Weblate (Polish)

Currently translated at 98.9% (876 of 885 strings)

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

* Translated using Weblate (Korean)

Currently translated at 97.7% (865 of 885 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (885 of 885 strings)

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

* Translated using Weblate (Czech)

Currently translated at 59.0% (523 of 885 strings)

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

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

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

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

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

* Translated using Weblate (Korean)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

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

* Translated using Weblate (Czech)

Currently translated at 58.4% (494 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.8% (844 of 845 strings)

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

* Translated using Weblate (German)

Currently translated at 99.8% (844 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.7% (826 of 845 strings)

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

* Added translation using Weblate (Estonian)

* Translated using Weblate (Estonian)

Currently translated at 0.5% (5 of 845 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 37.2% (315 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.8% (827 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.8% (827 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 97.8% (827 of 845 strings)

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

* Translated using Weblate (Danish)

Currently translated at 97.5% (824 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 99.2% (839 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 99.2% (839 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 99.2% (839 of 845 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Estonian)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.8% (844 of 845 strings)

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

* Translated using Weblate (Russian)

Currently translated at 75.9% (642 of 845 strings)

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

* Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Russian)

Currently translated at 85.6% (724 of 845 strings)

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

* Translated using Weblate (Russian)

Currently translated at 85.6% (724 of 845 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.8% (844 of 845 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 19.5% (165 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Added translation using Weblate (Bengali)

* Translated using Weblate (Russian)

Currently translated at 91.0% (769 of 845 strings)

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

* Translated using Weblate (Russian)

Currently translated at 91.0% (769 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.7% (843 of 845 strings)

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

* Translated using Weblate (Russian)

Currently translated at 90.8% (768 of 845 strings)

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

* Translated using Weblate (Bengali (Bangladesh))

Currently translated at 17.1% (145 of 845 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (846 of 846 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (846 of 846 strings)

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

* Translated using Weblate (Russian)

Currently translated at 90.7% (768 of 846 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (848 of 848 strings)

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

* Translated using Weblate (Russian)

Currently translated at 90.5% (768 of 848 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (848 of 848 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (848 of 848 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (847 of 848 strings)

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

* Added translation using Weblate (Persian)

* Translated using Weblate (Persian)

Currently translated at 0.7% (6 of 848 strings)

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

* Translated using Weblate (Russian)

Currently translated at 90.5% (768 of 848 strings)

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

* Translated using Weblate (Bengali (Bangladesh))

Currently translated at 26.6% (226 of 848 strings)

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

* Translated using Weblate (Swedish)

Currently translated at 100.0% (848 of 848 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.6% (845 of 848 strings)

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

* Translated using Weblate (Russian)

Currently translated at 91.1% (773 of 848 strings)

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

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

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

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

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

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

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

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

issue with rendering UI filter dialog

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

View File

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

View File

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

8
.gitignore vendored
View File

@@ -17,12 +17,17 @@
# GraphQL generated output
internal/api/generated_*.go
ui/v2.5/src/core/generated-*.tsx
####
# Jetbrains
####
####
# Visual Studio
####
/.vs
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
@@ -58,3 +63,4 @@ node_modules
/stash
dist
.DS_Store
/.local

View File

@@ -9,14 +9,14 @@ endif
ifdef IS_WIN_SHELL
SEPARATOR := &&
SET := set
RM := del /s /q
RMDIR := rmdir /s /q
PWD := $(shell echo %cd%)
else
SEPARATOR := ;
SET := export
endif
IS_WIN_OS =
ifeq ($(OS),Windows_NT)
IS_WIN_OS = true
RM := rm -f
RMDIR := rm -rf
endif
# set LDFLAGS environment variable to any extra ldflags required
@@ -29,9 +29,14 @@ endif
export CGO_ENABLED = 1
# including netgo causes name resolution to go through the Go resolver
# and isn't necessary for static builds on Windows
GO_BUILD_TAGS_WINDOWS := sqlite_omit_load_extension sqlite_stat4 osusergo
GO_BUILD_TAGS_DEFAULT = $(GO_BUILD_TAGS_WINDOWS) netgo
.PHONY: release pre-build
release: generate ui build-release
release: pre-ui generate ui build-release
pre-build:
ifndef BUILD_DATE
@@ -47,14 +52,21 @@ ifndef STASH_VERSION
endif
ifndef OFFICIAL_BUILD
$(eval OFFICIAL_BUILD := false)
$(eval OFFICIAL_BUILD := false)
endif
ifndef GO_BUILD_TAGS
$(eval GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT))
endif
# NOTE: the build target still includes netgo because we cannot detect
# Windows easily from the Makefile.
build: pre-build
build:
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/internal/api.version=$(STASH_VERSION)' -X 'github.com/stashapp/stash/internal/api.buildstamp=$(BUILD_DATE)' -X 'github.com/stashapp/stash/internal/api.githash=$(GITHASH)')
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/internal/manager/config.officialBuild=$(OFFICIAL_BUILD)')
go build $(OUTPUT) -mod=vendor -v -tags "sqlite_omit_load_extension sqlite_stat4 osusergo netgo" $(GO_BUILD_FLAGS) -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS) $(PLATFORM_SPECIFIC_LDFLAGS)" ./cmd/stash
go build $(OUTPUT) -mod=vendor -v -tags "$(GO_BUILD_TAGS)" $(GO_BUILD_FLAGS) -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS) $(PLATFORM_SPECIFIC_LDFLAGS)" ./cmd/stash
# strips debug symbols from the release build
build-release: EXTRA_LDFLAGS := -s -w
@@ -71,6 +83,7 @@ cross-compile-windows: export GOARCH := amd64
cross-compile-windows: export CC := x86_64-w64-mingw32-gcc
cross-compile-windows: export CXX := x86_64-w64-mingw32-g++
cross-compile-windows: OUTPUT := -o dist/stash-win.exe
cross-compile-windows: GO_BUILD_TAGS := $(GO_BUILD_TAGS_WINDOWS)
cross-compile-windows: build-release-static
cross-compile-macos-intel: export GOOS := darwin
@@ -78,6 +91,7 @@ cross-compile-macos-intel: export GOARCH := amd64
cross-compile-macos-intel: export CC := o64-clang
cross-compile-macos-intel: export CXX := o64-clang++
cross-compile-macos-intel: OUTPUT := -o dist/stash-macos-intel
cross-compile-macos-intel: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
# can't use static build for OSX
cross-compile-macos-intel: build-release
@@ -86,10 +100,11 @@ 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
cross-compile-macos:
cross-compile-macos:
rm -rf dist/Stash.app dist/Stash-macos.zip
make cross-compile-macos-applesilicon
make cross-compile-macos-intel
@@ -106,17 +121,20 @@ cross-compile-macos:
cross-compile-freebsd: export GOOS := freebsd
cross-compile-freebsd: export GOARCH := amd64
cross-compile-freebsd: OUTPUT := -o dist/stash-freebsd
cross-compile-freebsd: GO_BUILD_TAGS += netgo
cross-compile-freebsd: build-release-static
cross-compile-linux: export GOOS := linux
cross-compile-linux: export GOARCH := amd64
cross-compile-linux: OUTPUT := -o dist/stash-linux
cross-compile-linux: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
cross-compile-linux: build-release-static
cross-compile-linux-arm64v8: export GOOS := linux
cross-compile-linux-arm64v8: export GOARCH := arm64
cross-compile-linux-arm64v8: export CC := aarch64-linux-gnu-gcc
cross-compile-linux-arm64v8: OUTPUT := -o dist/stash-linux-arm64v8
cross-compile-linux-arm64v8: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
cross-compile-linux-arm64v8: build-release-static
cross-compile-linux-arm32v7: export GOOS := linux
@@ -124,6 +142,7 @@ cross-compile-linux-arm32v7: export GOARCH := arm
cross-compile-linux-arm32v7: export GOARM := 7
cross-compile-linux-arm32v7: export CC := arm-linux-gnueabihf-gcc
cross-compile-linux-arm32v7: OUTPUT := -o dist/stash-linux-arm32v7
cross-compile-linux-arm32v7: GO_BUILD_TAGS := $(GO_BUILD_TAGS_DEFAULT)
cross-compile-linux-arm32v7: build-release-static
cross-compile-linux-arm32v6: export GOOS := linux
@@ -131,11 +150,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
@@ -159,7 +180,7 @@ generate-frontend:
cd ui/v2.5 && yarn run gqlgen
.PHONY: generate-backend
generate-backend: touch-ui
generate-backend: touch-ui
go generate -mod=vendor ./cmd/stash
.PHONY: generate-dataloaders
@@ -195,6 +216,23 @@ it:
generate-test-mocks:
go run -mod=vendor github.com/vektra/mockery/v2 --dir ./pkg/models --name '.*ReaderWriter' --outpkg mocks --output ./pkg/models/mocks
# runs server
# sets the config file to use the local dev config
.PHONY: server-start
server-start: export STASH_CONFIG_FILE=config.yml
server-start:
ifndef IS_WIN_SHELL
@mkdir -p .local
else
@if not exist ".local" mkdir .local
endif
cd .local && go run ../cmd/stash
# removes local dev config files
.PHONY: server-clean
server-clean:
$(RMDIR) .local
# installs UI dependencies. Run when first cloning repository, or if UI
# dependencies have changed
.PHONY: pre-ui
@@ -240,3 +278,8 @@ validate-backend: lint it
.PHONY: docker-build
docker-build: pre-build
docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/build -f docker/build/x86_64/Dockerfile .
# locally builds and tags a 'stash/cuda-build' docker image
.PHONY: docker-cuda-build
docker-cuda-build: pre-build
docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/cuda-build -f docker/build/x86_64/Dockerfile-CUDA .

View File

@@ -3,8 +3,11 @@ https://stashapp.cc
[![Build](https://github.com/stashapp/stash/actions/workflows/build.yml/badge.svg?branch=develop&event=push)](https://github.com/stashapp/stash/actions/workflows/build.yml)
[![Docker pulls](https://img.shields.io/docker/pulls/stashapp/stash.svg)](https://hub.docker.com/r/stashapp/stash 'DockerHub')
[![Open Collective backers](https://img.shields.io/opencollective/backers/stashapp?logo=opencollective)](https://opencollective.com/stashapp)
[![Go Report Card](https://goreportcard.com/badge/github.com/stashapp/stash)](https://goreportcard.com/report/github.com/stashapp/stash)
[![Discord](https://img.shields.io/discord/559159668438728723.svg?logo=discord)](https://discord.gg/2TsNFKt)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/stashapp/stash?logo=github)](https://github.com/stashapp/stash/releases/latest)
[![GitHub issues by-label](https://img.shields.io/github/issues-raw/stashapp/stash/bounty)](https://github.com/stashapp/stash/labels/bounty)
### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.**
![demo image](docs/readme_assets/demo_image.png)
@@ -26,7 +29,7 @@ For further information you can [read the in-app manual](ui/v2.5/src/docs/en).
## First Run
#### Windows Users: Security Prompt
Running the app might present a security prompt since the binary isn't yet signed. Bypass this by clicking "more info" and then the "run anyway" button.
Running the app might present a security prompt since the binary isn't yet signed. Bypass this by clicking "more info" and then the "run anyway" button.
#### FFMPEG
Stash requires ffmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
@@ -39,32 +42,31 @@ On first run, Stash will prompt you for some configuration options and media dir
Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en/Scraping.md), which integrate directly into Stash.
Many community-maintained scrapers are available for download at the [Community Scrapers Collection](https://github.com/stashapp/CommunityScrapers). The community also maintains StashDB, a crowd-sourced repository of scene, studio, and performer information, that can automatically identify much of a typical media collection. Inquire in the Discord for details. Identifying an entire collection will typically require a mix of multiple sources.
Many community-maintained scrapers are available for download from [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers). The community also maintains StashDB, a crowd-sourced repository of scene, studio, and performer information, that can automatically identify much of a typical media collection. Inquire in the Discord for details. Identifying an entire collection will typically require a mix of multiple sources.
<sub>StashDB is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box).</sub>
<sub>[StashDB](http://stashdb.org) is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box).</sub>
# Translation
[![Translate](https://translate.stashapp.cc/widgets/stash/-/stash-desktop-client/svg-badge.svg)](https://translate.stashapp.cc/engage/stash/)
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇪🇸 🇸🇪 🇹🇼 🇹🇷
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇪🇪 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇷🇺 🇪🇸 🇸🇪 🇹🇼 🇹🇷
Stash is available in 16 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks!
Stash is available in 25 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks!
# Support (FAQ)
Answers to other Frequently Asked Questions can be found [on our Wiki](https://github.com/stashapp/stash/wiki/FAQ)
Check out our documentation on [Stash-Docs](https://docs.stashapp.cc) for information about the software, questions, guides, add-ons and more.
For issues not addressed there, there are a few options.
* Read the [Wiki](https://github.com/stashapp/stash/wiki)
* Check the in-app documentation, in the top right corner of the app (also available [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en)
For more help you can:
* Check the in-app documentation, in the top right corner of the app (it's also mirrored on [Stash-Docs](https://docs.stashapp.cc/in-app-manual))
* Join the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support.
* Start a [discussion on GitHub](https://github.com/stashapp/stash/discussions)
# Customization
## Themes and CSS Customization
There is a [directory of community-created themes](https://github.com/stashapp/stash/wiki/Themes) on our Wiki, along with instructions on how to install them.
There is a [directory of community-created themes](https://docs.stashapp.cc/user-interface-ui/themes) on Stash-Docs, along with instructions on how to install them.
You can also make Stash interface fit your desired style with [Custom CSS snippets](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets).
You can also change the Stash interface to fit your desired style with various snippets from [Custom CSS snippets](https://docs.stashapp.cc/user-interface-ui/custom-css-snippets).
# For Developers

View File

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

View File

@@ -0,0 +1,51 @@
# This dockerfile should be built with `make docker-cuda-build` from the stash root.
# Build Frontend
FROM node:alpine as frontend
RUN apk add --no-cache make
## cache node_modules separately
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
WORKDIR /stash
RUN yarn --cwd ui/v2.5 install --frozen-lockfile.
COPY Makefile /stash/
COPY ./graphql /stash/graphql/
COPY ./ui /stash/ui/
RUN make generate-frontend
ARG GITHASH
ARG STASH_VERSION
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
# Build Backend
FROM golang:1.19-bullseye as backend
RUN apt update && apt install -y build-essential golang
WORKDIR /stash
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
COPY ./scripts /stash/scripts/
COPY ./vendor /stash/vendor/
COPY ./pkg /stash/pkg/
COPY ./cmd /stash/cmd
COPY ./internal /stash/internal
COPY --from=frontend /stash /stash/
RUN make generate-backend
ARG GITHASH
ARG STASH_VERSION
RUN make build
# Final Runnable Image
FROM nvidia/cuda:12.0.1-base-ubuntu22.04
RUN apt update && apt upgrade -y && apt install -y ca-certificates libvips-tools ffmpeg wget intel-media-va-driver-non-free vainfo
RUN rm -rf /var/lib/apt/lists/*
COPY --from=backend /stash/stash /usr/bin/
# NVENC Patch
RUN mkdir -p /usr/local/bin /patched-lib
RUN wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh -O /usr/local/bin/patch.sh
RUN wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/docker-entrypoint.sh -O /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/patch.sh /usr/local/bin/docker-entrypoint.sh /usr/bin/stash
ENV LANG C.UTF-8
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES=video,utility
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999
ENTRYPOINT ["docker-entrypoint.sh", "stash"]

View File

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

View File

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

View File

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

View File

@@ -36,5 +36,7 @@ services:
- ./metadata:/metadata
## Any other cache content.
- ./cache:/cache
## Where to store binary blob data (scene covers, images)
- ./blobs:/blobs
## Where to store generated content (screenshots,previews,transcodes,sprites)
- ./generated:/generated

View File

@@ -25,7 +25,17 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW. For examp
### macOS
1. If you don't have it already, install the [Homebrew package manager](https://brew.sh).
2. Install dependencies: `brew install go git yarn gcc make`
2. Install dependencies: `brew install go git yarn gcc make node ffmpeg`
### Linux
#### Arch Linux
1. Install dependencies: `sudo pacman -S go git yarn gcc make nodejs ffmpeg --needed`
#### Ubuntu
1. Install dependencies: `sudo apt-get install golang git gcc nodejs ffmpeg -y`
2. Enable corepack in Node.js: `corepack enable`
3. Install yarn: `corepack prepare yarn@stable --activate`
## Commands
@@ -39,13 +49,41 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW. For examp
* `make fmt` - Run `go fmt`
* `make it` - Run the unit and integration tests
* `make validate` - Run all of the tests and checks required to submit a PR
* `make server-start` - Runs an instance of the server in the `.local` directory.
* `make server-clean` - Removes the `.local` directory and all of its contents.
* `make ui-start` - Runs the UI in development mode. Requires a running stash server to connect to. Stash server port can be changed from the default of `9999` using environment variable `VITE_APP_PLATFORM_PORT`. UI runs on port `3000` or the next available port.
## Local development quickstart
1. Run `make pre-ui` to install UI dependencies
2. Run `make generate` to create generated files
3. In one terminal, run `make server-start` to run the server code
4. In a separate terminal, run `make ui-start` to run the UI in development mode
5. Open the UI in a browser `http://localhost:3000/`
Changes to the UI code can be seen by reloading the browser page.
Changes to the server code requires a restart (`CTRL-C` in the server terminal).
On first launch:
1. On the "Stash Setup Wizard" screen, choose a directory with some files to test with
2. Press "Next" to use the default locations for the database and generated content
3. Press the "Confirm" and "Finish" buttons to get into the UI
4. On the side menu, navigate to "Tasks -> Library -> Scan" and press the "Scan" button
5. You're all set! Set any other configurations you'd like and test your code changes.
To start fresh with new configuration:
1. Stop the server (`CTRL-C` in the server terminal)
2. Run `make server-clean` to clear all config, database, and generated files (under `.local/`)
3. Run `make server-start` to restart the server
4. Follow the "On first launch" steps above
## 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

29
go.mod
View File

@@ -15,7 +15,7 @@ require (
github.com/golang-migrate/migrate/v4 v4.15.0-beta.1
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.2
github.com/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/jmoiron/sqlx v1.3.1
github.com/json-iterator/go v1.1.12
@@ -30,16 +30,16 @@ require (
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.7.1
github.com/tidwall/gjson v1.9.3
github.com/tidwall/pretty v1.2.0 // indirect
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-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/image v0.5.0
golang.org/x/net v0.7.0
golang.org/x/sys v0.5.0
golang.org/x/term v0.5.0
golang.org/x/text v0.7.0
golang.org/x/tools v0.1.12 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.4.0
@@ -57,7 +57,9 @@ require (
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
github.com/vektah/gqlparser/v2 v2.4.2
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
github.com/zencoder/go-dash/v3 v3.0.2
gopkg.in/guregu/null.v4 v4.0.0
)
@@ -82,9 +84,9 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matryer/moq v0.2.6 // indirect
github.com/matryer/moq v0.2.3 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
@@ -99,13 +101,14 @@ require (
github.com/stretchr/objx v0.2.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/urfave/cli/v2 v2.4.0 // indirect
github.com/urfave/cli/v2 v2.8.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
go 1.17
go 1.19

47
go.sum
View File

@@ -396,8 +396,9 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
@@ -555,9 +556,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/matryer/moq v0.2.3 h1:Q06vEqnBYjjfx5KKgHfYRKE/lvlRu+Nj+xodG4YdHnU=
github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
github.com/matryer/moq v0.2.6 h1:X4+LF09udTsi2P+Z+1UhSb4p3K8IyiF7KSNFDR9M3M0=
github.com/matryer/moq v0.2.6/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -591,8 +591,9 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -700,7 +701,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=
@@ -735,8 +735,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tidwall/gjson v1.9.3 h1:hqzS9wAHMO+KVBBkLxYdkEeeFHuqr95GfClRLKlgK0E=
@@ -749,22 +750,27 @@ github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
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/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
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=
github.com/vektah/gqlparser/v2 v2.4.2 h1:29TGc6QmhEUq5fll+2FPoTmhUhR65WEKN4VK/jo0OlM=
github.com/vektah/gqlparser/v2 v2.4.2/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/vektra/mockery/v2 v2.10.0 h1:MiiQWxwdq7/ET6dCXLaJzSGEN17k758H7JHS9kOdiks=
github.com/vektra/mockery/v2 v2.10.0/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e h1:GruPsb+44XvYAzuAgJW1d1WHqmcI73L2XSjsbx/eJZw=
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e/go.mod h1:wA8kQ8WFipMciY9WcWzqQgZordm/P7l8IZdvx1crwmc=
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -775,6 +781,8 @@ github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/zencoder/go-dash/v3 v3.0.2 h1:oP1+dOh+Gp57PkvdCyMfbHtrHaxfl3w4kR3KBBbuqQE=
github.com/zencoder/go-dash/v3 v3.0.2/go.mod h1:30R5bKy1aUYY45yesjtZ9l8trNc2TwNqbS17WVQmCzk=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
@@ -839,8 +847,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -924,8 +932,9 @@ 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-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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=
@@ -1049,16 +1058,16 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs=
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/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=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1067,8 +1076,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1336,8 +1346,9 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=

View File

@@ -34,6 +34,8 @@ models:
title:
resolver: true
# autobind on config causes generation issues
BlobsStorageType:
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
StashConfig:
model: github.com/stashapp/stash/internal/manager/config.StashConfig
StashConfigInput:

View File

@@ -10,6 +10,8 @@ fragment ConfigGeneralData on ConfigGeneralResult {
metadataPath
scrapersPath
cachePath
blobsPath
blobsStorage
calculateMD5
videoFileNamingAlgorithm
parallelTasks
@@ -19,6 +21,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
previewExcludeStart
previewExcludeEnd
previewPreset
transcodeHardwareAcceleration
maxTranscodeSize
maxStreamingTranscodeSize
writeImageThumbnails
@@ -31,6 +34,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
logLevel
logAccess
createGalleriesFromFolders
galleryCoverRegex
videoExtensions
imageExtensions
galleryExtensions
@@ -46,6 +50,11 @@ fragment ConfigGeneralData on ConfigGeneralResult {
api_key
}
pythonPath
transcodeInputArgs
transcodeOutputArgs
liveTranscodeInputArgs
liveTranscodeOutputArgs
drawFunscriptHeatmapRange
}
fragment ConfigInterfaceData on ConfigInterfaceResult {
@@ -63,6 +72,8 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
showStudioAsText
css
cssEnabled
javascript
javascriptEnabled
customLocales
customLocalesEnabled
language
@@ -122,6 +133,7 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
scan {
useFileMetadata
stripFileExtension
scanGenerateCovers
scanGeneratePreviews
scanGenerateImagePreviews
scanGenerateSprites
@@ -150,6 +162,7 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
}
generate {
covers
sprites
previews
imagePreviews

View File

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

View File

@@ -0,0 +1,9 @@
fragment GalleryChapterData on GalleryChapter {
id
title
image_index
gallery {
id
}
}

View File

@@ -4,7 +4,7 @@ fragment SlimGalleryData on Gallery {
date
url
details
rating
rating100
organized
files {
...GalleryFileData
@@ -22,6 +22,11 @@ fragment SlimGalleryData on Gallery {
thumbnail
}
}
chapters {
id
title
image_index
}
studio {
id
name

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
mutation GalleryChapterCreate(
$title: String!,
$image_index: Int!,
$gallery_id: ID!) {
galleryChapterCreate(input: {
title: $title,
image_index: $image_index,
gallery_id: $gallery_id,
}) {
...GalleryChapterData
}
}
mutation GalleryChapterUpdate(
$id: ID!,
$title: String!,
$image_index: Int!,
$gallery_id: ID!) {
galleryChapterUpdate(input: {
id: $id,
title: $title,
image_index: $image_index,
gallery_id: $gallery_id,
}) {
...GalleryChapterData
}
}
mutation GalleryChapterDestroy($id: ID!) {
galleryChapterDestroy(id: $id)
}

View File

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

View File

@@ -0,0 +1,7 @@
mutation MigrateSceneScreenshots($input: MigrateSceneScreenshotsInput!) {
migrateSceneScreenshots(input: $input)
}
mutation MigrateBlobs($input: MigrateBlobsInput!) {
migrateBlobs(input: $input)
}

View File

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

View File

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

View File

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

View File

@@ -96,7 +96,7 @@ type Query {
"""Scrapes a complete performer record based on a URL"""
scrapePerformerURL(url: String!): ScrapedPerformer
"""Scrapes a complete performer record based on a URL"""
"""Scrapes a complete scene record based on a URL"""
scrapeSceneURL(url: String!): ScrapedScene
"""Scrapes a complete gallery record based on a URL"""
scrapeGalleryURL(url: String!): ScrapedGallery
@@ -144,6 +144,10 @@ type Query {
# Get everything
allScenes: [Scene!]!
allSceneMarkers: [SceneMarker!]!
allImages: [Image!]!
allGalleries: [Gallery!]!
allPerformers: [Performer!]!
allStudios: [Studio!]!
allMovies: [Movie!]!
@@ -155,14 +159,16 @@ type Query {
version: Version!
# LatestVersion
latestversion: ShortVersion!
latestversion: LatestVersion!
}
type Mutation {
setup(input: SetupInput!): Boolean!
migrate(input: MigrateInput!): Boolean!
sceneCreate(input: SceneCreateInput!): Scene
sceneUpdate(input: SceneUpdateInput!): Scene
sceneMerge(input: SceneMergeInput!): Scene
bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!]
sceneDestroy(input: SceneDestroyInput!): Boolean!
scenesDestroy(input: ScenesDestroyInput!): Boolean!
@@ -175,6 +181,12 @@ type Mutation {
"""Resets the o-counter for a scene to 0. Returns the new value"""
sceneResetO(id: ID!): Int!
"""Sets the resume time point (if provided) and adds the provided duration to the scene's play duration"""
sceneSaveActivity(id: ID!, resume_time: Float, playDuration: Float): Boolean!
"""Increments the play count for the scene. Returns the new play count value."""
sceneIncrementPlayCount(id: ID!): Int!
"""Generates screenshot at specified time in seconds. Leave empty to generate default screenshot"""
sceneGenerateScreenshot(id: ID!, at: Float): String!
@@ -182,6 +194,8 @@ type Mutation {
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean!
sceneAssignFile(input: AssignSceneFileInput!): Boolean!
imageUpdate(input: ImageUpdateInput!): Image
bulkImageUpdate(input: BulkImageUpdateInput!): [Image!]
imageDestroy(input: ImageDestroyInput!): Boolean!
@@ -204,6 +218,10 @@ type Mutation {
addGalleryImages(input: GalleryAddInput!): Boolean!
removeGalleryImages(input: GalleryRemoveInput!): Boolean!
galleryChapterCreate(input: GalleryChapterCreateInput!): GalleryChapter
galleryChapterUpdate(input: GalleryChapterUpdateInput!): GalleryChapter
galleryChapterDestroy(id: ID!): Boolean!
performerCreate(input: PerformerCreateInput!): Performer
performerUpdate(input: PerformerUpdateInput!): Performer
performerDestroy(input: PerformerDestroyInput!): Boolean!
@@ -227,6 +245,14 @@ type Mutation {
tagsDestroy(ids: [ID!]!): Boolean!
tagsMerge(input: TagsMergeInput!): Tag
"""Moves the given files to the given destination. Returns true if successful.
Either the destination_folder or destination_folder_id must be provided. If both are provided, the destination_folder_id takes precedence.
Destination folder must be a subfolder of one of the stash library paths.
If provided, destination_basename must be a valid filename with an extension that
matches one of the media extensions.
Creates folder hierarchy if needed.
"""
moveFiles(input: MoveFilesInput!): Boolean!
deleteFiles(ids: [ID!]!): Boolean!
# Saved filters
@@ -269,8 +295,16 @@ type Mutation {
metadataClean(input: CleanMetadataInput!): ID!
"""Identifies scenes using scrapers. Returns the job ID"""
metadataIdentify(input: IdentifyMetadataInput!): ID!
"""Migrate generated files for the current hash naming"""
migrateHashNaming: ID!
"""Migrates legacy scene screenshot files into the blob storage"""
migrateSceneScreenshots(input: MigrateSceneScreenshotsInput!): ID!
"""Migrates blobs from the old storage system to the current one"""
migrateBlobs(input: MigrateBlobsInput!): ID!
"""Anonymise the database in a separate file. Optionally returns a link to download the database file"""
anonymiseDatabase(input: AnonymiseDatabaseInput!): String
"""Reload scrapers"""
reloadScrapers: Boolean!

View File

@@ -6,6 +6,10 @@ input SetupInput {
databaseFile: String!
"""Empty to indicate default"""
generatedLocation: String!
"""Empty to indicate default"""
cacheLocation: String!
"""Empty to indicate database storage for blobs"""
blobsLocation: String!
}
enum StreamingResolutionEnum {
@@ -32,6 +36,13 @@ enum HashAlgorithm {
"oshash", OSHASH
}
enum BlobsStorageType {
# blobs are stored in the database
"Database", DATABASE
# blobs are stored in the filesystem under the configured blobs directory
"Filesystem", FILESYSTEM
}
input ConfigGeneralInput {
"""Array of file paths to content"""
stashes: [StashConfigInput!]
@@ -47,6 +58,10 @@ input ConfigGeneralInput {
scrapersPath: String
"""Path to cache"""
cachePath: String
"""Path to blobs - required for filesystem blob storage"""
blobsPath: String
"""Where to store blobs"""
blobsStorage: BlobsStorageType
"""Whether to calculate MD5 checksums for scene video files"""
calculateMD5: Boolean
"""Hash algorithm to use for generated file naming"""
@@ -65,10 +80,30 @@ input ConfigGeneralInput {
previewExcludeEnd: String
"""Preset when generating preview"""
previewPreset: PreviewPreset
"""Transcode Hardware Acceleration"""
transcodeHardwareAcceleration: Boolean
"""Max generated transcode size"""
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""ffmpeg transcode input args - injected before input file
These are applied to generated transcodes (previews and transcodes)"""
transcodeInputArgs: [String!]
"""ffmpeg transcode output args - injected before output file
These are applied to generated transcodes (previews and transcodes)"""
transcodeOutputArgs: [String!]
"""ffmpeg stream input args - injected before input file
These are applied when live transcoding"""
liveTranscodeInputArgs: [String!]
"""ffmpeg stream output args - injected before output file
These are applied when live transcoding"""
liveTranscodeOutputArgs: [String!]
"""whether to include range in generated funscript heatmaps"""
drawFunscriptHeatmapRange: Boolean
"""Write image thumbnails to disk when generating on the fly"""
writeImageThumbnails: Boolean
"""Username"""
@@ -89,6 +124,8 @@ input ConfigGeneralInput {
logAccess: Boolean
"""True if galleries should be created from folders with images"""
createGalleriesFromFolders: Boolean
"""Regex used to identify images as gallery covers"""
galleryCoverRegex: String
"""Array of video file extensions"""
videoExtensions: [String!]
"""Array of image file extensions"""
@@ -130,6 +167,10 @@ type ConfigGeneralResult {
scrapersPath: String!
"""Path to cache"""
cachePath: String!
"""Path to blobs - required for filesystem blob storage"""
blobsPath: String!
"""Where to store blobs"""
blobsStorage: BlobsStorageType!
"""Whether to calculate MD5 checksums for scene video files"""
calculateMD5: Boolean!
"""Hash algorithm to use for generated file naming"""
@@ -148,10 +189,30 @@ type ConfigGeneralResult {
previewExcludeEnd: String!
"""Preset when generating preview"""
previewPreset: PreviewPreset!
"""Transcode Hardware Acceleration"""
transcodeHardwareAcceleration: Boolean!
"""Max generated transcode size"""
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""ffmpeg transcode input args - injected before input file
These are applied to generated transcodes (previews and transcodes)"""
transcodeInputArgs: [String!]!
"""ffmpeg transcode output args - injected before output file
These are applied to generated transcodes (previews and transcodes)"""
transcodeOutputArgs: [String!]!
"""ffmpeg stream input args - injected before input file
These are applied when live transcoding"""
liveTranscodeInputArgs: [String!]!
"""ffmpeg stream output args - injected before output file
These are applied when live transcoding"""
liveTranscodeOutputArgs: [String!]!
"""whether to include range in generated funscript heatmaps"""
drawFunscriptHeatmapRange: Boolean!
"""Write image thumbnails to disk when generating on the fly"""
writeImageThumbnails: Boolean!
"""API Key"""
@@ -180,6 +241,8 @@ type ConfigGeneralResult {
galleryExtensions: [String!]!
"""True if galleries should be created from folders with images"""
createGalleriesFromFolders: Boolean!
"""Regex used to identify images as gallery covers"""
galleryCoverRegex: String!
"""Array of file regexp to exclude from Video Scans"""
excludes: [String!]!
"""Array of file regexp to exclude from Image Scans"""
@@ -264,6 +327,10 @@ input ConfigInterfaceInput {
css: String
cssEnabled: Boolean
"""Custom Javascript"""
javascript: String
javascriptEnabled: Boolean
"""Custom Locales"""
customLocales: String
customLocalesEnabled: Boolean
@@ -330,6 +397,10 @@ type ConfigInterfaceResult {
css: String
cssEnabled: Boolean
"""Custom Javascript"""
javascript: String
javascriptEnabled: Boolean
"""Custom Locales"""
customLocales: String
customLocalesEnabled: Boolean

View File

@@ -94,4 +94,16 @@ type GalleryFile implements BaseFile {
created_at: Time!
updated_at: Time!
}
}
input MoveFilesInput {
ids: [ID!]!
"""valid for single or multiple file ids"""
destination_folder: String
"""valid for single or multiple file ids"""
destination_folder_id: ID
"""valid only for single file id. If empty, existing basename is used"""
destination_basename: String
}

View File

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

@@ -0,0 +1,26 @@
type GalleryChapter {
id: ID!
gallery: Gallery!
title: String!
image_index: Int!
created_at: Time!
updated_at: Time!
}
input GalleryChapterCreateInput {
gallery_id: ID!
title: String!
image_index: Int!
}
input GalleryChapterUpdateInput {
id: ID!
gallery_id: ID!
title: String!
image_index: Int!
}
type FindGalleryChaptersResultType {
count: Int!
chapters: [GalleryChapter!]!
}

View File

@@ -7,7 +7,10 @@ type Gallery {
url: String
date: String
details: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean!
created_at: Time!
updated_at: Time!
@@ -16,6 +19,7 @@ type Gallery {
files: [GalleryFile!]!
folder: Folder
chapters: [GalleryChapter!]!
scenes: [Scene!]!
studio: Studio
image_count: Int!
@@ -23,7 +27,7 @@ type Gallery {
performers: [Performer!]!
"""The images in the gallery"""
images: [Image!]! # Resolver
images: [Image!]! @deprecated(reason: "Use findImages")
cover: Image
}
@@ -32,7 +36,10 @@ input GalleryCreateInput {
url: String
date: String
details: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
scene_ids: [ID!]
studio_id: ID
@@ -47,7 +54,10 @@ input GalleryUpdateInput {
url: String
date: String
details: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
scene_ids: [ID!]
studio_id: ID
@@ -63,7 +73,10 @@ input BulkGalleryUpdateInput {
url: String
date: String
details: String
rating: Int
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
scene_ids: BulkUpdateIds
studio_id: ID

View File

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

View File

@@ -1,6 +1,7 @@
scalar Upload
input GenerateMetadataInput {
covers: Boolean
sprites: Boolean
previews: Boolean
imagePreviews: Boolean
@@ -37,6 +38,7 @@ input GeneratePreviewOptionsInput {
}
type GenerateMetadataOptions {
covers: Boolean
sprites: Boolean
previews: Boolean
imagePreviews: Boolean
@@ -84,6 +86,8 @@ input ScanMetadataInput {
"""Strip file extension from title"""
stripFileExtension: Boolean @deprecated(reason: "Not implemented")
"""Generate covers during scan"""
scanGenerateCovers: Boolean
"""Generate previews during scan"""
scanGeneratePreviews: Boolean
"""Generate image previews during scan"""
@@ -101,9 +105,11 @@ input ScanMetadataInput {
type ScanMetadataOptions {
"""Set name, date, details from metadata (if present)"""
useFileMetadata: Boolean!
useFileMetadata: Boolean! @deprecated(reason: "Not implemented")
"""Strip file extension from title"""
stripFileExtension: Boolean!
stripFileExtension: Boolean! @deprecated(reason: "Not implemented")
"""Generate covers during scan"""
scanGenerateCovers: Boolean!
"""Generate previews during scan"""
scanGeneratePreviews: Boolean!
"""Generate image previews during scan"""
@@ -263,6 +269,10 @@ input BackupDatabaseInput {
download: Boolean
}
input AnonymiseDatabaseInput {
download: Boolean
}
enum SystemStatusEnum {
SETUP
NEEDS_MIGRATION

View File

@@ -0,0 +1,11 @@
input MigrateSceneScreenshotsInput {
# if true, delete screenshot files after migrating
deleteFiles: Boolean
# if true, overwrite existing covers with the covers from the screenshots directory
overwriteExisting: Boolean
}
input MigrateBlobsInput {
# if true, delete blob data from old storage system
deleteOld: Boolean
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,51 +0,0 @@
package api
import (
"strconv"
"strings"
)
type byteRange struct {
Start int64
End *int64
RawString string
}
func createByteRange(s string) byteRange {
// strip bytes=
r := strings.TrimPrefix(s, "bytes=")
e := strings.Split(r, "-")
ret := byteRange{
RawString: s,
}
if len(e) > 0 {
ret.Start, _ = strconv.ParseInt(e[0], 10, 64)
}
if len(e) > 1 && e[1] != "" {
end, _ := strconv.ParseInt(e[1], 10, 64)
ret.End = &end
}
return ret
}
func (r byteRange) toHeaderValue(fileLength int64) string {
if r.End == nil {
return ""
}
end := *r.End
return "bytes " + strconv.FormatInt(r.Start, 10) + "-" + strconv.FormatInt(end, 10) + "/" + strconv.FormatInt(fileLength, 10)
}
func (r byteRange) apply(bytes []byte) []byte {
if r.End == nil {
return bytes[r.Start:]
}
end := *r.End + 1
if int(end) > len(bytes) {
end = int64(len(bytes))
}
return bytes[r.Start:end]
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,6 +47,9 @@ func (r *Resolver) scraperCache() *scraper.Cache {
func (r *Resolver) Gallery() GalleryResolver {
return &galleryResolver{r}
}
func (r *Resolver) GalleryChapter() GalleryChapterResolver {
return &galleryChapterResolver{r}
}
func (r *Resolver) Mutation() MutationResolver {
return &mutationResolver{r}
}
@@ -83,6 +86,7 @@ type queryResolver struct{ *Resolver }
type subscriptionResolver struct{ *Resolver }
type galleryResolver struct{ *Resolver }
type galleryChapterResolver struct{ *Resolver }
type performerResolver struct{ *Resolver }
type sceneResolver struct{ *Resolver }
type sceneMarkerResolver struct{ *Resolver }
@@ -95,8 +99,12 @@ func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) err
return txn.WithTxn(ctx, r.txnManager, fn)
}
func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithReadTxn(ctx, r.txnManager, fn)
}
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.Wall(ctx, q)
return err
}); err != nil {
@@ -106,7 +114,7 @@ func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*model
}
func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models.Scene, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.Wall(ctx, q)
return err
}); err != nil {
@@ -117,7 +125,7 @@ func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models
}
func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *string) (ret []*models.MarkerStringsResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.GetMarkerStrings(ctx, q, sort)
return err
}); err != nil {
@@ -129,7 +137,7 @@ func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *stri
func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
var ret StatsResultType
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
repo := r.repository
scenesQB := repo.Scene
imageQB := repo.Image
@@ -180,19 +188,22 @@ func (r *queryResolver) Version(ctx context.Context) (*Version, error) {
}, nil
}
// Latestversion returns the latest git shorthash commit.
func (r *queryResolver) Latestversion(ctx context.Context) (*ShortVersion, error) {
ver, url, err := GetLatestVersion(ctx, true)
if err == nil {
logger.Infof("Retrieved latest hash: %s", ver)
} else {
logger.Errorf("Error while retrieving latest hash: %s", err)
func (r *queryResolver) Latestversion(ctx context.Context) (*LatestVersion, error) {
latestRelease, err := GetLatestRelease(ctx)
if err != nil {
if !errors.Is(err, context.Canceled) {
logger.Errorf("Error while retrieving latest version: %v", err)
}
return nil, err
}
logger.Infof("Retrieved latest version: %s (%s)", latestRelease.Version, latestRelease.ShortHash)
return &ShortVersion{
Shorthash: ver,
URL: url,
}, err
return &LatestVersion{
Version: latestRelease.Version,
Shorthash: latestRelease.ShortHash,
ReleaseDate: latestRelease.Date,
URL: latestRelease.Url,
}, nil
}
// Get scene marker tags which show up under the video.
@@ -205,7 +216,7 @@ func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([
var keys []int
tags := make(map[int]*SceneMarkerTag)
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneMarkers, err := r.repository.SceneMarker.FindBySceneID(ctx, sceneID)
if err != nil {
return err

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/image"
@@ -73,7 +74,7 @@ func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*Fol
var ret *file.Folder
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Folder.Find(ctx, *obj.FolderID)
@@ -123,8 +124,9 @@ func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery)
return nil, nil
}
// Images is deprecated, slow and shouldn't be used
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret []*models.Image, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
// #2376 - sort images by path
@@ -143,25 +145,10 @@ func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret
}
func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *models.Image, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
// doing this via Query is really slow, so stick with FindByGalleryID
imgs, err := r.repository.Image.FindByGalleryID(ctx, obj.ID)
if err != nil {
return err
}
if len(imgs) > 0 {
ret = imgs[0]
}
for _, img := range imgs {
if image.IsCover(img) {
ret = img
break
}
}
return nil
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
// Find cover image first
ret, err = image.FindGalleryCover(ctx, r.repository.Image, obj.ID, config.GetInstance().GetGalleryCoverRegex())
return err
}); err != nil {
return nil, err
}
@@ -179,7 +166,7 @@ func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*strin
func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (string, error) {
if !obj.Files.PrimaryLoaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPrimaryFile(ctx, r.repository.File)
}); err != nil {
return "", err
@@ -189,9 +176,21 @@ func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (st
return obj.PrimaryChecksum(), nil
}
func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *galleryResolver) Rating100(ctx context.Context, obj *models.Gallery) (*int, error) {
return obj.Rating, nil
}
func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) {
if !obj.SceneIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadSceneIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
@@ -213,7 +212,7 @@ func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
@@ -227,7 +226,7 @@ func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) {
if !obj.PerformerIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPerformerIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
@@ -240,7 +239,7 @@ func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (
}
func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (ret int, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Image.CountByGalleryID(ctx, obj.ID)
return err
@@ -250,3 +249,14 @@ func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (
return ret, nil
}
func (r *galleryResolver) Chapters(ctx context.Context, obj *models.Gallery) (ret []*models.GalleryChapter, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.GalleryChapter.FindByGalleryID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -0,0 +1,32 @@
package api
import (
"context"
"time"
"github.com/stashapp/stash/pkg/models"
)
func (r *galleryChapterResolver) Gallery(ctx context.Context, obj *models.GalleryChapter) (ret *models.Gallery, err error) {
if !obj.GalleryID.Valid {
panic("Invalid gallery id")
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
galleryID := int(obj.GalleryID.Int64)
ret, err = r.repository.Gallery.Find(ctx, galleryID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *galleryChapterResolver) CreatedAt(ctx context.Context, obj *models.GalleryChapter) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *galleryChapterResolver) UpdatedAt(ctx context.Context, obj *models.GalleryChapter) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,8 @@ func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (
obj.Files.SetPrimary(ret)
return ret, nil
} else {
_ = obj.LoadPrimaryFile(ctx, r.repository.File)
}
return nil, nil
@@ -139,6 +141,18 @@ func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoF
return ret, nil
}
func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *sceneResolver) Rating100(ctx context.Context, obj *models.Scene) (*int, error) {
return obj.Rating, nil
}
func resolveFingerprints(f *file.BaseFile) []*Fingerprint {
ret := make([]*Fingerprint, len(f.Fingerprints))
@@ -165,13 +179,13 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
config := manager.GetInstance().Config
builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
builder.APIKey = config.GetAPIKey()
screenshotPath := builder.GetScreenshotURL(obj.UpdatedAt)
previewPath := builder.GetStreamPreviewURL()
streamPath := builder.GetStreamURL().String()
streamPath := builder.GetStreamURL(config.GetAPIKey()).String()
webpPath := builder.GetStreamPreviewImageURL()
vttPath := builder.GetSpriteVTTURL()
spritePath := builder.GetSpriteURL()
objHash := obj.GetHash(config.GetVideoFileNamingAlgorithm())
vttPath := builder.GetSpriteVTTURL(objHash)
spritePath := builder.GetSpriteURL(objHash)
chaptersVttPath := builder.GetChaptersVTTURL()
funscriptPath := builder.GetFunscriptURL()
captionBasePath := builder.GetCaptionURL()
@@ -192,7 +206,7 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
}
func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (ret []*models.SceneMarker, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.FindBySceneID(ctx, obj.ID)
return err
}); err != nil {
@@ -211,7 +225,7 @@ func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []
return nil, nil
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.File.GetCaptions(ctx, primaryFile.Base().ID)
return err
}); err != nil {
@@ -223,7 +237,7 @@ func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
if !obj.GalleryIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadGalleryIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
@@ -245,7 +259,7 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) {
if !obj.Movies.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
return obj.LoadMovies(ctx, qb)
@@ -276,7 +290,7 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
@@ -290,7 +304,7 @@ func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*mod
func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) {
if !obj.PerformerIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPerformerIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
@@ -313,7 +327,7 @@ func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID {
}
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadStashIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
@@ -357,9 +371,9 @@ func (r *sceneResolver) SceneStreams(ctx context.Context, obj *models.Scene) ([]
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
builder.APIKey = config.GetAPIKey()
apiKey := config.GetAPIKey()
return manager.GetSceneStreamPaths(obj, builder.GetStreamURL(), config.GetMaxStreamingTranscodeSize())
return manager.GetSceneStreamPaths(obj, builder.GetStreamURL(apiKey), config.GetMaxStreamingTranscodeSize())
}
func (r *sceneResolver) Interactive(ctx context.Context, obj *models.Scene) (bool, error) {

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"path/filepath"
"regexp"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
@@ -58,7 +59,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
validateDir := func(key string, value string, optional bool) error {
if err := checkConfigOverride(config.Metadata); err != nil {
if err := checkConfigOverride(key); err != nil {
return err
}
@@ -122,6 +123,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
c.Set(config.Metadata, input.MetadataPath)
}
refreshStreamManager := false
existingCachePath := c.GetCachePath()
if input.CachePath != nil && existingCachePath != *input.CachePath {
if err := validateDir(config.Cache, *input.CachePath, true); err != nil {
@@ -129,6 +131,29 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
c.Set(config.Cache, input.CachePath)
refreshStreamManager = true
}
refreshBlobStorage := false
existingBlobsPath := c.GetBlobsPath()
if input.BlobsPath != nil && existingBlobsPath != *input.BlobsPath {
if err := validateDir(config.BlobsPath, *input.BlobsPath, true); err != nil {
return makeConfigGeneralResult(), err
}
c.Set(config.BlobsPath, input.BlobsPath)
refreshBlobStorage = true
}
if input.BlobsStorage != nil && *input.BlobsStorage != c.GetBlobsStorage() {
if *input.BlobsStorage == config.BlobStorageTypeFilesystem && c.GetBlobsPath() == "" {
return makeConfigGeneralResult(), fmt.Errorf("blobs path must be set when using filesystem storage")
}
// TODO - migrate between systems
c.Set(config.BlobsStorage, input.BlobsStorage)
refreshBlobStorage = true
}
if input.VideoFileNamingAlgorithm != nil && *input.VideoFileNamingAlgorithm != c.GetVideoFileNamingAlgorithm() {
@@ -178,6 +203,9 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
c.Set(config.PreviewPreset, input.PreviewPreset.String())
}
if input.TranscodeHardwareAcceleration != nil {
c.Set(config.TranscodeHardwareAcceleration, *input.TranscodeHardwareAcceleration)
}
if input.MaxTranscodeSize != nil {
c.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
}
@@ -190,6 +218,16 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
c.Set(config.WriteImageThumbnails, *input.WriteImageThumbnails)
}
if input.GalleryCoverRegex != nil {
_, err := regexp.Compile(*input.GalleryCoverRegex)
if err != nil {
return makeConfigGeneralResult(), fmt.Errorf("Gallery cover regex '%v' invalid, '%v'", *input.GalleryCoverRegex, err.Error())
}
c.Set(config.GalleryCoverRegex, *input.GalleryCoverRegex)
}
if input.Username != nil {
c.Set(config.Username, input.Username)
}
@@ -227,10 +265,22 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
if input.Excludes != nil {
for _, r := range input.Excludes {
_, err := regexp.Compile(r)
if err != nil {
return makeConfigGeneralResult(), fmt.Errorf("video exclusion pattern '%v' invalid: %w", r, err)
}
}
c.Set(config.Exclude, input.Excludes)
}
if input.ImageExcludes != nil {
for _, r := range input.ImageExcludes {
_, err := regexp.Compile(r)
if err != nil {
return makeConfigGeneralResult(), fmt.Errorf("image/gallery exclusion pattern '%v' invalid: %w", r, err)
}
}
c.Set(config.ImageExclude, input.ImageExcludes)
}
@@ -280,6 +330,23 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
c.Set(config.PythonPath, input.PythonPath)
}
if input.TranscodeInputArgs != nil {
c.Set(config.TranscodeInputArgs, input.TranscodeInputArgs)
}
if input.TranscodeOutputArgs != nil {
c.Set(config.TranscodeOutputArgs, input.TranscodeOutputArgs)
}
if input.LiveTranscodeInputArgs != nil {
c.Set(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs)
}
if input.LiveTranscodeOutputArgs != nil {
c.Set(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs)
}
if input.DrawFunscriptHeatmapRange != nil {
c.Set(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
}
if err := c.Write(); err != nil {
return makeConfigGeneralResult(), err
}
@@ -288,6 +355,12 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
if refreshScraperCache {
manager.GetInstance().RefreshScraperCache()
}
if refreshStreamManager {
manager.GetInstance().RefreshStreamManager()
}
if refreshBlobStorage {
manager.GetInstance().SetBlobStoreOptions()
}
return makeConfigGeneralResult(), nil
}
@@ -365,6 +438,12 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
setBool(config.CSSEnabled, input.CSSEnabled)
if input.Javascript != nil {
c.SetJavascript(*input.Javascript)
}
setBool(config.JavascriptEnabled, input.JavascriptEnabled)
if input.CustomLocales != nil {
c.SetCustomLocales(*input.CustomLocales)
}
@@ -445,6 +524,12 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input ConfigSc
}
if input.ExcludeTagPatterns != nil {
for _, r := range input.ExcludeTagPatterns {
_, err := regexp.Compile(r)
if err != nil {
return makeConfigScrapingResult(), fmt.Errorf("tag exclusion pattern '%v' invalid: %w", r, err)
}
}
c.Set(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns)
}
@@ -470,6 +555,8 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input ConfigDe
}
if input.Scan != nil {
// if input.Scan is used then ScanMetadataOptions is included in the config file
// this causes the values to not be read correctly
c.Set(config.DefaultScanSettings, input.Scan.ScanMetadataOptions)
}

View File

@@ -3,11 +3,145 @@ package api
import (
"context"
"fmt"
"strconv"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput) (bool, error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
fileStore := r.repository.File
folderStore := r.repository.Folder
mover := file.NewMover(fileStore, folderStore)
mover.RegisterHooks(ctx, r.txnManager)
var (
folder *file.Folder
basename string
)
fileIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return fmt.Errorf("converting file ids: %w", err)
}
switch {
case input.DestinationFolderID != nil:
var err error
folderID, err := strconv.Atoi(*input.DestinationFolderID)
if err != nil {
return fmt.Errorf("invalid folder id %s: %w", *input.DestinationFolderID, err)
}
folder, err = folderStore.Find(ctx, file.FolderID(folderID))
if err != nil {
return fmt.Errorf("finding destination folder: %w", err)
}
if folder == nil {
return fmt.Errorf("folder with id %d not found", input.DestinationFolderID)
}
if folder.ZipFileID != nil {
return fmt.Errorf("cannot move to %s, is in a zip file", folder.Path)
}
case input.DestinationFolder != nil:
folderPath := *input.DestinationFolder
// ensure folder path is within the library
if err := r.validateFolderPath(folderPath); err != nil {
return err
}
// get or create folder hierarchy
var err error
folder, err = file.GetOrCreateFolderHierarchy(ctx, folderStore, folderPath)
if err != nil {
return fmt.Errorf("getting or creating folder hierarchy: %w", err)
}
default:
return fmt.Errorf("must specify destination folder or path")
}
if input.DestinationBasename != nil {
// ensure only one file was supplied
if len(input.Ids) != 1 {
return fmt.Errorf("must specify one file when providing destination path")
}
basename = *input.DestinationBasename
}
// create the folder hierarchy in the filesystem if needed
if err := mover.CreateFolderHierarchy(folder.Path); err != nil {
return fmt.Errorf("creating folder hierarchy %s in filesystem: %w", folder.Path, err)
}
for _, fileIDInt := range fileIDs {
fileID := file.ID(fileIDInt)
f, err := fileStore.Find(ctx, fileID)
if err != nil {
return fmt.Errorf("finding file %d: %w", fileID, err)
}
// ensure that the file extension matches the existing file type
if basename != "" {
if err := r.validateFileExtension(f[0].Base().Basename, basename); err != nil {
return err
}
}
if err := mover.Move(ctx, f[0], folder, basename); err != nil {
return err
}
}
return nil
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) validateFolderPath(folderPath string) error {
paths := manager.GetInstance().Config.GetStashPaths()
if l := paths.GetStashFromDirPath(folderPath); l == nil {
return fmt.Errorf("folder path %s must be within a stash library path", folderPath)
}
return nil
}
func (r *mutationResolver) validateFileExtension(oldBasename, newBasename string) error {
c := manager.GetInstance().Config
if err := r.validateFileExtensionList(c.GetVideoExtensions(), oldBasename, newBasename); err != nil {
return err
}
if err := r.validateFileExtensionList(c.GetImageExtensions(), oldBasename, newBasename); err != nil {
return err
}
if err := r.validateFileExtensionList(c.GetGalleryExtensions(), oldBasename, newBasename); err != nil {
return err
}
return nil
}
func (r *mutationResolver) validateFileExtensionList(exts []string, oldBasename, newBasename string) error {
if fsutil.MatchExtension(oldBasename, exts) && !fsutil.MatchExtension(newBasename, exts) {
return fmt.Errorf("file extension for %s is inconsistent with old filename %s", newBasename, oldBasename)
}
return nil
}
func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret bool, err error) {
fileIDs, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {

View File

@@ -2,6 +2,7 @@ package api
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
@@ -10,6 +11,7 @@ import (
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
@@ -68,7 +70,13 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
d := models.NewDate(*input.Date)
newGallery.Date = &d
}
newGallery.Rating = input.Rating
if input.Rating100 != nil {
newGallery.Rating = input.Rating100
} else if input.Rating != nil {
rating := models.Rating5To100(*input.Rating)
newGallery.Rating = &rating
}
if input.StudioID != nil {
studioID, _ := strconv.Atoi(*input.StudioID)
@@ -177,8 +185,8 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
if input.Title != nil {
// ensure title is not empty
if *input.Title == "" {
return nil, errors.New("title must not be empty")
if *input.Title == "" && originalGallery.IsUserCreated() {
return nil, errors.New("title must not be empty for user-created galleries")
}
updatedGallery.Title = models.NewOptionalString(*input.Title)
@@ -187,7 +195,7 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
updatedGallery.Details = translator.optionalString(input.Details, "details")
updatedGallery.URL = translator.optionalString(input.URL, "url")
updatedGallery.Date = translator.optionalDate(input.Date, "date")
updatedGallery.Rating = translator.optionalInt(input.Rating, "rating")
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
@@ -262,8 +270,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
updatedGallery.Details = translator.optionalString(input.Details, "details")
updatedGallery.URL = translator.optionalString(input.URL, "url")
updatedGallery.Date = translator.optionalDate(input.Date, "date")
updatedGallery.Rating = translator.optionalInt(input.Rating, "rating")
updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
var err error
updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
@@ -484,3 +491,150 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input Galler
return true, nil
}
func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret *models.GalleryChapter, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.GalleryChapter.Find(ctx, id)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input GalleryChapterCreateInput) (*models.GalleryChapter, error) {
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return nil, err
}
var imageCount int
if err := r.withTxn(ctx, func(ctx context.Context) error {
imageCount, err = r.repository.Image.CountByGalleryID(ctx, galleryID)
return err
}); err != nil {
return nil, err
}
// Sanity Check of Index
if input.ImageIndex > imageCount || input.ImageIndex < 1 {
return nil, errors.New("Image # must greater than zero and in range of the gallery images")
}
currentTime := time.Now()
newGalleryChapter := models.GalleryChapter{
Title: input.Title,
ImageIndex: input.ImageIndex,
GalleryID: sql.NullInt64{Int64: int64(galleryID), Valid: galleryID != 0},
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
}
if err != nil {
return nil, err
}
ret, err := r.changeChapter(ctx, create, newGalleryChapter)
if err != nil {
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.GalleryChapterCreatePost, input, nil)
return r.getGalleryChapter(ctx, ret.ID)
}
func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input GalleryChapterUpdateInput) (*models.GalleryChapter, error) {
// Populate gallery chapter from the input
galleryChapterID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return nil, err
}
var imageCount int
if err := r.withTxn(ctx, func(ctx context.Context) error {
imageCount, err = r.repository.Image.CountByGalleryID(ctx, galleryID)
return err
}); err != nil {
return nil, err
}
// Sanity Check of Index
if input.ImageIndex > imageCount || input.ImageIndex < 1 {
return nil, errors.New("Image # must greater than zero and in range of the gallery images")
}
updatedGalleryChapter := models.GalleryChapter{
ID: galleryChapterID,
Title: input.Title,
ImageIndex: input.ImageIndex,
GalleryID: sql.NullInt64{Int64: int64(galleryID), Valid: galleryID != 0},
UpdatedAt: models.SQLiteTimestamp{Timestamp: time.Now()},
}
ret, err := r.changeChapter(ctx, update, updatedGalleryChapter)
if err != nil {
return nil, err
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.GalleryChapterUpdatePost, input, translator.getFields())
return r.getGalleryChapter(ctx, ret.ID)
}
func (r *mutationResolver) GalleryChapterDestroy(ctx context.Context, id string) (bool, error) {
chapterID, err := strconv.Atoi(id)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.GalleryChapter
chapter, err := qb.Find(ctx, chapterID)
if err != nil {
return err
}
if chapter == nil {
return fmt.Errorf("Chapter with id %d not found", chapterID)
}
return gallery.DestroyChapter(ctx, chapter, qb)
}); err != nil {
return false, err
}
r.hookExecutor.ExecutePostHooks(ctx, chapterID, plugin.GalleryChapterDestroyPost, id, nil)
return true, nil
}
func (r *mutationResolver) changeChapter(ctx context.Context, changeType int, changedChapter models.GalleryChapter) (*models.GalleryChapter, error) {
var galleryChapter *models.GalleryChapter
// Start the transaction and save the gallery chapter
var err = r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.GalleryChapter
var err error
switch changeType {
case create:
galleryChapter, err = qb.Create(ctx, changedChapter)
case update:
galleryChapter, err = qb.Update(ctx, changedChapter)
if err != nil {
return err
}
}
return err
})
return galleryChapter, err
}

View File

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

View File

@@ -156,3 +156,55 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
return nil, nil
}
func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input AnonymiseDatabaseInput) (*string, error) {
// if download is true, then backup to temporary file and return a link
download := input.Download != nil && *input.Download
mgr := manager.GetInstance()
database := mgr.Database
var outPath string
if download {
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
}
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "anonymous*.sqlite")
if err != nil {
return nil, err
}
outPath = f.Name()
f.Close()
} else {
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
if backupDirectoryPath != "" {
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
}
}
outPath = database.AnonymousDatabasePath(backupDirectoryPath)
}
err := database.Anonymise(outPath)
if err != nil {
logger.Errorf("Error anonymising database: %v", err)
return nil, err
}
if download {
downloadHash, err := mgr.DownloadStore.RegisterFile(outPath, "", false)
if err != nil {
return nil, fmt.Errorf("error registering file for download: %w", err)
}
logger.Debugf("Generated anonymised file %s with hash %s", outPath, downloadHash)
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
fn := filepath.Base(database.DatabaseBackupPath(""))
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
return &ret, nil
} else {
logger.Infof("Successfully anonymised database to: %s", outPath)
}
return nil, nil
}

View File

@@ -0,0 +1,40 @@
package api
import (
"context"
"strconv"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/task"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/utils"
)
func (r *mutationResolver) MigrateSceneScreenshots(ctx context.Context, input MigrateSceneScreenshotsInput) (string, error) {
db := manager.GetInstance().Database
t := &task.MigrateSceneScreenshotsJob{
ScreenshotsPath: manager.GetInstance().Paths.Generated.Screenshots,
Input: scene.MigrateSceneScreenshotsInput{
DeleteFiles: utils.IsTrue(input.DeleteFiles),
OverwriteExisting: utils.IsTrue(input.OverwriteExisting),
},
SceneRepo: db.Scene,
TxnManager: db,
}
jobID := manager.GetInstance().JobManager.Add(ctx, "Migrating scene screenshots to blobs...", t)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) MigrateBlobs(ctx context.Context, input MigrateBlobsInput) (string, error) {
db := manager.GetInstance().Database
t := &task.MigrateBlobsJob{
TxnManager: db,
BlobStore: db.Blobs,
Vacuumer: db,
DeleteOld: utils.IsTrue(input.DeleteOld),
}
jobID := manager.GetInstance().JobManager.Add(ctx, "Migrating blobs...", t)
return strconv.Itoa(jobID), nil
}

View File

@@ -76,9 +76,11 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
newMovie.Date = models.SQLiteDate{String: *input.Date, Valid: true}
}
if input.Rating != nil {
rating := int64(*input.Rating)
newMovie.Rating = sql.NullInt64{Int64: rating, Valid: true}
if input.Rating100 != nil {
newMovie.Rating = sql.NullInt64{Int64: int64(*input.Rating100), Valid: true}
} else if input.Rating != nil {
rating := models.Rating5To100(*input.Rating)
newMovie.Rating = sql.NullInt64{Int64: int64(rating), Valid: true}
}
if input.StudioID != nil {
@@ -109,7 +111,13 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
// update image table
if len(frontimageData) > 0 {
if err := qb.UpdateImages(ctx, movie.ID, frontimageData, backimageData); err != nil {
if err := qb.UpdateFrontImage(ctx, movie.ID, frontimageData); err != nil {
return err
}
}
if len(backimageData) > 0 {
if err := qb.UpdateBackImage(ctx, movie.ID, backimageData); err != nil {
return err
}
}
@@ -166,7 +174,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
updatedMovie.Aliases = translator.nullString(input.Aliases, "aliases")
updatedMovie.Duration = translator.nullInt64(input.Duration, "duration")
updatedMovie.Date = translator.sqliteDate(input.Date, "date")
updatedMovie.Rating = translator.nullInt64(input.Rating, "rating")
updatedMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100)
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedMovie.Director = translator.nullString(input.Director, "director")
updatedMovie.Synopsis = translator.nullString(input.Synopsis, "synopsis")
@@ -182,35 +190,15 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
}
// update image table
if frontImageIncluded || backImageIncluded {
if !frontImageIncluded {
frontimageData, err = qb.GetFrontImage(ctx, updatedMovie.ID)
if err != nil {
return err
}
}
if !backImageIncluded {
backimageData, err = qb.GetBackImage(ctx, updatedMovie.ID)
if err != nil {
return err
}
if frontImageIncluded {
if err := qb.UpdateFrontImage(ctx, movie.ID, frontimageData); err != nil {
return err
}
}
if len(frontimageData) == 0 && len(backimageData) == 0 {
// both images are being nulled. Destroy them.
if err := qb.DestroyImages(ctx, movie.ID); err != nil {
return err
}
} else {
// HACK - if front image is null and back image is not null, then set the front image
// to the default image since we can't have a null front image and a non-null back image
if frontimageData == nil && backimageData != nil {
frontimageData, _ = utils.ProcessImageInput(ctx, models.DefaultMovieImage)
}
if err := qb.UpdateImages(ctx, movie.ID, frontimageData, backimageData); err != nil {
return err
}
if backImageIncluded {
if err := qb.UpdateBackImage(ctx, movie.ID, backimageData); err != nil {
return err
}
}
@@ -239,7 +227,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
updatedMovie.Rating = translator.nullInt64(input.Rating, "rating")
updatedMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100)
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedMovie.Director = translator.nullString(input.Director, "director")

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import (
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/scraper/stashbox"
)
@@ -51,16 +52,23 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
}
var res *string
err = r.withTxn(ctx, func(ctx context.Context) error {
err = r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
scene, err := qb.Find(ctx, id)
if err != nil {
return err
}
filepath := manager.GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
if scene == nil {
return fmt.Errorf("scene with id %d not found", id)
}
res, err = client.SubmitSceneDraft(ctx, scene, boxes[input.StashBoxIndex].Endpoint, filepath)
cover, err := qb.GetCover(ctx, id)
if err != nil {
logger.Errorf("Error getting scene cover: %v", err)
}
res, err = client.SubmitSceneDraft(ctx, scene, boxes[input.StashBoxIndex].Endpoint, cover)
return err
})
@@ -82,7 +90,7 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
}
var res *string
err = r.withTxn(ctx, func(ctx context.Context) error {
err = r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
performer, err := qb.Find(ctx, id)
if err != nil {

View File

@@ -58,11 +58,18 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateI
newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true}
}
if input.Rating != nil {
newStudio.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
newStudio.Rating = sql.NullInt64{Valid: false}
if input.Rating100 != nil {
newStudio.Rating = sql.NullInt64{
Int64: int64(*input.Rating100),
Valid: true,
}
} else if input.Rating != nil {
newStudio.Rating = sql.NullInt64{
Int64: int64(models.Rating5To100(*input.Rating)),
Valid: true,
}
}
if input.Details != nil {
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
}
@@ -150,7 +157,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI
updatedStudio.URL = translator.nullString(input.URL, "url")
updatedStudio.Details = translator.nullString(input.Details, "details")
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
updatedStudio.Rating = translator.ratingConversion(input.Rating, input.Rating100)
updatedStudio.IgnoreAutoTag = input.IgnoreAutoTag
// Start the transaction and save the studio
@@ -169,15 +176,10 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI
}
// update image table
if len(imageData) > 0 {
if imageIncluded {
if err := qb.UpdateImage(ctx, s.ID, imageData); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyImage(ctx, s.ID); err != nil {
return err
}
}
// Save the stash_ids

View File

@@ -208,15 +208,10 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
}
// update image table
if len(imageData) > 0 {
if imageIncluded {
if err := qb.UpdateImage(ctx, tagID, imageData); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyImage(ctx, tagID); err != nil {
return err
}
}
if translator.hasField("aliases") {

View File

@@ -83,46 +83,55 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
scraperCDPPath := config.GetScraperCDPPath()
return &ConfigGeneralResult{
Stashes: config.GetStashPaths(),
DatabasePath: config.GetDatabasePath(),
BackupDirectoryPath: config.GetBackupDirectoryPath(),
GeneratedPath: config.GetGeneratedPath(),
MetadataPath: config.GetMetadataPath(),
ConfigFilePath: config.GetConfigFile(),
ScrapersPath: config.GetScrapersPath(),
CachePath: config.GetCachePath(),
CalculateMd5: config.IsCalculateMD5(),
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
ParallelTasks: config.GetParallelTasks(),
PreviewAudio: config.GetPreviewAudio(),
PreviewSegments: config.GetPreviewSegments(),
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
PreviewExcludeStart: config.GetPreviewExcludeStart(),
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
PreviewPreset: config.GetPreviewPreset(),
MaxTranscodeSize: &maxTranscodeSize,
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
WriteImageThumbnails: config.IsWriteImageThumbnails(),
APIKey: config.GetAPIKey(),
Username: config.GetUsername(),
Password: config.GetPasswordHash(),
MaxSessionAge: config.GetMaxSessionAge(),
LogFile: &logFile,
LogOut: config.GetLogOut(),
LogLevel: config.GetLogLevel(),
LogAccess: config.GetLogAccess(),
VideoExtensions: config.GetVideoExtensions(),
ImageExtensions: config.GetImageExtensions(),
GalleryExtensions: config.GetGalleryExtensions(),
CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(),
Excludes: config.GetExcludes(),
ImageExcludes: config.GetImageExcludes(),
CustomPerformerImageLocation: &customPerformerImageLocation,
ScraperUserAgent: &scraperUserAgent,
ScraperCertCheck: config.GetScraperCertCheck(),
ScraperCDPPath: &scraperCDPPath,
StashBoxes: config.GetStashBoxes(),
PythonPath: config.GetPythonPath(),
Stashes: config.GetStashPaths(),
DatabasePath: config.GetDatabasePath(),
BackupDirectoryPath: config.GetBackupDirectoryPath(),
GeneratedPath: config.GetGeneratedPath(),
MetadataPath: config.GetMetadataPath(),
ConfigFilePath: config.GetConfigFile(),
ScrapersPath: config.GetScrapersPath(),
CachePath: config.GetCachePath(),
BlobsPath: config.GetBlobsPath(),
BlobsStorage: config.GetBlobsStorage(),
CalculateMd5: config.IsCalculateMD5(),
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
ParallelTasks: config.GetParallelTasks(),
PreviewAudio: config.GetPreviewAudio(),
PreviewSegments: config.GetPreviewSegments(),
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
PreviewExcludeStart: config.GetPreviewExcludeStart(),
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
PreviewPreset: config.GetPreviewPreset(),
TranscodeHardwareAcceleration: config.GetTranscodeHardwareAcceleration(),
MaxTranscodeSize: &maxTranscodeSize,
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
WriteImageThumbnails: config.IsWriteImageThumbnails(),
GalleryCoverRegex: config.GetGalleryCoverRegex(),
APIKey: config.GetAPIKey(),
Username: config.GetUsername(),
Password: config.GetPasswordHash(),
MaxSessionAge: config.GetMaxSessionAge(),
LogFile: &logFile,
LogOut: config.GetLogOut(),
LogLevel: config.GetLogLevel(),
LogAccess: config.GetLogAccess(),
VideoExtensions: config.GetVideoExtensions(),
ImageExtensions: config.GetImageExtensions(),
GalleryExtensions: config.GetGalleryExtensions(),
CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(),
Excludes: config.GetExcludes(),
ImageExcludes: config.GetImageExcludes(),
CustomPerformerImageLocation: &customPerformerImageLocation,
ScraperUserAgent: &scraperUserAgent,
ScraperCertCheck: config.GetScraperCertCheck(),
ScraperCDPPath: &scraperCDPPath,
StashBoxes: config.GetStashBoxes(),
PythonPath: config.GetPythonPath(),
TranscodeInputArgs: config.GetTranscodeInputArgs(),
TranscodeOutputArgs: config.GetTranscodeOutputArgs(),
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
DrawFunscriptHeatmapRange: config.GetDrawFunscriptHeatmapRange(),
}
}
@@ -142,6 +151,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
showStudioAsText := config.GetShowStudioAsText()
css := config.GetCSS()
cssEnabled := config.GetCSSEnabled()
javascript := config.GetJavascript()
javascriptEnabled := config.GetJavascriptEnabled()
customLocales := config.GetCustomLocales()
customLocalesEnabled := config.GetCustomLocalesEnabled()
language := config.GetLanguage()
@@ -166,6 +177,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
ContinuePlaylistDefault: &continuePlaylistDefault,
CSS: &css,
CSSEnabled: &cssEnabled,
Javascript: &javascript,
JavascriptEnabled: &javascriptEnabled,
CustomLocales: &customLocales,
CustomLocalesEnabled: &customLocalesEnabled,
Language: &language,

View File

@@ -2,6 +2,8 @@ package api
import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/stashapp/stash/pkg/models"
@@ -13,10 +15,10 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Gallery.Find(ctx, idInt)
return err
}); err != nil {
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
@@ -24,7 +26,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
}
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *FindGalleriesResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
galleries, total, err := r.repository.Gallery.Query(ctx, galleryFilter, filter)
if err != nil {
return err
@@ -41,3 +43,14 @@ func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models
return ret, nil
}
func (r *queryResolver) AllGalleries(ctx context.Context) (ret []*models.Gallery, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Gallery.All(ctx)
return err
}); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -2,6 +2,8 @@ package api
import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/99designs/gqlgen/graphql"
@@ -12,7 +14,7 @@ import (
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
var image *models.Image
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
var err error
@@ -23,7 +25,7 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
}
image, err = qb.Find(ctx, idInt)
if err != nil {
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
} else if checksum != nil {
@@ -47,7 +49,7 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
}
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *FindImagesResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
fields := graphql.CollectAllFields(ctx)
@@ -84,3 +86,14 @@ func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.Imag
return ret, nil
}
func (r *queryResolver) AllImages(ctx context.Context) (ret []*models.Image, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Image.All(ctx)
return err
}); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -2,6 +2,8 @@ package api
import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/stashapp/stash/pkg/models"
@@ -13,10 +15,10 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.Find(ctx, idInt)
return err
}); err != nil {
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
@@ -24,7 +26,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
}
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *FindMoviesResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
movies, total, err := r.repository.Movie.Query(ctx, movieFilter, filter)
if err != nil {
return err
@@ -34,7 +36,6 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi
Count: total,
Movies: movies,
}
return nil
}); err != nil {
return nil, err
@@ -44,7 +45,7 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi
}
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Movie, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.All(ctx)
return err
}); err != nil {

View File

@@ -2,6 +2,8 @@ package api
import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/stashapp/stash/pkg/models"
@@ -13,10 +15,10 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Performer.Find(ctx, idInt)
return err
}); err != nil {
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
@@ -24,7 +26,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
}
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *FindPerformersResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
performers, total, err := r.repository.Performer.Query(ctx, performerFilter, filter)
if err != nil {
return err
@@ -43,7 +45,7 @@ func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *mod
}
func (r *queryResolver) AllPerformers(ctx context.Context) (ret []*models.Performer, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Performer.All(ctx)
return err
}); err != nil {

View File

@@ -2,6 +2,8 @@ package api
import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/stashapp/stash/pkg/models"
@@ -13,17 +15,17 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SavedFilter.Find(ctx, idInt)
return err
}); err != nil {
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
return ret, err
}
func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.FilterMode) (ret []*models.SavedFilter, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
if mode != nil {
ret, err = r.repository.SavedFilter.FindByMode(ctx, *mode)
} else {
@@ -37,10 +39,10 @@ func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.Filte
}
func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
return err
}); err != nil {
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
return ret, err

View File

@@ -2,6 +2,8 @@ package api
import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/99designs/gqlgen/graphql"
@@ -12,7 +14,7 @@ import (
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
var scene *models.Scene
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
var err error
if id != nil {
@@ -21,7 +23,7 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str
return err
}
scene, err = qb.Find(ctx, idInt)
if err != nil {
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
} else if checksum != nil {
@@ -43,7 +45,7 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str
func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInput) (*models.Scene, error) {
var scene *models.Scene
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
if input.Checksum != nil {
scenes, err := qb.FindByChecksum(ctx, *input.Checksum)
@@ -74,7 +76,7 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInpu
}
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var scenes []*models.Scene
var err error
@@ -135,7 +137,7 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
}
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneFilter := &models.SceneFilterType{}
@@ -192,7 +194,7 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config manager.SceneParserInput) (ret *SceneParserResultType, err error) {
parser := manager.NewSceneFilenameParser(filter, config)
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
result, count, err := parser.Parse(ctx, manager.SceneFilenameParserRepository{
Scene: r.repository.Scene,
Performer: r.repository.Performer,
@@ -223,7 +225,7 @@ func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int)
if distance != nil {
dist = *distance
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.FindDuplicates(ctx, dist)
return err
}); err != nil {
@@ -232,3 +234,14 @@ func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int)
return ret, nil
}
func (r *queryResolver) AllScenes(ctx context.Context) (ret []*models.Scene, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.All(ctx)
return err
}); err != nil {
return nil, err
}
return ret, nil
}

View File

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

View File

@@ -2,6 +2,8 @@ package api
import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/stashapp/stash/pkg/models"
@@ -13,11 +15,11 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Studio.Find(ctx, idInt)
return err
}); err != nil {
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
@@ -25,7 +27,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
}
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (ret *FindStudiosResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
studios, total, err := r.repository.Studio.Query(ctx, studioFilter, filter)
if err != nil {
return err
@@ -45,7 +47,7 @@ func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.St
}
func (r *queryResolver) AllStudios(ctx context.Context) (ret []*models.Studio, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.All(ctx)
return err
}); err != nil {

View File

@@ -2,6 +2,8 @@ package api
import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/stashapp/stash/pkg/models"
@@ -13,10 +15,10 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
return nil, err
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.Find(ctx, idInt)
return err
}); err != nil {
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
@@ -24,7 +26,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
}
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType) (ret *FindTagsResultType, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
tags, total, err := r.repository.Tag.Query(ctx, tagFilter, filter)
if err != nil {
return err
@@ -44,7 +46,7 @@ func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilte
}
func (r *queryResolver) AllTags(ctx context.Context) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.All(ctx)
return err
}); err != nil {

View File

@@ -7,14 +7,13 @@ import (
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*manager.SceneStreamEndpoint, error) {
// find the scene
var scene *models.Scene
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
idInt, _ := strconv.Atoi(*id)
var err error
scene, err = r.repository.Scene.Find(ctx, idInt)
@@ -32,8 +31,11 @@ func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*manage
return nil, errors.New("nil scene")
}
config := manager.GetInstance().Config
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewSceneURLBuilder(baseURL, scene.ID)
apiKey := config.GetAPIKey()
return manager.GetSceneStreamPaths(scene, builder.GetStreamURL(), config.GetInstance().GetMaxStreamingTranscodeSize())
return manager.GetSceneStreamPaths(scene, builder.GetStreamURL(apiKey), config.GetMaxStreamingTranscodeSize())
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,16 +56,21 @@ func (rs sceneRoutes) Routes() chi.Router {
// streaming endpoints
r.Get("/stream", rs.StreamDirect)
r.Get("/stream.mkv", rs.StreamMKV)
r.Get("/stream.webm", rs.StreamWebM)
r.Get("/stream.m3u8", rs.StreamHLS)
r.Get("/stream.ts", rs.StreamTS)
r.Get("/stream.mp4", rs.StreamMp4)
r.Get("/stream.webm", rs.StreamWebM)
r.Get("/stream.mkv", rs.StreamMKV)
r.Get("/stream.m3u8", rs.StreamHLS)
r.Get("/stream.m3u8/{segment}.ts", rs.StreamHLSSegment)
r.Get("/stream.mpd", rs.StreamDASH)
r.Get("/stream.mpd/{segment}_v.webm", rs.StreamDASHVideoSegment)
r.Get("/stream.mpd/{segment}_a.webm", rs.StreamDASHAudioSegment)
r.Get("/screenshot", rs.Screenshot)
r.Get("/preview", rs.Preview)
r.Get("/webp", rs.Webp)
r.Get("/vtt/chapter", rs.ChapterVtt)
r.Get("/vtt/chapter", rs.VttChapter)
r.Get("/vtt/thumbs", rs.VttThumbs)
r.Get("/vtt/sprite", rs.VttSprite)
r.Get("/funscript", rs.Funscript)
r.Get("/interactive_heatmap", rs.InteractiveHeatmap)
r.Get("/caption", rs.CaptionLang)
@@ -74,8 +79,8 @@ func (rs sceneRoutes) Routes() chi.Router {
r.Get("/scene_marker/{sceneMarkerId}/preview", rs.SceneMarkerPreview)
r.Get("/scene_marker/{sceneMarkerId}/screenshot", rs.SceneMarkerScreenshot)
})
r.With(rs.SceneCtx).Get("/{sceneId}_thumbs.vtt", rs.VttThumbs)
r.With(rs.SceneCtx).Get("/{sceneId}_sprite.jpg", rs.VttSprite)
r.Get("/{sceneHash}_thumbs.vtt", rs.VttThumbs)
r.Get("/{sceneHash}_sprite.jpg", rs.VttSprite)
return r
}
@@ -83,13 +88,32 @@ func (rs sceneRoutes) Routes() chi.Router {
// region Handlers
func (rs sceneRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
ss := manager.SceneServer{
TxnManager: rs.txnManager,
SceneCoverGetter: rs.sceneFinder,
scene := r.Context().Value(sceneKey).(*models.Scene)
// #3526 - return 404 if the scene does not have any files
if scene.Path == "" {
w.WriteHeader(http.StatusNotFound)
return
}
ss.StreamSceneDirect(scene, w, r)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, sceneHash)
streamRequestCtx := ffmpeg.NewStreamRequestContext(w, r)
// #2579 - hijacking and closing the connection here causes video playback to fail in Safari
// We trust that the request context will be closed, so we don't need to call Cancel on the
// returned context here.
_ = manager.GetInstance().ReadLockManager.ReadLock(streamRequestCtx, filepath)
http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) {
rs.streamTranscode(w, r, ffmpeg.StreamTypeMP4)
}
func (rs sceneRoutes) StreamWebM(w http.ResponseWriter, r *http.Request) {
rs.streamTranscode(w, r, ffmpeg.StreamTypeWEBM)
}
func (rs sceneRoutes) StreamMKV(w http.ResponseWriter, r *http.Request) {
@@ -114,118 +138,118 @@ func (rs sceneRoutes) StreamMKV(w http.ResponseWriter, r *http.Request) {
return
}
rs.streamTranscode(w, r, ffmpeg.StreamFormatMKVAudio)
rs.streamTranscode(w, r, ffmpeg.StreamTypeMKV)
}
func (rs sceneRoutes) StreamWebM(w http.ResponseWriter, r *http.Request) {
rs.streamTranscode(w, r, ffmpeg.StreamFormatVP9)
}
func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) {
rs.streamTranscode(w, r, ffmpeg.StreamFormatH264)
}
func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, streamType ffmpeg.StreamFormat) {
scene := r.Context().Value(sceneKey).(*models.Scene)
pf := scene.Files.Primary()
if pf == nil {
streamManager := manager.GetInstance().StreamManager
if streamManager == nil {
http.Error(w, "Live transcoding disabled", http.StatusServiceUnavailable)
return
}
logger.Debug("Returning HLS playlist")
// getting the playlist manifest only
w.Header().Set("Content-Type", ffmpeg.MimeHLS)
var str strings.Builder
ffmpeg.WriteHLSPlaylist(pf.Duration, r.URL.String(), &str)
requestByteRange := createByteRange(r.Header.Get("Range"))
if requestByteRange.RawString != "" {
logger.Debugf("Requested range: %s", requestByteRange.RawString)
}
ret := requestByteRange.apply([]byte(str.String()))
rangeStr := requestByteRange.toHeaderValue(int64(str.Len()))
w.Header().Set("Content-Range", rangeStr)
if n, err := w.Write(ret); err != nil {
logger.Warnf("[stream] error writing stream (wrote %v bytes): %v", n, err)
}
}
func (rs sceneRoutes) StreamTS(w http.ResponseWriter, r *http.Request) {
rs.streamTranscode(w, r, ffmpeg.StreamFormatHLS)
}
func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, streamFormat ffmpeg.StreamFormat) {
scene := r.Context().Value(sceneKey).(*models.Scene)
f := scene.Files.Primary()
if f == nil {
return
}
logger.Debugf("Streaming as %s", streamFormat.MimeType)
// start stream based on query param, if provided
if err := r.ParseForm(); err != nil {
logger.Warnf("[stream] error parsing query form: %v", err)
logger.Warnf("[transcode] error parsing query form: %v", err)
}
startTime := r.Form.Get("start")
ss, _ := strconv.ParseFloat(startTime, 64)
requestedSize := r.Form.Get("resolution")
resolution := r.Form.Get("resolution")
audioCodec := ffmpeg.MissingUnsupported
if f.AudioCodec != "" {
audioCodec = ffmpeg.ProbeAudioCodec(f.AudioCodec)
options := ffmpeg.TranscodeOptions{
StreamType: streamType,
VideoFile: f,
Resolution: resolution,
StartTime: ss,
}
width := f.Width
height := f.Height
logger.Debugf("[transcode] streaming scene %d as %s", scene.ID, streamType.MimeType)
streamManager.ServeTranscode(w, r, options)
}
options := ffmpeg.TranscodeStreamOptions{
Input: f.Path,
Codec: streamFormat,
VideoOnly: audioCodec == ffmpeg.MissingUnsupported,
func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
rs.streamManifest(w, r, ffmpeg.StreamTypeHLS, "HLS")
}
VideoWidth: width,
VideoHeight: height,
func (rs sceneRoutes) StreamDASH(w http.ResponseWriter, r *http.Request) {
rs.streamManifest(w, r, ffmpeg.StreamTypeDASHVideo, "DASH")
}
StartTime: ss,
MaxTranscodeSize: config.GetInstance().GetMaxStreamingTranscodeSize().GetMaxResolution(),
}
func (rs sceneRoutes) streamManifest(w http.ResponseWriter, r *http.Request, streamType *ffmpeg.StreamType, logName string) {
scene := r.Context().Value(sceneKey).(*models.Scene)
if requestedSize != "" {
options.MaxTranscodeSize = models.StreamingResolutionEnum(requestedSize).GetMaxResolution()
}
encoder := manager.GetInstance().FFMPEG
lm := manager.GetInstance().ReadLockManager
streamRequestCtx := manager.NewStreamRequestContext(w, r)
lockCtx := lm.ReadLock(streamRequestCtx, f.Path)
// hijacking and closing the connection here causes video playback to hang in Chrome
// due to ERR_INCOMPLETE_CHUNKED_ENCODING
// We trust that the request context will be closed, so we don't need to call Cancel on the returned context here.
stream, err := encoder.GetTranscodeStream(lockCtx, options)
if err != nil {
logger.Errorf("[stream] error transcoding video file: %v", err)
w.WriteHeader(http.StatusBadRequest)
if _, err := w.Write([]byte(err.Error())); err != nil {
logger.Warnf("[stream] error writing response: %v", err)
}
streamManager := manager.GetInstance().StreamManager
if streamManager == nil {
http.Error(w, "Live transcoding disabled", http.StatusServiceUnavailable)
return
}
lockCtx.AttachCommand(stream.Cmd)
f := scene.Files.Primary()
if f == nil {
return
}
stream.Serve(w, r)
w.(http.Flusher).Flush()
if err := r.ParseForm(); err != nil {
logger.Warnf("[transcode] error parsing query form: %v", err)
}
resolution := r.Form.Get("resolution")
logger.Debugf("[transcode] returning %s manifest for scene %d", logName, scene.ID)
streamManager.ServeManifest(w, r, streamType, f, resolution)
}
func (rs sceneRoutes) StreamHLSSegment(w http.ResponseWriter, r *http.Request) {
rs.streamSegment(w, r, ffmpeg.StreamTypeHLS)
}
func (rs sceneRoutes) StreamDASHVideoSegment(w http.ResponseWriter, r *http.Request) {
rs.streamSegment(w, r, ffmpeg.StreamTypeDASHVideo)
}
func (rs sceneRoutes) StreamDASHAudioSegment(w http.ResponseWriter, r *http.Request) {
rs.streamSegment(w, r, ffmpeg.StreamTypeDASHAudio)
}
func (rs sceneRoutes) streamSegment(w http.ResponseWriter, r *http.Request, streamType *ffmpeg.StreamType) {
scene := r.Context().Value(sceneKey).(*models.Scene)
streamManager := manager.GetInstance().StreamManager
if streamManager == nil {
http.Error(w, "Live transcoding disabled", http.StatusServiceUnavailable)
return
}
f := scene.Files.Primary()
if f == nil {
return
}
if err := r.ParseForm(); err != nil {
logger.Warnf("[transcode] error parsing query form: %v", err)
}
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
segment := chi.URLParam(r, "segment")
resolution := r.Form.Get("resolution")
options := ffmpeg.StreamOptions{
StreamType: streamType,
VideoFile: f,
Resolution: resolution,
Hash: sceneHash,
Segment: segment,
}
streamManager.ServeSegment(w, r, options)
}
func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) {
@@ -240,7 +264,8 @@ func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) {
func (rs sceneRoutes) Preview(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
filepath := manager.GetInstance().Paths.Scene.GetVideoPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
filepath := manager.GetInstance().Paths.Scene.GetVideoPreviewPath(sceneHash)
serveFileNoCache(w, r, filepath)
}
@@ -254,7 +279,8 @@ func serveFileNoCache(w http.ResponseWriter, r *http.Request, filepath string) {
func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
filepath := manager.GetInstance().Paths.Scene.GetWebpPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
filepath := manager.GetInstance().Paths.Scene.GetWebpPreviewPath(sceneHash)
http.ServeFile(w, r, filepath)
}
@@ -264,7 +290,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce
}
var title string
if err := txn.WithTxn(ctx, rs.txnManager, func(ctx context.Context) error {
if err := txn.WithReadTxn(ctx, rs.txnManager, func(ctx context.Context) error {
qb := rs.tagFinder
primaryTag, err := qb.Find(ctx, marker.PrimaryTagID)
if err != nil {
@@ -290,10 +316,10 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce
return &title, nil
}
func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
func (rs sceneRoutes) VttChapter(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
var sceneMarkers []*models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarkers, err = rs.sceneMarkerFinder.FindBySceneID(ctx, scene.ID)
return err
@@ -332,6 +358,32 @@ func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(vtt))
}
func (rs sceneRoutes) VttThumbs(w http.ResponseWriter, r *http.Request) {
scene, ok := r.Context().Value(sceneKey).(*models.Scene)
var sceneHash string
if ok && scene != nil {
sceneHash = scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
} else {
sceneHash = chi.URLParam(r, "sceneHash")
}
w.Header().Set("Content-Type", "text/vtt")
filepath := manager.GetInstance().Paths.Scene.GetSpriteVttFilePath(sceneHash)
http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) VttSprite(w http.ResponseWriter, r *http.Request) {
scene, ok := r.Context().Value(sceneKey).(*models.Scene)
var sceneHash string
if ok && scene != nil {
sceneHash = scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
} else {
sceneHash = chi.URLParam(r, "sceneHash")
}
w.Header().Set("Content-Type", "image/jpeg")
filepath := manager.GetInstance().Paths.Scene.GetSpriteImageFilePath(sceneHash)
http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) Funscript(w http.ResponseWriter, r *http.Request) {
s := r.Context().Value(sceneKey).(*models.Scene)
funscript := video.GetFunscriptPath(s.Path)
@@ -340,8 +392,9 @@ func (rs sceneRoutes) Funscript(w http.ResponseWriter, r *http.Request) {
func (rs sceneRoutes) InteractiveHeatmap(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
w.Header().Set("Content-Type", "image/png")
filepath := manager.GetInstance().Paths.Scene.GetInteractiveHeatmapPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
filepath := manager.GetInstance().Paths.Scene.GetInteractiveHeatmapPath(sceneHash)
http.ServeFile(w, r, filepath)
}
@@ -349,7 +402,7 @@ func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang strin
s := r.Context().Value(sceneKey).(*models.Scene)
var captions []*models.VideoCaption
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
primaryFile := s.Files.Primary()
if primaryFile == nil {
@@ -377,14 +430,14 @@ func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang strin
sub, err := video.ReadSubs(caption.Path(s.Path))
if err != nil {
logger.Warnf("error while reading subs: %v", err)
http.Error(w, readTxnErr.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var b bytes.Buffer
err = sub.WriteToWebVTT(&b)
if err != nil {
http.Error(w, readTxnErr.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -405,25 +458,12 @@ func (rs sceneRoutes) CaptionLang(w http.ResponseWriter, r *http.Request) {
rs.Caption(w, r, l, ext)
}
func (rs sceneRoutes) VttThumbs(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
w.Header().Set("Content-Type", "text/vtt")
filepath := manager.GetInstance().Paths.Scene.GetSpriteVttFilePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) VttSprite(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
w.Header().Set("Content-Type", "image/jpeg")
filepath := manager.GetInstance().Paths.Scene.GetSpriteImageFilePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@@ -442,15 +482,16 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request)
return
}
filepath := manager.GetInstance().Paths.SceneMarkers.GetVideoPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
filepath := manager.GetInstance().Paths.SceneMarkers.GetVideoPreviewPath(sceneHash, int(sceneMarker.Seconds))
http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@@ -469,7 +510,7 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
return
}
filepath := manager.GetInstance().Paths.SceneMarkers.GetWebpPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
filepath := manager.GetInstance().Paths.SceneMarkers.GetWebpPreviewPath(sceneHash, int(sceneMarker.Seconds))
// If the image doesn't exist, send the placeholder
exists, _ := fsutil.FileExists(filepath)
@@ -485,9 +526,10 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@@ -506,7 +548,7 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque
return
}
filepath := manager.GetInstance().Paths.SceneMarkers.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
filepath := manager.GetInstance().Paths.SceneMarkers.GetScreenshotPath(sceneHash, int(sceneMarker.Seconds))
// If the image doesn't exist, send the placeholder
exists, _ := fsutil.FileExists(filepath)
@@ -524,28 +566,16 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque
func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sceneIdentifierQueryParam := chi.URLParam(r, "sceneId")
sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam)
sceneID, err := strconv.Atoi(chi.URLParam(r, "sceneId"))
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
var scene *models.Scene
_ = txn.WithTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
qb := rs.sceneFinder
if sceneID == 0 {
var scenes []*models.Scene
// determine checksum/os by the length of the query param
if len(sceneIdentifierQueryParam) == 32 {
scenes, _ = qb.FindByChecksum(ctx, sceneIdentifierQueryParam)
} else {
scenes, _ = qb.FindByOSHash(ctx, sceneIdentifierQueryParam)
}
if len(scenes) > 0 {
scene = scenes[0]
}
} else {
scene, _ = qb.Find(ctx, sceneID)
}
scene, _ = qb.Find(ctx, sceneID)
if scene != nil {
if err := scene.LoadPrimaryFile(ctx, rs.fileFinder); err != nil {

View File

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

View File

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

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