Compare commits

...

223 Commits

Author SHA1 Message Date
AdultSun
ac5dfdece2 Expands quickstart language based on DogmaDragon's suggestions 2024-03-11 15:02:14 -07:00
AdultSun
eb658834c3 Update StashDB details in README.md
- Directs users to new guide in the StashDB docs instead of Discord
- No longer necessary to join Discord/Matrix for new users of StashDB now that invite codes are multi-use
- Updates formatting of the same "Quickstart Guide" section a little
2024-03-10 17:32:46 -07:00
InfiniteStash
c5bc106c1a Fix text color of medium fingerprint matches (#4662) 2024-03-08 14:59:17 +11:00
CJ
9735d0fad1 fix image card width on front page (#4665) 2024-03-08 14:40:00 +11:00
CJ
353d889fd5 fit cards code improvement (#4658) 2024-03-08 14:36:15 +11:00
WithoutPants
c7b2314bb1 Fix image clip webm not being cleaned (#4657) 2024-03-07 09:03:00 +11:00
WithoutPants
4614471ad9 Fix ffmpeg error when trying to scale and copy video (#4660) 2024-03-07 09:02:45 +11:00
Weblate (bot)
7733a214d3 Translations update from Hosted Weblate (#4641)
* Translated using Weblate (Finnish)

Currently translated at 73.5% (835 of 1136 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/fi/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (1136 of 1136 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/sv/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1136 of 1136 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Czech)

Currently translated at 82.9% (942 of 1136 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/cs/

* Translated using Weblate (Czech)

Currently translated at 90.4% (1027 of 1136 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/cs/

* Translated using Weblate (Italian)

Currently translated at 78.7% (895 of 1136 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/it/

* Translated using Weblate (Russian)

Currently translated at 100.0% (1136 of 1136 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/ru/

* Translated using Weblate (Indonesian)

Currently translated at 43.7% (497 of 1136 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/id/

---------

Co-authored-by: gimmeliina <jarruraita@outlook.com>
Co-authored-by: alpacaserious <srhsgsef@gmail.com>
Co-authored-by: wql219 <wanqinglin219@hotmail.com>
Co-authored-by: Nymeria <Tractorb@seznam.cz>
Co-authored-by: its-josh4 <myselftdev@gmail.com>
Co-authored-by: Old gnome <orpgnome@users.noreply.hosted.weblate.org>
Co-authored-by: Tukimin Satrio <k797du3eh@mozmail.com>
2024-03-06 10:20:42 +11:00
WithoutPants
cd2f0922ab Update changelog 2024-03-06 10:12:53 +11:00
dogwithakeyboard
f1f6e84aa0 Add bmp to image codec list (#4653) 2024-03-06 09:56:17 +11:00
WithoutPants
22986097c4 Fix code from #4570 2024-03-06 09:52:06 +11:00
WithoutPants
409a200ebc Fix URL prefix re-inserted when redirecting settings page (#4650) 2024-03-05 14:54:07 +11:00
WithoutPants
20ac388f77 Update changelog 2024-03-05 14:46:32 +11:00
CJ
0626a7aea1 fix lightbox display modes (#4644) 2024-03-05 13:37:39 +11:00
Flashy78
2ca9e0f43a Adding disambiguation to performer draft (#4122) 2024-02-29 12:47:20 +11:00
WithoutPants
b4823bec8a Use proxy in utils.ReadImageFromURL (#4637) 2024-02-29 11:28:30 +11:00
WithoutPants
945188a0ba Fix images with , character not rendering (#4636) 2024-02-29 11:28:11 +11:00
Weblate (bot)
b59afd2dcd Translations update from Hosted Weblate (#4634)
* Translated using Weblate (Indonesian)

Currently translated at 33.9% (386 of 1136 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/id/

* Add Indonesian locale option

---------

Co-authored-by: Tukimin Satrio <k797du3eh@mozmail.com>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-02-29 10:18:20 +11:00
Weblate (bot)
9202787be0 Translations update from Hosted Weblate (#4615)
* Translated using Weblate (Swedish)

Currently translated at 100.0% (1135 of 1135 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/sv/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (1117 of 1135 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Indonesian)

Currently translated at 13.9% (158 of 1135 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/id/

* Translated using Weblate (Polish)

Currently translated at 85.2% (968 of 1135 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/pl/

* Translated using Weblate (French)

Currently translated at 95.5% (1085 of 1135 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/fr/

* Translated using Weblate (French)

Currently translated at 95.5% (1085 of 1135 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/fr/

* Translated using Weblate (Polish)

Currently translated at 85.3% (969 of 1135 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/pl/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1135 of 1135 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Czech)

Currently translated at 53.3% (606 of 1135 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/cs/

* Translated using Weblate (Czech)

Currently translated at 56.0% (636 of 1135 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/cs/

---------

Co-authored-by: alpacaserious <srhsgsef@gmail.com>
Co-authored-by: wql219 <wanqinglin219@hotmail.com>
Co-authored-by: Tukimin Satrio <k797du3eh@mozmail.com>
Co-authored-by: Mateusz <mateuszvx8.96@gmail.com>
Co-authored-by: doodoo <adr.web@hotmail.fr>
Co-authored-by: Nathan Lafrenière-Racine <nathan.lafreniere@gmail.com>
Co-authored-by: Nymeria <Tractorb@seznam.cz>
2024-02-28 11:54:37 +11:00
WithoutPants
4999e85fae Fix performer disambiguation styling in select 2024-02-28 11:21:29 +11:00
WithoutPants
2bdf0d9e62 Fix penis length being incorrectly truncated (#4630) 2024-02-28 11:08:28 +11:00
WithoutPants
2e00cb6c5a Document plugin settings 2024-02-28 11:07:31 +11:00
WithoutPants
33857122b8 Add 0.25.0 changelog 2024-02-28 10:18:59 +11:00
CJ
768f74a0b3 Fix unset gallery card width on initialization (#4612) 2024-02-28 09:10:15 +11:00
InfiniteStash
98c428ba4e Improve scene tagger prioritization (#4618) 2024-02-28 08:29:49 +11:00
WithoutPants
fcf249e5f6 Improve plugin hook cyclic detection (#4625)
* Move and rename HookTriggerEnum into separate package
* Move visited plugin hook handler code
* Allow up to ten plugin hook loops
2024-02-28 08:29:25 +11:00
WithoutPants
3a56dd98db Change handlerRequiredFilter to check for .forcegallery (#4627) 2024-02-28 08:28:29 +11:00
WithoutPants
48c287ed76 Add last o sort option (#4626) 2024-02-28 08:28:11 +11:00
NodudeWasTaken
9c6fbfc16f Add filename to image serve (#4616) 2024-02-27 16:11:40 +11:00
Raghavan
6a9175c954 Implement UI event dispatcher/listener (#4492)
* page change event
* expose event to plugin api
* Update UIPluginApi.md
* Add to example plugin
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-02-27 09:27:28 +11:00
WithoutPants
56896d7c7d Fix panic 2024-02-26 15:08:45 +11:00
Weblate (bot)
2e35221003 Translations update from Hosted Weblate (#4585)
* Translated using Weblate (Estonian)

Currently translated at 100.0% (1106 of 1106 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/et/

* Translated using Weblate (Hungarian)

Currently translated at 42.7% (473 of 1106 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/hu/

* Translated using Weblate (Hungarian)

Currently translated at 42.7% (473 of 1106 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/hu/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (1106 of 1106 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/sv/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 87.2% (965 of 1106 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 80.9% (895 of 1106 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hant/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 91.4% (1011 of 1106 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 91.4% (1011 of 1106 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 91.4% (1011 of 1106 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.6% (1071 of 1108 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.6% (1071 of 1108 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.6% (1071 of 1108 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.7% (1072 of 1108 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.7% (1072 of 1108 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.8% (1073 of 1108 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.8% (1073 of 1108 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.8% (1073 of 1108 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Added translation using Weblate (Indonesian)

* Translated using Weblate (Swedish)

Currently translated at 100.0% (1108 of 1108 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/sv/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 97.6% (1098 of 1125 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 97.6% (1098 of 1125 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 97.6% (1098 of 1125 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 97.6% (1098 of 1125 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Indonesian)

Currently translated at 8.3% (94 of 1125 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/id/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 97.6% (1098 of 1125 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (1103 of 1125 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (1107 of 1125 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (1107 of 1125 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/zh_Hans/

---------

Co-authored-by: LauriL <TheLauri@users.noreply.hosted.weblate.org>
Co-authored-by: Kolbász <services@perjel.hu>
Co-authored-by: R Balu <balu.ka@hotmail.com>
Co-authored-by: alpacaserious <srhsgsef@gmail.com>
Co-authored-by: BugGeeker <darling940620@foxmail.com>
Co-authored-by: Y. Chen <daniel.yifu.chen@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Co-authored-by: 张Ly <zanzhz1101@gmail.com>
Co-authored-by: wql219 <wanqinglin219@hotmail.com>
Co-authored-by: Tukimin Satrio <k797du3eh@mozmail.com>
Co-authored-by: celery1806 <qincai233@gmail.com>
2024-02-23 16:36:12 +11:00
WithoutPants
ba1ebba6c0 Add Clean generated files task (#4607)
* Add clean generate task
* Add to library tasks
* Save and read defaults
* Stop handling and logging
* Make filename parsing more robust
2024-02-23 15:56:00 +11:00
WithoutPants
4a3ce8b6ec Fix auto tag from object not honouring the ignore autotag flag (#4610)
* Fix auto tag from object ignoring the ignore autotag field
* Disable auto tag buttons where ignore auto tag is enabled
2024-02-23 14:34:04 +11:00
WithoutPants
4b84ec0d85 Support setting galleries in multiple images (#4608) 2024-02-23 11:42:04 +11:00
WithoutPants
a302fc78ea Fix crash when generating thumbnail during scan 2024-02-23 11:20:07 +11:00
WithoutPants
f2bc3d5567 Log on scene cover fetch error 2024-02-22 14:59:03 +11:00
WithoutPants
a303446bb7 Scene play and o-counter history view and editing (#4532)
Co-authored-by: randemgame <61895715+randemgame@users.noreply.github.com>
2024-02-22 11:28:18 +11:00
WithoutPants
0c2a2190e5 Plugin API improvements (#4603)
* Accept plain map for runPluginTask
* Support running plugin task without task name
* Add interface to run plugin operations
* Update RunPluginTask client mutation
2024-02-22 11:20:21 +11:00
WithoutPants
a8c909e0c9 Add option to generate image thumbnails during generate (#4602)
* Add option to generate image thumbnails
* Limit number of concurrent image thumbnail generation ops
2024-02-22 11:19:23 +11:00
dogwithakeyboard
c4a91d15a6 include primary tag name in search and sort (#4606) 2024-02-22 11:18:29 +11:00
WithoutPants
61bd9233b2 Fix incorrect documentation in Plugins.md 2024-02-22 08:15:40 +11:00
dogwithakeyboard
37acd6b79b restore image performer age filter (#4601) 2024-02-21 11:22:04 +11:00
WithoutPants
5bb9bf902c Show upgradable packages only when checking for updates (#4599)
* Sort upgradable packages to top
* Show upgradable packages only by default
* Fix loading state when refetching
2024-02-21 08:24:18 +11:00
WithoutPants
76e5598876 Improve handling of moved and added video files (#4598)
* If old file path is not in library, treat as move
* Use existing phash if file with same oshash exists
2024-02-20 14:04:31 +11:00
dogwithakeyboard
8b1d4ccc97 Movie scene sorting (#4588) 2024-02-20 14:01:27 +11:00
Raghavan
cff068f519 add pluginApi.d.ts (#4595)
* add pluginApi.d.ts
* Don't lint pluginApi.d.ts
2024-02-20 13:00:44 +11:00
WithoutPants
276bc5a8cb Fix 404 not returning immediately in asset resolver (#4597) 2024-02-20 13:00:27 +11:00
WithoutPants
b4a6cc43d1 Default tag hover popover to right (#4593) 2024-02-20 09:03:06 +11:00
WithoutPants
777fb44ac6 Fix default url not redirecting in studio page (#4592) 2024-02-20 09:02:52 +11:00
bob123491234
f5a42ede2d Add galleries to image edit panel (#4573)
* Add Galleries to ImageEditPanel
* Exclude filesystem-based galleries from selection
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-02-19 14:45:29 +11:00
WithoutPants
7bb38ae6dc Don't error out if a single url not fetched (#4591) 2024-02-19 14:45:11 +11:00
WithoutPants
7d56f1a093 Fix panic 2024-02-19 14:10:41 +11:00
WithoutPants
afd7f02644 Error if duplicate plugin id is loaded (#4571)
* Error if duplicate plugin id is loaded
* Use case insensitive comparison
2024-02-19 13:55:52 +11:00
WithoutPants
93b851eae6 Merge studio stash ids (#4572) 2024-02-19 11:45:04 +11:00
WithoutPants
1dfb960a87 Fix import 2024-02-19 11:43:14 +11:00
WithoutPants
e231812203 Movie select overhaul (#4563)
* Add ids to findMovies input
* Use ids for other find interfaces
* Update client side
* Fix gallery select function
* Replace movie select
* Re-add creatable
* Overhaul movie table
* Remove and deprecated unused code
2024-02-19 10:25:08 +11:00
WithoutPants
e7f610ce18 Fix invalid share causing error during cleaning (#4570) 2024-02-19 10:24:10 +11:00
WithoutPants
6e9718a600 Toast improvements (#4584)
* Change default toast placement
* Position at bottom on mobile
* Show single toast message at a time
* Optionally show dialog for error messages
* Fix circular dependency
* Animate toast
2024-02-19 10:22:34 +11:00
WithoutPants
6fb1c41ae9 Update translation files (#4581)
Updated by "Cleanup translation files" hook in Weblate.

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2024-02-17 14:26:37 +11:00
WithoutPants
5aba3c1a98 Weblate translations update (#4578)
* Translated using Weblate (French)

Currently translated at 100.0% (1056 of 1056 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/fr/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 81.3% (859 of 1056 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/pt_BR/

* Translated using Weblate (Swedish)

Currently translated at 96.3% (1017 of 1056 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/sv/

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/

* Translated using Weblate (French)

Currently translated at 100.0% (1056 of 1056 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/fr/

* Translated using Weblate (Swedish)

Currently translated at 99.8% (1054 of 1056 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/sv/

* Translated using Weblate (French)

Currently translated at 100.0% (1056 of 1056 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/fr/

* Added translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (French)

Currently translated at 100.0% (1056 of 1056 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/fr/

* Translated using Weblate (Hungarian)

Currently translated at 37.5% (397 of 1056 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/hu/

* Translated using Weblate (Swedish)

Currently translated at 99.9% (1055 of 1056 strings)

Translation: stashapp/stash
Translate-URL: https://hosted.weblate.org/projects/stashapp/stash/sv/

* Pretty locale files

* Sort locale keys

---------

Co-authored-by: doodoo <adr.web@hotmail.fr>
Co-authored-by: Robert de Abreu Viana <deathrobert2010@gmail.com>
Co-authored-by: alpacaserious <srhsgsef@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: R Balu <balu.ka@hotmail.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
2024-02-17 14:06:40 +11:00
WithoutPants
440c261f5b Developer option: extra blob paths (#4566)
* Allow additional read-only blob paths
* Add developer option to add more blob sources
* Add makefile targets to start and remove build container
* Documentation
2024-02-16 12:39:45 +11:00
WithoutPants
8fc997dfe9 Fix merge not deleting deleted scene generated files (#4567) 2024-02-16 12:38:34 +11:00
WithoutPants
5b9bdadaec Mount edit panels only when first entering (#4564) 2024-02-16 12:37:49 +11:00
WithoutPants
706b61233f Fix country selector in bulk performer edit dialog (#4565)
* Fix country select in edit performers dialog
* Fix edit performer dialog height
2024-02-16 12:37:24 +11:00
yoshnopa
aaf3114194 Bugfix: No Image Upscale for Clips (#4569) 2024-02-16 12:21:36 +11:00
NodudeWasTaken
15aac68a14 Fix segment repeating + cleanup speed calculation (#4557) 2024-02-15 13:46:59 +11:00
WithoutPants
dad4ab6a6f Fix scraped studio not matching existing value (#4548)
* Fix scraped studio not matching existing
* Fix incorrect key value
2024-02-13 12:24:11 +11:00
WithoutPants
e9703e9a6e Fix url not updated for default tab on Performer/Studio/Tag pages (#4538) 2024-02-12 15:08:11 +11:00
WithoutPants
46eb01198a Plugin api improvements (#4546)
* Expose useToast

* Expose components:

- studio/tag/performer/gallery selects
- date input
- country select
- folder select
2024-02-12 15:07:44 +11:00
WithoutPants
235c9c90c2 Add disambiguation to performer link and performer select values (#4541)
* Add disambiguation to PerformerLink
* Add disambiguation to performer select values
2024-02-12 14:03:45 +11:00
WithoutPants
a4bbdcfbae Common studio overlay component (#4540)
* Move GridCard to own directory
* Make common studio overlay component
2024-02-12 14:03:13 +11:00
WithoutPants
8c410a9a14 Wrap card popovers (#4539) 2024-02-12 14:02:46 +11:00
WithoutPants
9981574e82 Add gallery select filter and fix image gallery filtering (#4535)
* Accept gallery ids in findGalleries
* Add gallery select component
* Add and fix image gallery filter
* Show gallery path as alias
2024-02-09 16:42:07 +11:00
WithoutPants
79e72ff3bc Fix UI config mutation not working (#4533) 2024-02-09 12:27:08 +11:00
CJ
a16f3da33e Fix tag popper over card (#4529) 2024-02-08 16:46:55 +11:00
WithoutPants
8770e81ec5 Improve sorting of results when entering text in select fields (#4528)
* Sort select results by relevance
* Apply relevance sorting to studio select
* Apply relevance sorting to filter select
2024-02-07 10:32:19 +11:00
dependabot[bot]
9284ede0fb Bump vite from 4.4.12 to 4.5.2 in /ui/v2.5 (#4477)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.12 to 4.5.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 10:30:17 +11:00
DingDongSoLong4
2d73912f15 Improve client-side graphql scalar types (#4511)
* Add types to graphql scalars
* Upgrade dependencies
* Override UI config type
* Remove all IUIConfig casts
* Add tableColumns to IUIConfig
* Add BoolMap type, set strictScalars
* Add PluginConfigMap
* Replace any with unknown
* Add SavedObjectFilter and SavedUIOptions
* Remove unused items from CriterionType
2024-02-07 09:49:32 +11:00
dogwithakeyboard
9ac6505241 Studio child filter and sort (#4479) 2024-02-06 13:24:00 +11:00
CJ
a402ee5fa7 support filesize in scene list (#4480) 2024-02-06 13:10:21 +11:00
CJ
a8df95c3a4 Fit cards properly within their containers (#4514)
* created missing cards grids
2024-02-06 13:06:47 +11:00
WithoutPants
330581283a Fix UI crash during setup (#4527) 2024-02-06 11:48:26 +11:00
Raghavan
892d74c98b expose useful libs (#4489)
* expose useful libs
* Update UIPluginApi.md
2024-02-06 11:29:01 +11:00
WithoutPants
de2b28d3f9 Studio select refactor (#4493)
* Add id interface to findStudios
* Replace existing selects
* Remove unused code
* Fix scrape/merge select
* Make clearable
2024-02-06 11:26:16 +11:00
WithoutPants
217c02f181 Fix tag selector in scrape dialogs (#4526) 2024-02-06 10:26:16 +11:00
DingDongSoLong4
3ea31aeb76 Add blank favicon on login page (#4498) 2024-02-06 10:22:32 +11:00
DingDongSoLong4
cf8efa9035 Link improvements and fixes (#4501)
* Add ExternalLink
* Replace <a> with <Link>
2024-02-06 10:21:19 +11:00
CJ
1d0fa27c71 Improve list table readability (#4497)
* fixes mandatory columns bug and consistency issues
2024-02-06 10:18:24 +11:00
NodudeWasTaken
0b82dbf666 Frontend fix media_err_decode on playback (#4506) 2024-02-06 10:08:40 +11:00
CJ
11cafe933a only use details background image when provided (#4515) 2024-01-29 18:04:41 +11:00
DingDongSoLong4
d82c526ada Minor UI fixes (#4509)
* Fix a zero value disabling the SettingModal
* Fix performer disambiguation styling
* Fix performer tagger error message
2024-01-29 17:56:39 +11:00
DogmaDragon
1588d1cb4e Update scene list setting (#4473) 2024-01-24 21:01:35 +11:00
DogmaDragon
64f2071d8c Add note for auto tag (#4490) 2024-01-24 21:00:57 +11:00
DogmaDragon
3573795cf7 Clarify session description (#4468) 2024-01-24 21:00:08 +11:00
WithoutPants
723211a620 Tag select refactor (#4478)
* Add interface to load tags by id
* Use minimal data for tag select queries
* Center image/text in select list
* Overhaul tag select
* Support excludeIds. Comment out image in dropdown
* Replace existing selects
* Remove unused code
* Fix styling of aliases
2024-01-24 20:24:13 +11:00
CJ
dd8da7f339 Improved list view for scenes, galleries and performers (#4368)
Co-authored-by: InfiniteStash <117855276+InfiniteStash@users.noreply.github.com>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-01-17 10:46:09 +11:00
WithoutPants
e7311a60d2 Advanced settings (#4378)
* Add advanced settings mode toggle
* Add advanced settings
2024-01-16 16:22:04 +11:00
flubber1234
29677696fd Add support for disabling mobile media-viewer's fullscreen auto-rotate (#4416) 2024-01-16 15:00:58 +11:00
WithoutPants
403f7c54ef Move zip files while moving folders (#4374) 2024-01-16 14:48:28 +11:00
OFP
75099b38a8 Update Performer gender filter (#4419)
* Update Performer gender filter

Updated the Performer gender filter to allow selection of multiple genders (`IS`) or performers with no gender specified (`IS NULL`).

* Add default modifier for circumcised
* Handle existing saved filters
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-01-16 14:47:33 +11:00
CJ
45e2e12594 Improved scene queue (#4448)
* Improved scene queue
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-01-16 14:40:44 +11:00
dependabot[bot]
ec547e8d30 Bump vite from 4.4.11 to 4.4.12 in /ui/v2.5 (#4348)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.11 to 4.4.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.4.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.4.12/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 13:59:17 +11:00
dependabot[bot]
e470dc5f52 Bump golang.org/x/crypto from 0.14.0 to 0.17.0 (#4375)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 13:50:41 +11:00
keenbed
14bde44597 added support for image orientation filter (#4404)
* added support for image orientation filter
* Add orientation filtering to scenes
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-01-16 13:50:17 +11:00
WithoutPants
aeb68a5851 Update changelog 2024-01-15 10:34:59 +11:00
WithoutPants
5cf28cf8af Fix studio name uniqueness validation (#4454) 2024-01-14 12:52:16 +11:00
DingDongSoLong4
08b73581a6 Don't retry failed autoplay (#4450) 2024-01-13 10:50:38 +11:00
WithoutPants
95a2c8d13f Update changelog for bugfix release 2024-01-10 11:21:06 +11:00
DingDongSoLong4
0b131f76df Fix scene marker merging (#4446) 2024-01-10 10:25:05 +11:00
WithoutPants
6271f18979 Fix error when creating/updating performer with alias == name (#4443)
* Filter out performer aliases that match the name
* Validate when creating/updating performer in stash-box task
2024-01-09 14:57:49 +11:00
WithoutPants
ca976a0994 Don't bail on error when scraping all (#4442) 2024-01-09 11:39:00 +11:00
DingDongSoLong4
9859ec61fb Calculate DetailImage fallback width using rem (#4441) 2024-01-09 11:11:46 +11:00
DingDongSoLong4
a998497004 Hide tag input when set tags is disabled (#4440) 2024-01-09 11:09:42 +11:00
bayured
f5e3fe77b7 Update FieldStrategyOverwrite to work when scene has no existing URL (#4412) 2024-01-09 10:23:29 +11:00
WithoutPants
743ab9a52c Sort plugin settings (#4435) 2024-01-09 09:32:26 +11:00
WithoutPants
d23cecfc18 Disable select all checkbox for plugin sources (#4434) 2024-01-09 09:32:16 +11:00
DingDongSoLong4
d8990e655d Fix settings tab links (#4430) 2024-01-08 12:08:09 +11:00
DingDongSoLong4
5b9a96b843 Scene queue autoplay (#4428)
* Remove unnecessary undefined checks
* Respect autostartVideoOnPlaySelected in scene queue
2024-01-08 12:04:30 +11:00
CJ
b968aa3f31 Fixes package manger head border (#4420) 2024-01-08 11:54:14 +11:00
DingDongSoLong4
910c7025dc Fix scraped performer alias matching (#4432) 2024-01-08 11:50:31 +11:00
bayured
ea503833c5 Add join to intCriterionHandler (#4414)
* Add join to intCriterionHandler
* Add join to floatCriterionHandler
2024-01-08 11:48:16 +11:00
cc1234475
6848dec5f4 Add CSP to plugin from the yaml file (#4424) 2024-01-08 11:45:55 +11:00
WithoutPants
bd7d4ac7ff Update changelog for bugfix release 2023-12-29 14:29:49 +11:00
WithoutPants
5a6504b4ba Workaround setting protocol on external player url (#4403) 2023-12-29 14:26:30 +11:00
WithoutPants
f8a93789bb Use 8.14.3-r0 of vips (#4402) 2023-12-28 18:34:34 +11:00
WithoutPants
82cbeff9b5 Revert modal button order change (#4400) 2023-12-28 13:56:05 +11:00
DingDongSoLong4
f32d60f208 Update macOS readme links (#4347)
* Update readme macOS download links

* Update docker readme to point to develop branch
2023-12-27 10:57:28 +02:00
DingDongSoLong4
3e9bd8507f Fix Scene Tagger config blacklist (#4396)
* Refactoring
* Add item on Enter
* Don't add duplicate items
2023-12-27 11:02:43 +11:00
DingDongSoLong4
6ee7e6112b Refresh package managers after setup (#4397)
* Refresh package managers after setup
* Fix default plugins/scrapers paths
2023-12-27 10:57:10 +11:00
DingDongSoLong4
9bd36408ee Fix saved filter UI bugs (#4394)
* Fix missing intl strings
* Fix saved filter list z-index
* Fix saved filter list double scrollbar
* Display error inside filter list
* Filter out nonexistent saved filter rows in FrontPageConfig
2023-12-26 16:03:55 +11:00
DingDongSoLong4
0cdea209bb Fix stats page SUM error (#4390) 2023-12-22 12:40:05 +02:00
WithoutPants
e1782d094d Ensure plugin scripts are loaded in correct order (#4388) 2023-12-22 15:10:21 +11:00
WithoutPants
d258976358 Update changelog 2023-12-22 14:13:29 +11:00
WithoutPants
afda6decf2 Support setting file fingerprints (#4376)
* Support setting file fingerprints
* Disallow modifying managed hashes
2023-12-22 14:07:10 +11:00
DingDongSoLong4
a1bd7cf817 Package manager UI-related tweaks (#4382)
* Add Plugins Path setting
* Fix/improve cache invalidation
* Hide load error when collapsing package source
* Package manager style tweaks
* Show error if installed packages query failed
* Prevent "No packages found" flicker
* Show <unknown> if empty version
* Always show latest version, highlight if new version available
* Fix issues with non-unique cross-source package ids
* Don't wrap id, version and date
* Decrease collapse button padding
* Display description for scraper packages
* Fix default packages population
* Change default package path to community
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-12-22 14:05:53 +11:00
WithoutPants
23b4d4f1e0 Show confirmation when deleting via keybind (#4387) 2023-12-22 10:07:02 +11:00
DingDongSoLong4
d0d0d1e11f Include image total O-Count on stats page (#4386) 2023-12-21 16:19:41 +11:00
WithoutPants
e304d981d0 Add changelog for 0.24.0 (#4380) 2023-12-20 13:32:35 +11:00
Maista
e8af3c8e98 Set PYTHONPATH environment variable for Python script scrapers (#4372)
* Set PYTHONPATH environment variable for Python script scrapers
* Convert PYTHONPATH to absolute
* Generalise and apply to plugins

---------

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-12-20 13:32:19 +11:00
WithoutPants
8c922ed9e1 Fix studio svg scaling (#4371) 2023-12-20 13:06:57 +11:00
WithoutPants
c9501ef881 Default package sources (#4364)
* Set default package sources
* Add release notes
* Add documentation
* Index URL -> Source URL
2023-12-18 09:45:21 +11:00
DingDongSoLong4
26c3873122 Use int64 for sample-related FFProbeStream fields (#4361) 2023-12-14 13:31:58 +11:00
DingDongSoLong4
2ef2d89b06 Fix desktop notification duration formatting (#4358) 2023-12-14 13:31:20 +11:00
DingDongSoLong4
43a9df8621 Fix CI pip externally-managed-environment error (#4360)
* Add missing Makefile PHONY target
* Add Dockerfile-CUDA vim modeline
* Fix CI pip externally-managed-environment error
2023-12-14 13:29:44 +11:00
Emilo2
7a9cb548ab Merge url's in scene tagger instead of replacing (#4343) 2023-12-12 11:47:48 +11:00
DingDongSoLong4
74ddfa47e9 Refactor and fix stashbox submit dialog (#4355) 2023-12-12 11:45:52 +11:00
DingDongSoLong4
d37de0e49b Fix crash on blank aliases/urls (#4344)
* Fix crash on blank alias/url
* Fix StringListInput clear issue
2023-12-12 11:28:00 +11:00
WithoutPants
eca5838ce0 Add freebsd to releases (#4341) 2023-12-04 14:27:34 +11:00
DingDongSoLong4
ccb1b7c3c4 Fix exit from systray (#4337) 2023-12-02 17:44:20 +02:00
DingDongSoLong4
d4ef182871 Remove railsTimeLayout from ParseDateStringAsTime (#4333) 2023-12-01 22:28:38 +02:00
DingDongSoLong4
d24b52ae7f Fix nil deference when generating markers by ID (#4335) 2023-12-01 22:28:18 +02:00
anonymous-ants
46e1715a59 Update MacOS dev preview URL (#4330) 2023-11-30 21:03:03 +02:00
WithoutPants
5ebd3b3568 Fix issues navigating scene queue (#4325)
* Fix issues navigating between queue scenes
* Fix queue next/previous button display
* Autplay when clicking scene in queue
* Sticky queue controls
2023-11-30 10:45:45 +11:00
vt-idiot
54461aa140 Update vrmode.ts (#3799)
* Update vrmode.ts

Enabling the most common VR projection (180_LR) and an older but no longer used one (360_TB) in the UI.

* Downgrade videojs-vr
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-11-29 15:01:35 +11:00
WithoutPants
1659c8f185 Fix navbar styling on smaller viewports (#4324) 2023-11-29 12:38:39 +11:00
WithoutPants
599deb71b6 Use visual files to populate image file names (#4323) 2023-11-29 12:38:28 +11:00
bob123491234
413311711f Add Details, Studio Code, and Photographer to Images (#4217)
* Add Details, Code, and Photographer to Images
* Add date and details to image card
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-11-28 15:45:07 +11:00
bob123491234
d1018b4c5d Add Studio Code and Photographer to Galleries. (#4195)
* Added Studio Code and Photographer to Galleries
* Fix gallery display on mobile
* Fixed potential panic when scraping with a bad configuration
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-11-28 15:05:33 +11:00
DingDongSoLong4
b78771dbcd Manager refactor, part 1 (#4298)
* Move BackupDatabase and AnonymiseDatabase to internal/manager
* Rename config.Instance to config.Config
* Rename FFMPEG
* Rework manager and initialization process
* Fix Makefile
* Tweak phasher
* Fix config races
* Fix setup error not clearing
2023-11-28 13:56:46 +11:00
DingDongSoLong4
fc1fc20df4 Setup tweaks (#4304) 2023-11-28 13:56:07 +11:00
WithoutPants
b915428f06 UI Plugin API (#4256)
* Add page registration
* Add example plugin
* First version of proper react plugins
* Make reference react plugin
* Add patching functions
* Add tools link poc
* NavItem poc
* Add loading hook for lazily loaded components
* Add documentation
2023-11-28 13:06:44 +11:00
WithoutPants
11be56cc42 UI plugin dependencies (#4307)
* Add requires field to UI plugin config
* Use defer instead of async for useScript
* Load plugins based on dependency
* Document new field
2023-11-27 13:41:04 +11:00
WithoutPants
910ff27730 Convert legacy parsed date to UTC 2023-11-27 13:05:29 +11:00
WithoutPants
11a1e49292 Remove timezone from package date format (#4313)
* Use UTC date for manifest
* Use intl to format package dates
2023-11-27 12:13:01 +11:00
DingDongSoLong4
0e7c58a5a6 Fix gallery export panic (#4311)
* Load URLs when exporting galleries
* Remove err.Error()
* Log gallery DisplayName rather than PrimaryChecksum
2023-11-27 10:43:34 +11:00
WithoutPants
a8140c11ec Cache package list in memory instead of filesystem (#4309) 2023-11-23 14:16:13 +11:00
Maista
0dcd58763f Log more when resolving Python (#4185)
* Log more when resolving Python

Users often have problems configuring their Python installations

* Convert if-else ladder to switch statement
* Consolidate Python resolution

Adds additional logging to plugin tasks to
align with the logging that scrapers output.
2023-11-22 10:04:15 +11:00
WithoutPants
987fa80786 Scraper and plugin manager (#4242)
* Add package manager
* Add SettingModal validate
* Reverse modal button order
* Add plugin package management
* Refactor ClearableInput
2023-11-22 10:01:11 +11:00
elkorol
d95ef4059a Added various tooltips (#4264) 2023-11-22 09:53:05 +11:00
DingDongSoLong4
13a24a634d Ignore mousemove when just clicking (#4302) 2023-11-22 09:52:36 +11:00
DingDongSoLong4
b0a34a3dc0 Update compiler image in golangci-lint workflow 2023-11-21 22:40:49 +02:00
DingDongSoLong4
343660c378 Fix build Dockerfile after #4153 2023-11-21 22:08:00 +02:00
DingDongSoLong4
611a1e7854 Fix Ubuntu dependencies (#4299) 2023-11-20 14:43:49 +11:00
DingDongSoLong4
2ea35c4dbc Minor UI tweaks (#4297)
* Don't render PluginTasks if no tasks available
* Improve query refetching
2023-11-20 14:19:43 +11:00
DingDongSoLong4
f9e11813f0 Toast fixes/refactoring (#4289)
* Memoize Toast functions
* Rename Toast.success to Toast.toast, add new Toast.success
* Disable prefer-destructuring on AssignmentExpression
2023-11-20 14:14:34 +11:00
DingDongSoLong4
049a1b15c3 Add fingerprint resolver (#4287)
* Refactor into internal/api/models.go
* Add file fingerprint resolver
2023-11-20 13:09:12 +11:00
InfiniteStash
a0f33e3dab Add filtering to folder select (#4277) 2023-11-20 13:00:28 +11:00
yer2
61f4d8bd12 Duplicate checker option for selecting highest resolution (#4286)
* Added duplicate checker dropdown menu option to allow selecting all except highest resolution.
2023-11-20 12:54:59 +11:00
DingDongSoLong4
959f2531fd Form-related fixes, improvements and refactoring (#4283)
* Fix another validateDOMNesting error
* Fix React.forwardRef error
* Fix encoding_image intl message
* Return null instead of undefined from RatingSystem
* DurationInput tweaks
* DateInput tweaks, remove unused utils functions
* Refactor and deduplicate edit form rendering
* Improve/fix yup validation
2023-11-20 12:42:26 +11:00
DingDongSoLong4
65b416a2d9 Fix FreeBSD cross-compilation (#4251)
* Cleanup compiler container
* Fix FreeBSD cross-compilation
* Bump compiler version
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-11-20 11:26:57 +11:00
WithoutPants
222475df82 Plugin assets, external scripts and CSP overrides (#4260)
* Add assets for plugins
* Move plugin javascript and css into separate endpoints
* Allow loading external scripts
* Add csp overrides
* Only include enabled plugins
* Move URLMap to utils
* Use URLMap for assets
* Add documentation
2023-11-19 10:41:16 +11:00
DingDongSoLong4
4dd4c3c658 Improved/fixed macOS support (#4153)
* Fix macOS notifications
* Change CFBundleIdentifier to match domain
* Distribute Stash.app
* Also build universal phasher binary
* Fix binary name in check_version.go
* Expose GOOS, working dir and home dir in systemStatus endpoint
* Disable setup in working directory when running Stash.app
* More Makefile improvements, remove unused scripts
* Improve READMEs and documentation
2023-11-19 10:36:13 +11:00
DingDongSoLong4
72779e618d Fix batch performer tag panic (#4281) 2023-11-12 03:50:09 +02:00
DingDongSoLong4
4715c5ebb2 Fix performer validation (#4248)
* Fix performer validation
* Add tests
* Rename QueryCount argument
* Minor refactoring
* Add duplicate alias validation
* Make UI alias validation also case-insensitive
2023-11-02 16:23:54 +11:00
DingDongSoLong4
d96558704a Improve random sorting algorithm (#4246) 2023-11-02 16:13:56 +11:00
elkorol
795af64e8e Fixed nesting anchor elements (#4255) 2023-11-02 15:58:03 +11:00
DingDongSoLong4
9621213424 Genericise sliceutil functions (#4253)
* Genericise sliceutil.SliceSame
* Genericise intslice functions
* Genericise stringutil functions
2023-11-02 08:58:32 +11:00
Ian McKenzie
cc6673f276 Add OpenBSD development instructions (#4243) 2023-10-26 16:26:33 +11:00
its-josh4
2b8c2534dd Update a number of dependencies (incl. CVE fixes) (#4107)
* Update a number of dependencies (incl. CVE fixes)

Includes some dependencies that were upgraded in #4106 as well as a few more dependencies.

Some deps that have been upgraded had CVEs.

Notably, upgrades deprecated dependencies such as:
- `github.com/go-chi/chi` (replaced with `/v5`)
- `github.com/gofrs/uuid` (replaced with `/v5`)
- `github.com/hashicorp/golang-lru` (replaced with `/v2` which uses generics)

* Upgraded a few more deps

* lint

* reverted yaml library to v2

* remove unnecessary mod replace

* Update chromedp

Fixes #3733
2023-10-26 16:24:32 +11:00
WithoutPants
552f86586a Show performer image in select (#4227) 2023-10-26 15:01:49 +11:00
DingDongSoLong4
c4d7a7ab2c Accept incorrectly insensitivised frontpage config keys (#4237) 2023-10-26 15:01:11 +11:00
WithoutPants
298f3d4e19 Lazy load images (#4228)
* Add lazy loading for many images
* Load sprites on first hover of scrubber
2023-10-23 16:52:56 +11:00
DingDongSoLong4
87bdbb2058 Vite dev server authentication tweaks (#4234)
* Add VITE_APP_PLATFORM_URL, error on dev server auth
* Remove experimentalDeepDynamicChunkOptimization
2023-10-23 16:52:02 +11:00
its-josh4
b99d16b712 Some cosmetic fixes from #4106 (#4236) 2023-10-23 08:20:41 +11:00
DingDongSoLong4
24984da16e Fix UI config conversion yet again (#4128) 2023-10-19 10:21:03 +11:00
WithoutPants
2b8718100b Plugin settings (#4143)
* Add backend support for plugin settings
* Add plugin settings config
* Add UI support for plugin settings
2023-10-18 14:09:13 +11:00
DingDongSoLong4
06d8353f4f Fix tagger result styling (#4222)
* Fix tagger result styling
* Fix DEFAULT_SLIDESHOW_DELAY
2023-10-18 13:14:11 +11:00
dependabot[bot]
939bb422d1 Bump @babel/traverse from 7.20.13 to 7.23.2 in /ui/v2.5 (#4226)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.20.13 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-18 13:09:06 +11:00
WithoutPants
339b9fcc16 Upgrade vite to fix ui-start issues (#4225) 2023-10-18 12:29:20 +11:00
WithoutPants
a83dfff5ff Show stash-ids with their endpoint (#4216) 2023-10-18 07:56:49 +11:00
Ian McKenzie
21baa23fc5 Fix unix build flags to be friendly to niche operating systems (#4214) 2023-10-17 12:00:19 +11:00
WithoutPants
b6714fafba Remove deprecated graphql fields (#4064)
* Remove deprecated list*Scrapers queries
* Remove other deprecated query resolvers
* Remove deprecated config fields
* Remove deprecated gallery fields
* Remove deprecated image fields
* Remove deprecated movie fields
* Remove deprecated performer fields
* Document scrape function issue
* Remove deprecated studio fields
* Remove deprecated scan input fields
* Remove deprecated scene fields
* Remove deprecated fields from filters
* Remove scene.file_mod_time
2023-10-17 11:59:35 +11:00
WithoutPants
a9ab1fcca7 Fix performer stash ids being overwritten in performer tagger (#4215) 2023-10-17 11:42:57 +11:00
WithoutPants
5e0f27bed2 Don't unset organized if tagger flag is false (#4213) 2023-10-17 09:38:32 +11:00
Flashy78
789de2d5f6 Errors for performer/studio non-unique names (#4178)
* Errors for performer/studio non-unique names
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-10-16 17:26:43 +11:00
WithoutPants
2fd7141f0f Javascript scraper postprocess (#4200)
* Add javascript post-process action
* Add documentation
2023-10-16 17:17:36 +11:00
bob123491234
bdf705fe7c Fix images total megapixels and filesize queries (#4203) 2023-10-16 17:16:40 +11:00
Flashy78
2ec948a836 Add disambiguation and links to tagger results (#4180)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2023-10-16 16:35:08 +11:00
dogwithakeyboard
7605eec6da scene framerate filter (#4161) 2023-10-16 16:28:28 +11:00
0xb0af
8eb069054e improve tooltip (#4198) 2023-10-16 16:16:53 +11:00
WithoutPants
b6808dc714 Add support for disabling plugins (#4141)
* Move timestamp to own file
* Backend changes
* UI changes
2023-10-16 16:15:12 +11:00
Flashy78
e5af37efbc Tagger match performer by alias (#4182) 2023-10-16 14:39:41 +11:00
InfiniteStash
409f8fc70c Fix type issues (#4176) 2023-10-16 14:34:54 +11:00
dependabot[bot]
90dfaf668b Bump graphql from 16.6.0 to 16.8.1 in /ui/v2.5 (#4140)
Bumps [graphql](https://github.com/graphql/graphql-js) from 16.6.0 to 16.8.1.
- [Release notes](https://github.com/graphql/graphql-js/releases)
- [Commits](https://github.com/graphql/graphql-js/compare/v16.6.0...v16.8.1)

---
updated-dependencies:
- dependency-name: graphql
  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-10-16 14:29:40 +11:00
dependabot[bot]
bc261f789a Bump postcss from 8.4.25 to 8.4.31 in /ui/v2.5 (#4179)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.25 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.25...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 14:29:00 +11:00
dependabot[bot]
9552273478 Bump golang.org/x/net from 0.7.0 to 0.17.0 (#4192)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.7.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.7.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  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-10-16 14:28:33 +11:00
DingDongSoLong4
33f2ebf2a3 Model refactor, part 3 (#4152)
* Remove manager.Repository
* Refactor other repositories
* Fix tests and add database mock
* Add AssertExpectations method
* Refactor routes
* Move default movie image to internal/static and add convenience methods
* Refactor default performer image boxes
2023-10-16 14:26:34 +11:00
Emilo2
40bcb4baa5 Fix url's from gallery scrapers (#4206)
* Fill in urls array when gallery scraper provides only single url
2023-10-16 13:27:42 +11:00
WithoutPants
479e716385 Update changelog for bugfix release 2023-10-14 10:46:49 +11:00
WithoutPants
06c9d6f554 Fix performer select not working correctly in scrape dialog (#4199) 2023-10-13 15:43:23 +11:00
RPaetau
1f0f5eb49c slideshow delay default values changed from 5000 to 5 seconds (#4186)
* Update Lightbox.tsx to also change default delay here to 5 sec instead of 5000
* Update config.go to set default slideshow delay from 5000 sec to 5 sec
2023-10-10 14:22:39 +11:00
Emilo2
c109a58231 Fix gallery url scraping (#4187) 2023-10-10 14:08:48 +11:00
722 changed files with 46317 additions and 27909 deletions

View File

@@ -17,7 +17,7 @@
# GraphQL generated output
pkg/models/generated_*.go
ui/v2.5/src/core/generated-*.tsx
ui/v2.5/src/core/generated-graphql.ts
####
# Jetbrains

View File

@@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true
env:
COMPILER_IMAGE: stashapp/compiler:7
COMPILER_IMAGE: stashapp/compiler:8
jobs:
build:
@@ -84,13 +84,13 @@ jobs:
- name: Compile for all supported platforms
run: |
docker exec -t build /bin/bash -c "make cross-compile-windows"
docker exec -t build /bin/bash -c "make cross-compile-macos-intel"
docker exec -t build /bin/bash -c "make cross-compile-macos-applesilicon"
docker exec -t build /bin/bash -c "make cross-compile-linux"
docker exec -t build /bin/bash -c "make cross-compile-linux-arm64v8"
docker exec -t build /bin/bash -c "make cross-compile-linux-arm32v7"
docker exec -t build /bin/bash -c "make cross-compile-linux-arm32v6"
docker exec -t build /bin/bash -c "make build-cc-windows"
docker exec -t build /bin/bash -c "make build-cc-macos"
docker exec -t build /bin/bash -c "make build-cc-linux"
docker exec -t build /bin/bash -c "make build-cc-linux-arm64v8"
docker exec -t build /bin/bash -c "make build-cc-linux-arm32v7"
docker exec -t build /bin/bash -c "make build-cc-linux-arm32v6"
docker exec -t build /bin/bash -c "make build-cc-freebsd"
- name: Cleanup build container
run: docker rm -f -v build
@@ -98,7 +98,7 @@ jobs:
- name: Generate checksums
run: |
git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
sha1sum dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
sha1sum dist/Stash.app.zip dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
echo "STASH_VERSION=$(git describe --tags --exclude latest_develop)" >> $GITHUB_ENV
echo "RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_ENV
@@ -110,13 +110,13 @@ jobs:
name: stash-win.exe
path: dist/stash-win.exe
- name: Upload OSX binary
- name: Upload macOS binary
# only upload binaries for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v2
with:
name: stash-macos-intel
path: dist/stash-macos-intel
name: stash-macos
path: dist/stash-macos
- name: Upload Linux binary
# only upload binaries for pull requests
@@ -139,13 +139,14 @@ jobs:
automatic_release_tag: latest_develop
title: "${{ env.STASH_VERSION }}: Latest development build"
files: |
dist/stash-macos-intel
dist/stash-macos-applesilicon
dist/Stash.app.zip
dist/stash-macos
dist/stash-win.exe
dist/stash-linux
dist/stash-linux-arm64v8
dist/stash-linux-arm32v7
dist/stash-linux-arm32v6
dist/stash-freebsd
CHECKSUMS_SHA1
- name: Master release
@@ -157,13 +158,14 @@ jobs:
token: "${{ secrets.GITHUB_TOKEN }}"
allow_override: true
files: |
dist/stash-macos-intel
dist/stash-macos-applesilicon
dist/Stash.app.zip
dist/stash-macos
dist/stash-win.exe
dist/stash-linux
dist/stash-linux-arm64v8
dist/stash-linux-arm32v7
dist/stash-linux-arm32v6
dist/stash-freebsd
CHECKSUMS_SHA1
gzip: false

View File

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

9
.gitignore vendored
View File

@@ -21,11 +21,6 @@ vendor
# GraphQL generated output
internal/api/generated_*.go
####
# Jetbrains
####
####
# Visual Studio
####
@@ -52,9 +47,6 @@ internal/api/generated_*.go
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Goland Junk
pkg/pkg
####
# Random
####
@@ -64,6 +56,7 @@ node_modules
*.db
/stash
/Stash.app
/phasher
dist
.DS_Store

292
Makefile
View File

@@ -9,9 +9,11 @@ endif
ifdef IS_WIN_SHELL
RM := del /s /q
RMDIR := rmdir /s /q
NOOP := @@
else
RM := rm -f
RMDIR := rm -rf
NOOP := @:
endif
# set LDFLAGS environment variable to any extra ldflags required
@@ -36,7 +38,7 @@ GO_BUILD_FLAGS := $(GO_BUILD_FLAGS)
# set GO_BUILD_TAGS environment variable to any extra build tags required
GO_BUILD_TAGS := $(GO_BUILD_TAGS)
GO_BUILD_TAGS += sqlite_stat4
GO_BUILD_TAGS += sqlite_stat4 sqlite_math_functions
# set STASH_NOLEGACY environment variable or uncomment to disable legacy browser support
# STASH_NOLEGACY := true
@@ -46,33 +48,49 @@ GO_BUILD_TAGS += sqlite_stat4
export CGO_ENABLED := 1
# define COMPILER_IMAGE for cross-compilation docker container
ifndef COMPILER_IMAGE
COMPILER_IMAGE := stashapp/compiler:latest
endif
.PHONY: release
release: pre-ui generate ui build-release
# targets to set various build flags
# use combinations on the make command-line to configure a build, e.g.:
# for a static-pie release build: `make flags-static-pie flags-release stash`
# for a static windows debug build: `make flags-static-windows stash`
# $(NOOP) prevents "nothing to be done" warnings
.PHONY: flags-release
flags-release:
flags-release:
$(NOOP)
$(eval LDFLAGS += -s -w)
$(eval GO_BUILD_FLAGS += -trimpath)
.PHONY: flags-pie
flags-pie:
$(NOOP)
$(eval GO_BUILD_FLAGS += -buildmode=pie)
.PHONY: flags-static
flags-static:
flags-static:
$(NOOP)
$(eval LDFLAGS += -extldflags=-static)
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
.PHONY: flags-static-pie
flags-static-pie:
flags-static-pie:
$(NOOP)
$(eval LDFLAGS += -extldflags=-static-pie)
$(eval GO_BUILD_FLAGS += -buildmode=pie)
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
# identical to flags-static-pie, but excluding netgo, which is not needed on windows
.PHONY: flags-static-windows
flags-static-windows:
$(NOOP)
$(eval LDFLAGS += -extldflags=-static-pie)
$(eval GO_BUILD_FLAGS += -buildmode=pie)
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo)
@@ -105,166 +123,141 @@ build-flags: build-info
stash: build-flags
go build $(STASH_OUTPUT) $(BUILD_FLAGS) ./cmd/stash
.PHONY: stash-release
stash-release: flags-release
stash-release: flags-pie
stash-release: stash
.PHONY: stash-release-static
stash-release-static: flags-release
stash-release-static: flags-static-pie
stash-release-static: stash
.PHONY: stash-release-static-windows
stash-release-static-windows: flags-release
stash-release-static-windows: flags-static-windows
stash-release-static-windows: stash
.PHONY: phasher
phasher: build-flags
go build $(PHASHER_OUTPUT) $(BUILD_FLAGS) ./cmd/phasher
.PHONY: phasher-release
phasher-release: flags-release
phasher-release: flags-pie
phasher-release: phasher
.PHONY: phasher-release-static
phasher-release-static: flags-release
phasher-release-static: flags-static-pie
phasher-release-static: phasher
.PHONY: phasher-release-static-windows
phasher-release-static-windows: flags-release
phasher-release-static-windows: flags-static-windows
phasher-release-static-windows: phasher
# builds dynamically-linked debug binaries
.PHONY: build
build: stash phasher
# builds dynamically-linked release binaries
# builds dynamically-linked PIE release binaries
.PHONY: build-release
build-release: stash-release phasher-release
build-release: flags-release flags-pie build
# builds statically-linked release binaries
.PHONY: build-release-static
build-release-static: stash-release-static phasher-release-static
# compile and bundle into Stash.app
# for when on macOS itself
.PHONY: stash-macapp
stash-macapp: STASH_OUTPUT := -o stash
stash-macapp: flags-release flags-pie stash
rm -rf Stash.app
cp -R scripts/macos-bundle Stash.app
mkdir Stash.app/Contents/MacOS
cp stash Stash.app/Contents/MacOS/stash
# build-release-static, but excluding netgo, which is not needed on windows
.PHONY: build-release-static-windows
build-release-static-windows: stash-release-static-windows phasher-release-static-windows
# build-cc- targets should be run within the compiler docker container
# cross-compile- targets should be run within the compiler docker container
.PHONY: cross-compile-windows
cross-compile-windows: export GOOS := windows
cross-compile-windows: export GOARCH := amd64
cross-compile-windows: export CC := x86_64-w64-mingw32-gcc
cross-compile-windows: export CXX := x86_64-w64-mingw32-g++
cross-compile-windows: STASH_OUTPUT := -o dist/stash-win.exe
cross-compile-windows: PHASHER_OUTPUT := -o dist/phasher-win.exe
cross-compile-windows: flags-release
cross-compile-windows: flags-static-windows
cross-compile-windows: build
.PHONY: build-cc-windows
build-cc-windows: export GOOS := windows
build-cc-windows: export GOARCH := amd64
build-cc-windows: export CC := x86_64-w64-mingw32-gcc
build-cc-windows: STASH_OUTPUT := -o dist/stash-win.exe
build-cc-windows: PHASHER_OUTPUT :=-o dist/phasher-win.exe
build-cc-windows: flags-release
build-cc-windows: flags-static-windows
build-cc-windows: build
.PHONY: cross-compile-macos-intel
cross-compile-macos-intel: export GOOS := darwin
cross-compile-macos-intel: export GOARCH := amd64
cross-compile-macos-intel: export CC := o64-clang
cross-compile-macos-intel: export CXX := o64-clang++
cross-compile-macos-intel: STASH_OUTPUT := -o dist/stash-macos-intel
cross-compile-macos-intel: PHASHER_OUTPUT := -o dist/phasher-macos-intel
cross-compile-macos-intel: flags-release
# can't use static build for OSX
cross-compile-macos-intel: flags-pie
cross-compile-macos-intel: build
.PHONY: build-cc-macos-intel
build-cc-macos-intel: export GOOS := darwin
build-cc-macos-intel: export GOARCH := amd64
build-cc-macos-intel: export CC := o64-clang
build-cc-macos-intel: STASH_OUTPUT := -o dist/stash-macos-intel
build-cc-macos-intel: PHASHER_OUTPUT := -o dist/phasher-macos-intel
build-cc-macos-intel: flags-release
# can't use static build for macOS
build-cc-macos-intel: flags-pie
build-cc-macos-intel: build
.PHONY: cross-compile-macos-applesilicon
cross-compile-macos-applesilicon: export GOOS := darwin
cross-compile-macos-applesilicon: export GOARCH := arm64
cross-compile-macos-applesilicon: export CC := oa64e-clang
cross-compile-macos-applesilicon: export CXX := oa64e-clang++
cross-compile-macos-applesilicon: STASH_OUTPUT := -o dist/stash-macos-applesilicon
cross-compile-macos-applesilicon: PHASHER_OUTPUT := -o dist/phasher-macos-applesilicon
cross-compile-macos-applesilicon: flags-release
# can't use static build for OSX
cross-compile-macos-applesilicon: flags-pie
cross-compile-macos-applesilicon: build
.PHONY: build-cc-macos-arm
build-cc-macos-arm: export GOOS := darwin
build-cc-macos-arm: export GOARCH := arm64
build-cc-macos-arm: export CC := oa64e-clang
build-cc-macos-arm: STASH_OUTPUT := -o dist/stash-macos-arm
build-cc-macos-arm: PHASHER_OUTPUT := -o dist/phasher-macos-arm
build-cc-macos-arm: flags-release
# can't use static build for macOS
build-cc-macos-arm: flags-pie
build-cc-macos-arm: build
.PHONY: build-cc-macos
build-cc-macos:
make build-cc-macos-arm
make build-cc-macos-intel
# Combine into universal binaries
lipo -create -output dist/stash-macos dist/stash-macos-intel dist/stash-macos-arm
rm dist/stash-macos-intel dist/stash-macos-arm
lipo -create -output dist/phasher-macos dist/phasher-macos-intel dist/phasher-macos-arm
rm dist/phasher-macos-intel dist/phasher-macos-arm
.PHONY: cross-compile-macos
cross-compile-macos:
rm -rf dist/Stash.app dist/Stash-macos.zip
make cross-compile-macos-applesilicon
make cross-compile-macos-intel
# Combine into one universal binary
lipo -create -output dist/stash-macos-universal dist/stash-macos-intel dist/stash-macos-applesilicon
rm dist/stash-macos-intel dist/stash-macos-applesilicon
# Place into bundle and zip up
rm -rf dist/Stash.app
cp -R scripts/macos-bundle dist/Stash.app
mkdir dist/Stash.app/Contents/MacOS
mv dist/stash-macos-universal dist/Stash.app/Contents/MacOS/stash
cd dist && zip -r Stash-macos.zip Stash.app && cd ..
cp dist/stash-macos dist/Stash.app/Contents/MacOS/stash
cd dist && rm -f Stash.app.zip && zip -r Stash.app.zip Stash.app
rm -rf dist/Stash.app
.PHONY: cross-compile-freebsd
cross-compile-freebsd: export GOOS := freebsd
cross-compile-freebsd: export GOARCH := amd64
cross-compile-freebsd: STASH_OUTPUT := -o dist/stash-freebsd
cross-compile-freebsd: PHASHER_OUTPUT := -o dist/phasher-freebsd
cross-compile-freebsd: flags-release
cross-compile-freebsd: flags-static-pie
cross-compile-freebsd: build
.PHONY: build-cc-freebsd
build-cc-freebsd: export GOOS := freebsd
build-cc-freebsd: export GOARCH := amd64
build-cc-freebsd: export CC := clang -target x86_64-unknown-freebsd12.0 --sysroot=/opt/cross-freebsd
build-cc-freebsd: STASH_OUTPUT := -o dist/stash-freebsd
build-cc-freebsd: PHASHER_OUTPUT := -o dist/phasher-freebsd
build-cc-freebsd: flags-release
build-cc-freebsd: flags-static-pie
build-cc-freebsd: build
.PHONY: cross-compile-linux
cross-compile-linux: export GOOS := linux
cross-compile-linux: export GOARCH := amd64
cross-compile-linux: STASH_OUTPUT := -o dist/stash-linux
cross-compile-linux: PHASHER_OUTPUT := -o dist/phasher-linux
cross-compile-linux: flags-release
cross-compile-linux: flags-static-pie
cross-compile-linux: build
.PHONY: build-cc-linux
build-cc-linux: export GOOS := linux
build-cc-linux: export GOARCH := amd64
build-cc-linux: STASH_OUTPUT := -o dist/stash-linux
build-cc-linux: PHASHER_OUTPUT := -o dist/phasher-linux
build-cc-linux: flags-release
build-cc-linux: flags-static-pie
build-cc-linux: build
.PHONY: cross-compile-linux-arm64v8
cross-compile-linux-arm64v8: export GOOS := linux
cross-compile-linux-arm64v8: export GOARCH := arm64
cross-compile-linux-arm64v8: export CC := aarch64-linux-gnu-gcc
cross-compile-linux-arm64v8: STASH_OUTPUT := -o dist/stash-linux-arm64v8
cross-compile-linux-arm64v8: PHASHER_OUTPUT := -o dist/phasher-linux-arm64v8
cross-compile-linux-arm64v8: flags-release
cross-compile-linux-arm64v8: flags-static-pie
cross-compile-linux-arm64v8: build
.PHONY: build-cc-linux-arm64v8
build-cc-linux-arm64v8: export GOOS := linux
build-cc-linux-arm64v8: export GOARCH := arm64
build-cc-linux-arm64v8: export CC := aarch64-linux-gnu-gcc
build-cc-linux-arm64v8: STASH_OUTPUT := -o dist/stash-linux-arm64v8
build-cc-linux-arm64v8: PHASHER_OUTPUT := -o dist/phasher-linux-arm64v8
build-cc-linux-arm64v8: flags-release
build-cc-linux-arm64v8: flags-static-pie
build-cc-linux-arm64v8: build
.PHONY: cross-compile-linux-arm32v7
cross-compile-linux-arm32v7: export GOOS := linux
cross-compile-linux-arm32v7: export GOARCH := arm
cross-compile-linux-arm32v7: export GOARM := 7
cross-compile-linux-arm32v7: export CC := arm-linux-gnueabi-gcc -march=armv7-a
cross-compile-linux-arm32v7: STASH_OUTPUT := -o dist/stash-linux-arm32v7
cross-compile-linux-arm32v7: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v7
cross-compile-linux-arm32v7: flags-release
cross-compile-linux-arm32v7: flags-static
cross-compile-linux-arm32v7: build
.PHONY: build-cc-linux-arm32v7
build-cc-linux-arm32v7: export GOOS := linux
build-cc-linux-arm32v7: export GOARCH := arm
build-cc-linux-arm32v7: export GOARM := 7
build-cc-linux-arm32v7: export CC := arm-linux-gnueabi-gcc -march=armv7-a
build-cc-linux-arm32v7: STASH_OUTPUT := -o dist/stash-linux-arm32v7
build-cc-linux-arm32v7: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v7
build-cc-linux-arm32v7: flags-release
build-cc-linux-arm32v7: flags-static
build-cc-linux-arm32v7: build
.PHONY: cross-compile-linux-arm32v6
cross-compile-linux-arm32v6: export GOOS := linux
cross-compile-linux-arm32v6: export GOARCH := arm
cross-compile-linux-arm32v6: export GOARM := 6
cross-compile-linux-arm32v6: export CC := arm-linux-gnueabi-gcc
cross-compile-linux-arm32v6: STASH_OUTPUT := -o dist/stash-linux-arm32v6
cross-compile-linux-arm32v6: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v6
cross-compile-linux-arm32v6: flags-release
cross-compile-linux-arm32v6: flags-static
cross-compile-linux-arm32v6: build
.PHONY: build-cc-linux-arm32v6
build-cc-linux-arm32v6: export GOOS := linux
build-cc-linux-arm32v6: export GOARCH := arm
build-cc-linux-arm32v6: export GOARM := 6
build-cc-linux-arm32v6: export CC := arm-linux-gnueabi-gcc
build-cc-linux-arm32v6: STASH_OUTPUT := -o dist/stash-linux-arm32v6
build-cc-linux-arm32v6: PHASHER_OUTPUT := -o dist/phasher-linux-arm32v6
build-cc-linux-arm32v6: flags-release
build-cc-linux-arm32v6: flags-static
build-cc-linux-arm32v6: build
.PHONY: cross-compile-all
cross-compile-all:
make cross-compile-windows
make cross-compile-macos-intel
make cross-compile-macos-applesilicon
make cross-compile-linux
make cross-compile-linux-arm64v8
make cross-compile-linux-arm32v7
make cross-compile-linux-arm32v6
.PHONY: build-cc-all
build-cc-all:
make build-cc-windows
make build-cc-macos
make build-cc-linux
make build-cc-linux-arm64v8
make build-cc-linux-arm32v7
make build-cc-linux-arm32v6
make build-cc-freebsd
.PHONY: touch-ui
touch-ui:
@@ -360,14 +353,6 @@ endif
ui: ui-env
cd ui/v2.5 && yarn build
.PHONY: ui-nolegacy
ui-nolegacy: STASH_NOLEGACY := true
ui-nolegacy: ui
.PHONY: ui-sourcemaps
ui-sourcemaps: STASH_SOURCEMAPS := true
ui-sourcemaps: ui
.PHONY: ui-start
ui-start: ui-env
cd ui/v2.5 && yarn start --host
@@ -398,3 +383,16 @@ docker-build: build-info
.PHONY: docker-cuda-build
docker-cuda-build: build-info
docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/cuda-build -f docker/build/x86_64/Dockerfile-CUDA .
# start the build container - for cross compilation
# this is adapted from the github actions build.yml file
.PHONY: start-compiler-container
start-compiler-container:
docker run -d --name build --mount type=bind,source="$(PWD)",target=/stash,consistency=delegated $(EXTRA_CONTAINER_ARGS) -w /stash $(COMPILER_IMAGE) tail -f /dev/null
# run the cross-compilation using
# docker exec -t build /bin/bash -c "make build-cc-<platform>"
.PHONY: remove-compiler-container
remove-compiler-container:
docker rm -f -v build

View File

@@ -24,15 +24,22 @@ For further information you can consult the [documentation](https://docs.stashap
# Installing Stash
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> MacOS| <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS | <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
:---:|:---:|:---:|:---:
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release (Apple Silicon)](https://github.com/stashapp/stash/releases/latest/download/stash-macos-applesilicon) <br /> <sup><sub>[Development Preview (Apple Silicon)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-macos-applesilicon)</sub></sup> <br />[Latest Release (Intel)](https://github.com/stashapp/stash/releases/latest/download/stash-macos-intel) <br /> <sup><sub>[Development Preview (Intel)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-macos-intel)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub> [Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release](https://github.com/stashapp/stash/releases/latest/download/Stash.app.zip) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/Stash.app.zip)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub>[Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
Download links for other platforms and architectures are available on the [Releases page](https://github.com/stashapp/stash/releases).
## First Run
#### Windows Users: Security Prompt
Running the app might present a security prompt since the binary isn't yet signed. Bypass this by clicking "more info" and then the "run anyway" button.
#### FFMPEG
Stash requires ffmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
#### Windows/macOS Users: Security Prompt
On Windows or macOS, running the app might present a security prompt since the binary isn't yet signed.
On Windows, bypass this by clicking "more info" and then the "run anyway" button. On macOS, Control+Click the app, click "Open", and then "Open" again.
#### FFmpeg
Stash requires FFmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
# Usage
@@ -41,9 +48,11 @@ Stash is a web-based application. Once the application is running, the interface
On first run, Stash will prompt you for some configuration options and media directories to index, called "Scanning" in Stash. After scanning, your media will be available for browsing, curating, editing, and tagging.
Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en/Scraping.md), which integrate directly into Stash.
Many community-maintained scrapers are available for download from [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers). The community also maintains StashDB, a crowd-sourced repository of scene, studio, and performer information, that can automatically identify much of a typical media collection. Inquire in the Discord for details. Identifying an entire collection will typically require a mix of multiple sources.
Stash can pull metadata (performers, tags, descriptions, studios, and more) directly from many sites through the use of [scrapers](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en/Scraping.md), which integrate directly into Stash. Identifying an entire collection will typically require a mix of multiple sources:
- The project maintains [StashDB](https://stashdb.org/), a crowd-sourced repository of scene, studio, and performer information. Connecting it to Stash will allow you to automatically identify much of a typical media collection. It runs on our stash-box software and is primarily focused on mainstream digital scenes and studios. Instructions, invite codes, and more can be found in this guide to [Accessing StashDB](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stashdb/).
- Several community-managed stash-box databases can also be connected to Stash in a similar manner. Each one serves a slightly different niche and follows their own methodology. A rundown of each stash-box, their differences, and the information you need to sign up can be found in this guide to [Accessing Stash-Boxes](https://guidelines.stashdb.org/docs/faq_getting-started/stashdb/accessing-stash-boxes/).
- Many community-maintained scrapers can also be downloaded, installed, and updated from within Stash, allowing you to pull data from a wide range of other websites and databases. They can be found by navigating to Settings -> Metadata Providers -> Available Scrapers -> Community (stable). These can be trickier to use than a stash-box because every scraper works a little differently. For more information, please visit the [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers).
- All of the above methods of scraping data into Stash are also covered in more detail in our [Guide to Scraping](https://docs.stashapp.cc/beginner-guides/guide-to-scraping/).
<sub>[StashDB](http://stashdb.org) is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box).</sub>

View File

@@ -2,7 +2,6 @@
package main
import (
"context"
"fmt"
"os"
@@ -66,13 +65,13 @@ func main() {
}
if len(args) > 1 {
fmt.Fprintln(os.Stderr, "Files will be processed sequentially! Consier using GNU Parallel.")
fmt.Fprintln(os.Stderr, "Files will be processed sequentially! If required, use e.g. GNU Parallel to run concurrently.")
fmt.Fprintf(os.Stderr, "Example: parallel %v ::: *.mp4\n", os.Args[0])
}
ffmpegPath, ffprobePath := ffmpeg.GetPaths(nil)
encoder := ffmpeg.NewEncoder(ffmpegPath)
encoder.InitHWSupport(context.TODO())
// don't need to InitHWSupport, phashing doesn't use hw acceleration
ffprobe := ffmpeg.FFProbe(ffprobePath)
for _, item := range args {

View File

@@ -2,67 +2,154 @@
package main
import (
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"runtime/debug"
"runtime/pprof"
"syscall"
"github.com/spf13/pflag"
"github.com/stashapp/stash/internal/api"
"github.com/stashapp/stash/internal/build"
"github.com/stashapp/stash/internal/desktop"
"github.com/stashapp/stash/internal/log"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/ui"
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
var exitCode = 0
func main() {
defer recoverPanic()
_, err := manager.Initialize()
if err != nil {
panic(err)
}
go func() {
defer recoverPanic()
if err := api.Start(); err != nil {
handleError(err)
} else {
manager.GetInstance().Shutdown(0)
defer func() {
if exitCode != 0 {
os.Exit(exitCode)
}
}()
go handleSignals()
desktop.Start(manager.GetInstance(), &ui.FaviconProvider)
defer recoverPanic()
blockForever()
helpFlag := false
pflag.BoolVarP(&helpFlag, "help", "h", false, "show this help text and exit")
versionFlag := false
pflag.BoolVarP(&versionFlag, "version", "v", false, "show version number and exit")
cpuProfilePath := ""
pflag.StringVar(&cpuProfilePath, "cpuprofile", "", "write cpu profile to file")
pflag.Parse()
if helpFlag {
pflag.Usage()
return
}
if versionFlag {
fmt.Println(build.VersionString())
return
}
cfg, err := config.Initialize()
if err != nil {
exitError(fmt.Errorf("config initialization error: %w", err))
return
}
l := initLog(cfg)
if cpuProfilePath != "" {
if err := initProfiling(cpuProfilePath); err != nil {
exitError(err)
return
}
defer pprof.StopCPUProfile()
}
mgr, err := manager.Initialize(cfg, l)
if err != nil {
exitError(fmt.Errorf("manager initialization error: %w", err))
return
}
defer mgr.Shutdown()
server, err := api.Initialize()
if err != nil {
exitError(fmt.Errorf("api initialization error: %w", err))
return
}
defer server.Shutdown()
exit := make(chan int)
go func() {
err := server.Start()
if !errors.Is(err, http.ErrServerClosed) {
exitError(fmt.Errorf("http server error: %w", err))
exit <- 1
}
}()
go handleSignals(exit)
desktop.Start(exit, &ui.FaviconProvider)
exitCode = <-exit
}
func initLog(cfg *config.Config) *log.Logger {
l := log.NewLogger()
l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel())
logger.Logger = l
return l
}
func initProfiling(path string) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("unable to create CPU profile file: %v", err)
}
if err = pprof.StartCPUProfile(f); err != nil {
return fmt.Errorf("could not start CPU profiling: %v", err)
}
logger.Infof("profiling to %s", path)
return nil
}
func recoverPanic() {
if p := recover(); p != nil {
handleError(fmt.Errorf("Panic: %v", p))
if err := recover(); err != nil {
exitCode = 1
logger.Errorf("panic: %v\n%s", err, debug.Stack())
if desktop.IsDesktop() {
desktop.FatalError(fmt.Errorf("Panic: %v", err))
}
}
}
func handleError(err error) {
func exitError(err error) {
exitCode = 1
logger.Error(err)
if desktop.IsDesktop() {
desktop.FatalError(err)
manager.GetInstance().Shutdown(0)
} else {
panic(err)
}
}
func handleSignals() {
func handleSignals(exit chan<- int) {
// handle signals
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
<-signals
manager.GetInstance().Shutdown(0)
}
func blockForever() {
select {}
exit <- 0
}

View File

@@ -28,7 +28,7 @@ COPY --from=frontend /stash /stash/
RUN make generate-backend
ARG GITHASH
ARG STASH_VERSION
RUN make stash-release
RUN make flags-release flags-pie stash
# Final Runnable Image
FROM alpine:latest

View File

@@ -28,7 +28,7 @@ COPY --from=frontend /stash /stash/
RUN make generate-backend
ARG GITHASH
ARG STASH_VERSION
RUN make stash-release
RUN make flags-release flags-pie stash
# Final Runnable Image
FROM nvidia/cuda:12.0.1-base-ubuntu22.04
@@ -48,3 +48,5 @@ ENV NVIDIA_DRIVER_CAPABILITIES=video,utility
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999
ENTRYPOINT ["docker-entrypoint.sh", "stash"]
# vim: ft=dockerfile

View File

@@ -11,9 +11,14 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-linux-arm32v6; \
FROM --platform=$TARGETPLATFORM alpine:latest AS app
COPY --from=binary /stash /usr/bin/
# vips version 8.15.0-r0 breaks thumbnail generation on arm32v6
# need to use 8.14.3-r0 from alpine 3.18 instead
RUN apk add --no-cache --virtual .build-deps gcc python3-dev musl-dev \
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg vips-tools ruby tzdata \
&& pip install mechanicalsoup cloudscraper bencoder.pyx \
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg ruby tzdata \
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.18/community vips=8.14.3-r0 vips-tools=8.14.3-r0 \
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx \
&& gem install faraday \
&& apk del .build-deps
ENV STASH_CONFIG_FILE=/root/.stash/config.yml

View File

@@ -1 +1 @@
This dockerfile is used by travis to build the stash image. It must be run after cross-compiling - that is, `stash-linux` must exist in the `dist` directory. This image must be built from the `dist` directory.
This Dockerfile is used by CI to build the `stashapp/stash` Docker image. It must be run after cross-compiling - that is, `stash-linux` must exist in the `dist` directory. This image must be built from the `dist` directory.

View File

@@ -2,66 +2,82 @@ FROM golang:1.19
LABEL maintainer="https://discord.gg/2TsNFKt"
# Install tools
RUN apt-get update && apt-get install -y apt-transport-https
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get install -y apt-transport-https ca-certificates gnupg
RUN mkdir -p /etc/apt/keyrings
ADD https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key nodesource.gpg.key
RUN cat nodesource.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && rm nodesource.gpg.key
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
# prevent caching of the key
ADD https://dl.yarnpkg.com/debian/pubkey.gpg yarn.gpg
RUN cat yarn.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
rm yarn.gpg
RUN cat yarn.gpg | gpg --dearmor -o /etc/apt/keyrings/yarn.gpg && rm yarn.gpg
RUN echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && \
apt-get install -y automake autogen cmake \
libtool libxml2-dev uuid-dev libssl-dev bash \
patch make tar xz-utils bzip2 gzip zlib1g-dev sed cpio \
gcc-10-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
gcc-arm-linux-gnueabi libc-dev-armel-cross linux-libc-dev-armel-cross \
gcc-arm-linux-gnueabihf libc-dev-armhf-cross \
gcc-aarch64-linux-gnu libc-dev-arm64-cross \
nodejs yarn zip --no-install-recommends || exit 1; \
rm -rf /var/lib/apt/lists/*;
apt-get install -y --no-install-recommends \
git make tar bash nodejs yarn zip \
clang llvm-dev cmake patch libxml2-dev uuid-dev libssl-dev xz-utils \
bzip2 gzip sed cpio libbz2-dev zlib1g-dev \
gcc-mingw-w64 \
gcc-arm-linux-gnueabi libc-dev-armel-cross linux-libc-dev-armel-cross \
gcc-aarch64-linux-gnu libc-dev-arm64-cross && \
rm -rf /var/lib/apt/lists/*;
# Cross compile setup
ENV OSX_SDK_VERSION 11.3
ENV OSX_SDK_DOWNLOAD_FILE=MacOSX${OSX_SDK_VERSION}.sdk.tar.xz
ENV OSX_SDK_DOWNLOAD_URL=https://github.com/phracker/MacOSX-SDKs/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
ENV OSX_SDK_SHA=cd4f08a75577145b8f05245a2975f7c81401d75e9535dcffbb879ee1deefcbf4
ENV OSX_SDK MacOSX$OSX_SDK_VERSION.sdk
ENV OSX_NDK_X86 /usr/local/osx-ndk-x86
# FreeBSD cross-compilation setup
# https://github.com/smartmontools/docker-build/blob/6b8c92560d17d325310ba02d9f5a4b250cb0764a/Dockerfile#L66
ENV FREEBSD_VERSION 12.4
ENV FREEBSD_DOWNLOAD_URL http://ftp.plusline.de/FreeBSD/releases/amd64/${FREEBSD_VERSION}-RELEASE/base.txz
ENV FREEBSD_SHA 581c7edacfd2fca2bdf5791f667402d22fccd8a5e184635e0cac075564d57aa8
RUN wget ${OSX_SDK_DOWNLOAD_URL}
RUN echo "$OSX_SDK_SHA $OSX_SDK_DOWNLOAD_FILE" | sha256sum -c - || exit 1; \
git clone https://github.com/tpoechtrager/osxcross.git; \
mv $OSX_SDK_DOWNLOAD_FILE osxcross/tarballs/
RUN cd /tmp && \
curl -o base.txz $FREEBSD_DOWNLOAD_URL && \
echo "$FREEBSD_SHA base.txz" | sha256sum -c - && \
mkdir -p /opt/cross-freebsd && \
cd /opt/cross-freebsd && \
tar -xf /tmp/base.txz ./lib/ ./usr/lib/ ./usr/include/ && \
rm -f /tmp/base.txz && \
cd /opt/cross-freebsd/usr/lib && \
find . -xtype l | xargs ls -l | grep ' /lib/' | awk '{print "ln -sf /opt/cross-freebsd"$11 " " $9}' | /bin/sh && \
ln -s libc++.a libstdc++.a && \
ln -s libc++.so libstdc++.so
RUN UNATTENDED=yes SDK_VERSION=${OSX_SDK_VERSION} OSX_VERSION_MIN=10.10 osxcross/build.sh || exit 1;
RUN cp osxcross/target/lib/* /usr/lib/ ; \
mv osxcross/target $OSX_NDK_X86; \
rm -rf osxcross;
# macOS cross-compilation setup
ENV OSX_SDK_VERSION 11.3
ENV OSX_SDK_DOWNLOAD_FILE MacOSX${OSX_SDK_VERSION}.sdk.tar.xz
ENV OSX_SDK_DOWNLOAD_URL https://github.com/phracker/MacOSX-SDKs/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
ENV OSX_SDK_SHA cd4f08a75577145b8f05245a2975f7c81401d75e9535dcffbb879ee1deefcbf4
ENV OSXCROSS_REVISION 5e1b71fcceb23952f3229995edca1b6231525b5b
ENV OSXCROSS_DOWNLOAD_URL https://codeload.github.com/tpoechtrager/osxcross/tar.gz/${OSXCROSS_REVISION}
ENV OSXCROSS_SHA d3f771bbc20612fea577b18a71be3af2eb5ad2dd44624196cf55de866d008647
ENV PATH $OSX_NDK_X86/bin:$PATH
RUN cd /tmp && \
curl -o osxcross.tar.gz $OSXCROSS_DOWNLOAD_URL && \
echo "$OSXCROSS_SHA osxcross.tar.gz" | sha256sum -c - && \
mkdir osxcross && \
tar --strip=1 -C osxcross -xf osxcross.tar.gz && \
rm -f osxcross.tar.gz && \
curl -Lo $OSX_SDK_DOWNLOAD_FILE $OSX_SDK_DOWNLOAD_URL && \
echo "$OSX_SDK_SHA $OSX_SDK_DOWNLOAD_FILE" | sha256sum -c - && \
mv $OSX_SDK_DOWNLOAD_FILE osxcross/tarballs/ && \
UNATTENDED=yes SDK_VERSION=$OSX_SDK_VERSION OSX_VERSION_MIN=10.10 osxcross/build.sh && \
cp osxcross/target/lib/* /usr/lib/ && \
mv osxcross/target /opt/osx-ndk-x86 && \
rm -rf /tmp/osxcross
RUN mkdir -p /root/.ssh; \
chmod 0700 /root/.ssh; \
ssh-keyscan github.com > /root/.ssh/known_hosts;
ENV PATH /opt/osx-ndk-x86/bin:$PATH
# Notes for self:
RUN mkdir -p /root/.ssh && \
chmod 0700 /root/.ssh && \
ssh-keyscan github.com > /root/.ssh/known_hosts
# ignore "dubious ownership" errors
RUN git config --global safe.directory '*'
# To test locally:
# make generate
# make ui
# cd docker/compiler
# make build
# docker run -it -v /PATH_TO_STASH:/go/stash stashapp/compiler:latest /bin/bash
# cd stash
# make cross-compile-all
# docker run --rm -v /PATH_TO_STASH:/stash -w /stash -i -t stashapp/compiler:latest make build-cc-all
# # binaries will show up in /dist
# Windows:
# GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go build -ldflags "-extldflags '-static'" -tags extended
# Darwin
# CC=o64-clang CXX=o64-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -tags extended
# env goreleaser --config=goreleaser-extended.yml --skip-publish --skip-validate --rm-dist --release-notes=temp/0.48-relnotes-ready.md

View File

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

View File

@@ -1,3 +1,3 @@
Modified from https://github.com/bep/dockerfiles/tree/master/ci-goreleaser
When the dockerfile is changed, the version number should be incremented in the Makefile and the new version tag should be pushed to docker hub. The `scripts/cross-compile.sh` script should also be updated to use the new version number tag, and the github workflow files need to be updated to pull the correct image tag.
When the Dockerfile is changed, the version number should be incremented in the Makefile and the new version tag should be pushed to Docker Hub. The GitHub workflow files also need to be updated to pull the correct image tag.

View File

@@ -9,11 +9,11 @@ https://docs.docker.com/engine/install/
### Get the docker-compose.yml file
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml) in the repository, or if you have curl, you can make your Linux console do it for you:
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/develop/docker/production/docker-compose.yml) in the repository, or if you have curl, you can make your Linux console do it for you:
```
mkdir stashapp && cd stashapp
curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml
curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/develop/docker/production/docker-compose.yml
```
Once you have that file where you want it, modify the settings as you please, and then run:

View File

@@ -32,9 +32,24 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MinGW. For examp
#### Ubuntu
1. Install dependencies: `sudo apt-get install golang git gcc nodejs ffmpeg -y`
2. Enable corepack in Node.js: `corepack enable`
3. Install yarn: `corepack prepare yarn@stable --activate`
1. Install dependencies: `sudo apt-get install golang git yarnpkg gcc nodejs ffmpeg -y`
### OpenBSD
1. Install dependencies `doas pkg_add gmake go git yarn node cmake`
2. Compile a custom ffmpeg from ports. The default ffmpeg in OpenBSD's packages is not compiled with WebP support, which is required by Stash.
- If you've already installed ffmpeg, uninstall it: `doas pkg_delete ffmpeg`
- If you haven't already, [fetch the ports tree and verify](https://www.openbsd.org/faq/ports/ports.html#PortsFetch).
- Find the ffmpeg port in `/usr/ports/graphics/ffmpeg`, and patch the Makefile to include libwebp
- Add `webp` to `WANTLIB`
- Add `graphics/libwebp` to the list in `LIB_DEPENDS`
- Add `-lwebp -lwebpdecoder -lwebpdemux -lwebpmux` to `LIBavcodec_EXTRALIBS`
- Add `--enable-libweb` to the list in `CONFIGURE_ARGS`
- If you've already built ffmpeg from ports before, you may need to also increment `REVISION`
- Run `doas make install`
- Follow the instructions below to build a release, but replace the final step `make build-release` with `gmake flags-release stash`, to [avoid the PIE buildmode](https://github.com/golang/go/issues/59866).
NOTE: The `make` command in OpenBSD will be `gmake`. For example, `make pre-ui` will be `gmake pre-ui`.
## Commands
@@ -43,11 +58,10 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MinGW. For examp
* `make generate-stash-box-client` - Generate Go files for the Stash-box client code.
* `make ui` - Builds the UI. Requires `make pre-ui` to have been run.
* `make stash` - Builds the `stash` binary (make sure to build the UI as well... see below)
* `make stash-release` - Builds a release version the `stash` binary, with debug information removed
* `make stash-macapp` - Builds the `Stash.app` macOS app (only works when on macOS, for cross-compilation see below)
* `make phasher` - Builds the `phasher` binary
* `make phasher-release` - Builds a release version the `phasher` binary, with debug information removed
* `make build` - Builds both the `stash` and `phasher` binaries
* `make build-release` - Builds release versions of both the `stash` and `phasher` binaries
* `make build` - Builds both the `stash` and `phasher` binaries, alias for `make stash phasher`
* `make build-release` - Builds release versions (debug information removed) of both the `stash` and `phasher` binaries, alias for `make flags-release flags-pie build`
* `make docker-build` - Locally builds and tags a complete 'stash/build' docker image
* `make docker-cuda-build` - Locally builds and tags a complete 'stash/cuda-build' docker image
* `make validate` - Runs all of the tests and checks required to submit a PR
@@ -57,7 +71,15 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MinGW. For examp
* `make fmt-ui` - Formats the UI source code
* `make server-start` - Runs a development stash server in the `.local` directory
* `make server-clean` - Removes the `.local` directory and all of its contents
* `make ui-start` - Runs the UI in development mode. Requires a running Stash server to connect to. The server port can be changed from the default of `9999` using the environment variable `VITE_APP_PLATFORM_PORT`. The UI runs on port `3000` or the next available port.
* `make ui-start` - Runs the UI in development mode. Requires a running Stash server to connect to - the server URL can be changed from the default of `http://localhost:9999` using the environment variable `VITE_APP_PLATFORM_URL`, but keep in mind that authentication cannot be used since the session authorization cookie cannot be sent cross-origin. The UI runs on port `3000` or the next available port.
When building, you can optionally prepend `flags-*` targets to the target list in your `make` command to use different build flags:
* `flags-release` (e.g. `make flags-release stash`) - Remove debug information from the binary.
* `flags-pie` (e.g. `make flags-pie build`) - Build a PIE (Position Independent Executable) binary. This provides increased security, but it is unsupported on some systems (notably 32-bit ARM and OpenBSD).
* `flags-static` (e.g. `make flags-static phasher`) - Build a statically linked binary (the default is a dynamically linked binary).
* `flags-static-pie` (e.g. `make flags-static-pie stash`) - Build a statically linked PIE binary (using `flags-static` and `flags-pie` separately will not work).
* `flags-static-windows` (e.g. `make flags-static-windows build`) - Identical to `flags-static-pie`, but does not enable the `netgo` build tag, which is not needed for static builds on Windows.
## Local development quickstart
@@ -95,13 +117,19 @@ Simply run `make` or `make release`, or equivalently:
3. Run `make ui` to build the frontend
4. Run `make build-release` to build a release executable for your current platform
## Cross compiling
## Cross-compiling
This project uses a modification of the [CI-GoReleaser](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment
where the app can be cross-compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following
command to open a bash shell to the container to poke around:
This project uses a modification of the [CI-GoReleaser](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) Docker container for cross-compilation, defined in `docker/compiler/Dockerfile`.
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashapp/compiler:latest /bin/bash`
To cross-compile the app yourself:
1. Run `make pre-ui`, `make generate` and `make ui` outside the container, to generate files and build the UI.
2. Pull the latest compiler image from Docker Hub: `docker pull stashapp/compiler`
3. Run `docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -it stashapp/compiler /bin/bash` to open a shell inside the container.
4. From inside the container, run `make build-cc-all` to build for all platforms, or run `make build-cc-{platform}` to build for a specific platform (have a look at the `Makefile` for the list of targets).
5. You will find the compiled binaries in `dist/`.
NOTE: Since the container is run as UID 0 (root), the resulting binaries (and the `dist/` folder itself, if it had to be created) will be owned by root.
## Profiling

123
go.mod
View File

@@ -1,67 +1,62 @@
module github.com/stashapp/stash
go 1.19
require (
github.com/99designs/gqlgen v0.17.2
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552
github.com/Yamashou/gqlgenc v0.0.6
github.com/anacrolix/dms v1.2.2
github.com/antchfx/htmlquery v1.3.0
github.com/chromedp/cdproto v0.0.0-20210622022015-fe1827b46b84
github.com/chromedp/chromedp v0.7.3
github.com/corona10/goimagehash v1.0.3
github.com/disintegration/imaging v1.6.0
github.com/go-chi/chi v4.0.2+incompatible
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang-jwt/jwt/v4 v4.0.0
github.com/golang-migrate/migrate/v4 v4.15.0-beta.1
github.com/asticode/go-astisub v0.26.0
github.com/chromedp/cdproto v0.0.0-20231007061347-18b01cd81617
github.com/chromedp/chromedp v0.9.2
github.com/corona10/goimagehash v1.1.0
github.com/disintegration/imaging v1.6.2
github.com/doug-martin/goqu/v9 v9.18.0
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1
github.com/go-chi/httplog v0.3.1
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/gofrs/uuid/v5 v5.0.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-migrate/migrate/v4 v4.16.2
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.0
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.5.0
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/jmoiron/sqlx v1.3.1
github.com/hashicorp/golang-lru/v2 v2.0.6
github.com/jinzhu/copier v0.4.0
github.com/jmoiron/sqlx v1.3.5
github.com/json-iterator/go v1.1.12
github.com/mattn/go-sqlite3 v1.14.7
github.com/kermieisinthehouse/gosx-notifier v0.1.2
github.com/kermieisinthehouse/systray v1.2.4
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-sqlite3 v1.14.17
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/remeh/sizedwaitgroup v1.0.0
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
github.com/sirupsen/logrus v1.8.1
github.com/spf13/afero v1.8.2 // indirect
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cast v1.5.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.1
github.com/tidwall/gjson v1.9.3
github.com/tidwall/pretty v1.2.0 // indirect
github.com/vektra/mockery/v2 v2.10.0
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
golang.org/x/image v0.5.0
golang.org/x/net v0.7.0
golang.org/x/sys v0.5.0
golang.org/x/term v0.5.0
golang.org/x/text v0.7.0
golang.org/x/tools v0.1.12 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552
github.com/asticode/go-astisub v0.20.0
github.com/doug-martin/goqu/v9 v9.18.0
github.com/go-chi/cors v1.2.1
github.com/go-chi/httplog v0.2.1
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/hashicorp/golang-lru v0.5.4
github.com/kermieisinthehouse/gosx-notifier v0.1.1
github.com/kermieisinthehouse/systray v1.2.4
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/spf13/cast v1.4.1
github.com/vearutop/statigz v1.1.6
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4
github.com/tidwall/gjson v1.16.0
github.com/vearutop/statigz v1.4.0
github.com/vektah/dataloaden v0.3.0
github.com/vektah/gqlparser/v2 v2.4.2
github.com/vektra/mockery/v2 v2.10.0
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
github.com/zencoder/go-dash/v3 v3.0.2
golang.org/x/crypto v0.17.0
golang.org/x/image v0.12.0
golang.org/x/net v0.17.0
golang.org/x/sys v0.15.0
golang.org/x/term v0.15.0
golang.org/x/text v0.14.0
gopkg.in/guregu/null.v4 v4.0.0
gopkg.in/yaml.v2 v2.4.0
)
require (
@@ -70,46 +65,48 @@ require (
github.com/asticode/go-astikit v0.20.0 // indirect
github.com/asticode/go-astits v1.8.0 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-chi/chi/v5 v5.0.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0-rc.5 // indirect
github.com/gobwas/ws v1.3.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matryer/moq v0.2.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/go-homedir v1.1.0 // 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
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/zerolog v1.26.1 // indirect
github.com/rs/zerolog v1.30.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/cobra v1.4.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/urfave/cli/v2 v2.8.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/tools v0.13.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
go 1.19

554
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ resolver:
struct_tag: gqlgen
autobind:
- github.com/stashapp/stash/internal/api
- github.com/stashapp/stash/pkg/models
- github.com/stashapp/stash/pkg/plugin
- github.com/stashapp/stash/pkg/scraper
@@ -33,36 +34,23 @@ models:
model: github.com/99designs/gqlgen/graphql.Int64
Timestamp:
model: github.com/stashapp/stash/internal/api.Timestamp
BoolMap:
model: github.com/stashapp/stash/internal/api.BoolMap
PluginConfigMap:
model: github.com/stashapp/stash/internal/api.PluginConfigMap
# define to force resolvers
Image:
model: github.com/stashapp/stash/pkg/models.Image
fields:
title:
resolver: true
# override models, from internal/api/models.go
BaseFile:
model: github.com/stashapp/stash/internal/api.BaseFile
GalleryFile:
model: github.com/stashapp/stash/internal/api.GalleryFile
fields:
# override fingerprint field
fingerprints:
fieldName: FingerprintSlice
VideoFile:
fields:
# override fingerprint field
fingerprints:
fieldName: FingerprintSlice
# override float fields - #1572
duration:
fieldName: DurationFinite
frame_rate:
fieldName: FrameRateFinite
ImageFile:
fields:
# override fingerprint field
fingerprints:
fieldName: FingerprintSlice
# autobind on config causes generation issues
BlobsStorageType:
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
@@ -82,6 +70,8 @@ models:
model: github.com/stashapp/stash/internal/manager/config.ConfigDisableDropdownCreate
ScanMetadataOptions:
model: github.com/stashapp/stash/internal/manager/config.ScanMetadataOptions
CleanGeneratedInput:
model: github.com/stashapp/stash/internal/manager/task.CleanGeneratedOptions
AutoTagMetadataOptions:
model: github.com/stashapp/stash/internal/manager/config.AutoTagMetadataOptions
SystemStatus:
@@ -150,4 +140,9 @@ models:
model: github.com/stashapp/stash/pkg/scraper.Source
SavedFindFilterType:
model: github.com/stashapp/stash/pkg/models.FindFilterType
# force resolvers
ConfigResult:
fields:
plugins:
resolver: true

View File

@@ -1,11 +0,0 @@
mutation ReloadPlugins {
reloadPlugins
}
mutation RunPluginTask(
$plugin_id: ID!
$task_name: String!
$args: [PluginArgInput!]
) {
runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args: $args)
}

View File

@@ -1,3 +0,0 @@
mutation ReloadScrapers {
reloadScrapers
}

View File

@@ -1,31 +0,0 @@
query Plugins {
plugins {
id
name
description
url
version
tasks {
name
description
}
hooks {
name
description
hooks
}
}
}
query PluginTasks {
pluginTasks {
name
description
plugin {
id
name
}
}
}

View File

@@ -1,3 +0,0 @@
query ScrapeFreeonesPerformers($q: String!) {
scrapeFreeonesPerformerList(query: $q)
}

View File

@@ -12,7 +12,8 @@ type Query {
"A function which queries Scene objects"
findScenes(
scene_filter: SceneFilterType
scene_ids: [Int!]
scene_ids: [Int!] @deprecated(reason: "use ids")
ids: [ID!]
filter: FindFilterType
): FindScenesResultType!
@@ -50,7 +51,8 @@ type Query {
"A function which queries Scene objects"
findImages(
image_filter: ImageFilterType
image_ids: [Int!]
image_ids: [Int!] @deprecated(reason: "use ids")
ids: [ID!]
filter: FindFilterType
): FindImagesResultType!
@@ -60,7 +62,8 @@ type Query {
findPerformers(
performer_filter: PerformerFilterType
filter: FindFilterType
performer_ids: [Int!]
performer_ids: [Int!] @deprecated(reason: "use ids")
ids: [ID!]
): FindPerformersResultType!
"Find a studio by ID"
@@ -69,6 +72,7 @@ type Query {
findStudios(
studio_filter: StudioFilterType
filter: FindFilterType
ids: [ID!]
): FindStudiosResultType!
"Find a movie by ID"
@@ -77,18 +81,21 @@ type Query {
findMovies(
movie_filter: MovieFilterType
filter: FindFilterType
ids: [ID!]
): FindMoviesResultType!
findGallery(id: ID!): Gallery
findGalleries(
gallery_filter: GalleryFilterType
filter: FindFilterType
ids: [ID!]
): FindGalleriesResultType!
findTag(id: ID!): Tag
findTags(
tag_filter: TagFilterType
filter: FindFilterType
ids: [ID!]
): FindTagsResultType!
"Retrieve random scene markers for the wall"
@@ -109,14 +116,6 @@ type Query {
"List available scrapers"
listScrapers(types: [ScrapeContentType!]!): [Scraper!]!
listPerformerScrapers: [Scraper!]!
@deprecated(reason: "Use listScrapers(types: [PERFORMER])")
listSceneScrapers: [Scraper!]!
@deprecated(reason: "Use listScrapers(types: [SCENE])")
listGalleryScrapers: [Scraper!]!
@deprecated(reason: "Use listScrapers(types: [GALLERY])")
listMovieScrapers: [Scraper!]!
@deprecated(reason: "Use listScrapers(types: [MOVIE])")
"Scrape for a single scene"
scrapeSingleScene(
@@ -170,33 +169,18 @@ type Query {
"Scrapes a complete movie record based on a URL"
scrapeMovieURL(url: String!): ScrapedMovie
"Scrape a list of performers based on name"
scrapePerformerList(scraper_id: ID!, query: String!): [ScrapedPerformer!]!
@deprecated(reason: "use scrapeSinglePerformer")
"Scrapes a complete performer record based on a scrapePerformerList result"
scrapePerformer(
scraper_id: ID!
scraped_performer: ScrapedPerformerInput!
): ScrapedPerformer @deprecated(reason: "use scrapeSinglePerformer")
"Scrapes a complete scene record based on an existing scene"
scrapeScene(scraper_id: ID!, scene: SceneUpdateInput!): ScrapedScene
@deprecated(reason: "use scrapeSingleScene")
"Scrapes a complete gallery record based on an existing gallery"
scrapeGallery(scraper_id: ID!, gallery: GalleryUpdateInput!): ScrapedGallery
@deprecated(reason: "use scrapeSingleGallery")
"Scrape a list of performers from a query"
scrapeFreeonesPerformerList(query: String!): [String!]!
@deprecated(
reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones"
)
# Plugins
"List loaded plugins"
plugins: [Plugin!]
"List available plugin operations"
pluginTasks: [PluginTask!]
# Packages
"List installed packages"
installedPackages(type: PackageType!): [Package!]!
"List available packages"
availablePackages(type: PackageType!, source: String!): [Package!]!
# Config
"Returns the current, complete configuration"
configuration: ConfigResult!
@@ -220,16 +204,16 @@ type Query {
# Get everything
allScenes: [Scene!]!
allScenes: [Scene!]! @deprecated(reason: "Use findScenes instead")
allSceneMarkers: [SceneMarker!]!
allImages: [Image!]!
allGalleries: [Gallery!]!
allStudios: [Studio!]!
allMovies: [Movie!]!
allTags: [Tag!]!
@deprecated(reason: "Use findSceneMarkers instead")
allImages: [Image!]! @deprecated(reason: "Use findImages instead")
allGalleries: [Gallery!]! @deprecated(reason: "Use findGalleries instead")
# @deprecated
allPerformers: [Performer!]!
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
allMovies: [Movie!]! @deprecated(reason: "Use findMovies instead")
# Get everything with minimal metadata
@@ -253,9 +237,15 @@ type Mutation {
scenesUpdate(input: [SceneUpdateInput!]!): [Scene]
"Increments the o-counter for a scene. Returns the new value"
sceneIncrementO(id: ID!): Int!
sceneIncrementO(id: ID!): Int! @deprecated(reason: "Use sceneAddO instead")
"Decrements the o-counter for a scene. Returns the new value"
sceneDecrementO(id: ID!): Int!
sceneDecrementO(id: ID!): Int! @deprecated(reason: "Use sceneRemoveO instead")
"Increments the o-counter for a scene. Uses the current time if none provided."
sceneAddO(id: ID!, times: [Timestamp!]): HistoryMutationResult!
"Decrements the o-counter for a scene, removing the last recorded time if specific time not provided. Returns the new value"
sceneDeleteO(id: ID!, times: [Timestamp!]): HistoryMutationResult!
"Resets the o-counter for a scene to 0. Returns the new value"
sceneResetO(id: ID!): Int!
@@ -264,6 +254,14 @@ type Mutation {
"Increments the play count for the scene. Returns the new play count value."
sceneIncrementPlayCount(id: ID!): Int!
@deprecated(reason: "Use sceneAddPlay instead")
"Increments the play count for the scene. Uses the current time if none provided."
sceneAddPlay(id: ID!, times: [Timestamp!]): HistoryMutationResult!
"Decrements the play count for the scene, removing the specific times or the last recorded time if not provided."
sceneDeletePlay(id: ID!, times: [Timestamp!]): HistoryMutationResult!
"Resets the play count for a scene to 0. Returns the new play count value."
sceneResetPlayCount(id: ID!): Int!
"Generates screenshot at specified time in seconds. Leave empty to generate default screenshot"
sceneGenerateScreenshot(id: ID!, at: Float): String!
@@ -335,6 +333,8 @@ type Mutation {
moveFiles(input: MoveFilesInput!): Boolean!
deleteFiles(ids: [ID!]!): Boolean!
fileSetFingerprints(input: FileSetFingerprintsInput!): Boolean!
# Saved filters
saveFilter(input: SaveFilterInput!): SavedFilter!
destroySavedFilter(input: DestroyFilterInput!): Boolean!
@@ -349,6 +349,9 @@ type Mutation {
input: ConfigDefaultSettingsInput!
): ConfigDefaultSettingsResult!
# overwrites the entire plugin configuration for the given plugin
configurePlugin(plugin_id: ID!, input: Map!): Map!
# overwrites the entire UI configuration
configureUI(input: Map!): Map!
# sets a single UI key value
@@ -375,6 +378,8 @@ type Mutation {
metadataAutoTag(input: AutoTagMetadataInput!): ID!
"Clean metadata. Returns the job ID"
metadataClean(input: CleanMetadataInput!): ID!
"Clean generated files. Returns the job ID"
metadataCleanGenerated(input: CleanGeneratedInput!): ID!
"Identifies scenes using scrapers. Returns the job ID"
metadataIdentify(input: IdentifyMetadataInput!): ID!
@@ -394,14 +399,60 @@ type Mutation {
"Reload scrapers"
reloadScrapers: Boolean!
"Run plugin task. Returns the job ID"
"""
Enable/disable plugins - enabledMap is a map of plugin IDs to enabled booleans.
Plugins not in the map are not affected.
"""
setPluginsEnabled(enabledMap: BoolMap!): Boolean!
"""
Run a plugin task.
If task_name is provided, then the task must exist in the plugin config and the tasks configuration
will be used to run the plugin.
If no task_name is provided, then the plugin will be executed with the arguments provided only.
Returns the job ID
"""
runPluginTask(
plugin_id: ID!
task_name: String!
args: [PluginArgInput!]
"if provided, then the default args will be applied"
task_name: String
"displayed in the task queue"
description: String
args: [PluginArgInput!] @deprecated(reason: "Use args_map instead")
args_map: Map
): ID!
"""
Runs a plugin operation. The operation is run immediately and does not use the job queue.
Returns a map of the result.
"""
runPluginOperation(plugin_id: ID!, args: Map): Any
reloadPlugins: Boolean!
"""
Installs the given packages.
If a package is already installed, it will be updated if needed..
If an error occurs when installing a package, the job will continue to install the remaining packages.
Returns the job ID
"""
installPackages(type: PackageType!, packages: [PackageSpecInput!]!): ID!
"""
Updates the given packages.
If a package is not installed, it will not be installed.
If a package does not need to be updated, it will not be updated.
If no packages are provided, all packages of the given type will be updated.
If an error occurs when updating a package, the job will continue to update the remaining packages.
Returns the job ID.
"""
updatePackages(type: PackageType!, packages: [PackageSpecInput!]): ID!
"""
Uninstalls the given packages.
If an error occurs when uninstalling a package, the job will continue to uninstall the remaining packages.
Returns the job ID
"""
uninstallPackages(type: PackageType!, packages: [PackageSpecInput!]!): ID!
stopJob(job_id: ID!): Boolean!
stopAllJobs: Boolean!

View File

@@ -73,6 +73,8 @@ input ConfigGeneralInput {
metadataPath: String
"Path to scrapers"
scrapersPath: String
"Path to plugins"
pluginsPath: String
"Path to cache"
cachePath: String
"Path to blobs - required for filesystem blob storage"
@@ -139,8 +141,6 @@ input ConfigGeneralInput {
password: String
"Maximum session cookie age"
maxSessionAge: Int
"Comma separated list of proxies to allow traffic from"
trustedProxies: [String!] @deprecated(reason: "no longer supported")
"Name of the log file"
logFile: String
"Whether to also output to stderr"
@@ -165,25 +165,15 @@ input ConfigGeneralInput {
imageExcludes: [String!]
"Custom Performer Image Location"
customPerformerImageLocation: String
"Scraper user agent string"
scraperUserAgent: String
@deprecated(
reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead"
)
"Scraper CDP path. Path to chrome executable or remote address"
scraperCDPPath: String
@deprecated(
reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead"
)
"Whether the scraper should check for invalid certificates"
scraperCertCheck: Boolean
@deprecated(
reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead"
)
"Stash-box instances used for tagging"
stashBoxes: [StashBoxInput!]
"Python path - resolved using path if unset"
pythonPath: String
"Source of scraper packages"
scraperPackageSources: [PackageSourceInput!]
"Source of plugin packages"
pluginPackageSources: [PackageSourceInput!]
}
type ConfigGeneralResult {
@@ -201,6 +191,8 @@ type ConfigGeneralResult {
configFilePath: String!
"Path to scrapers"
scrapersPath: String!
"Path to plugins"
pluginsPath: String!
"Path to cache"
cachePath: String!
"Path to blobs - required for filesystem blob storage"
@@ -269,8 +261,6 @@ type ConfigGeneralResult {
password: String!
"Maximum session cookie age"
maxSessionAge: Int!
"Comma separated list of proxies to allow traffic from"
trustedProxies: [String!] @deprecated(reason: "no longer supported")
"Name of the log file"
logFile: String
"Whether to also output to stderr"
@@ -295,19 +285,15 @@ type ConfigGeneralResult {
imageExcludes: [String!]!
"Custom Performer Image Location"
customPerformerImageLocation: String
"Scraper user agent string"
scraperUserAgent: String
@deprecated(reason: "use ConfigResult.scraping instead")
"Scraper CDP path. Path to chrome executable or remote address"
scraperCDPPath: String
@deprecated(reason: "use ConfigResult.scraping instead")
"Whether the scraper should check for invalid certificates"
scraperCertCheck: Boolean!
@deprecated(reason: "use ConfigResult.scraping instead")
"Stash-box instances used for tagging"
stashBoxes: [StashBox!]!
"Python path - resolved using path if unset"
pythonPath: String!
"Source of scraper packages"
scraperPackageSources: [PackageSource!]!
"Source of plugin packages"
pluginPackageSources: [PackageSource!]!
}
input ConfigDisableDropdownCreateInput {
@@ -388,9 +374,6 @@ input ConfigInterfaceInput {
"Interface language"
language: String
"Slideshow Delay"
slideshowDelay: Int @deprecated(reason: "Use imageLightbox.slideshowDelay")
imageLightbox: ConfigImageLightboxInput
"Set to true to disable creating new objects via the dropdown menus"
@@ -461,15 +444,10 @@ type ConfigInterfaceResult {
"Interface language"
language: String
"Slideshow Delay"
slideshowDelay: Int @deprecated(reason: "Use imageLightbox.slideshowDelay")
imageLightbox: ConfigImageLightboxResult!
"Fields are true if creating via dropdown menus are disabled"
disableDropdownCreate: ConfigDisableDropdownCreate!
disabledDropdownCreate: ConfigDisableDropdownCreate!
@deprecated(reason: "Use disableDropdownCreate")
"Handy Connection Key"
handyKey: String
@@ -557,6 +535,7 @@ type ConfigResult {
scraping: ConfigScrapingResult!
defaults: ConfigDefaultSettingsResult!
ui: Map!
plugins(include: [ID!]): PluginConfigMap!
}
"Directory structure of a path"

View File

@@ -27,6 +27,7 @@ interface BaseFile {
mod_time: Time!
size: Int64!
fingerprint(type: String!): String
fingerprints: [Fingerprint!]!
created_at: Time!
@@ -44,6 +45,7 @@ type VideoFile implements BaseFile {
mod_time: Time!
size: Int64!
fingerprint(type: String!): String
fingerprints: [Fingerprint!]!
format: String!
@@ -70,6 +72,7 @@ type ImageFile implements BaseFile {
mod_time: Time!
size: Int64!
fingerprint(type: String!): String
fingerprints: [Fingerprint!]!
width: Int!
@@ -92,6 +95,7 @@ type GalleryFile implements BaseFile {
mod_time: Time!
size: Int64!
fingerprint(type: String!): String
fingerprints: [Fingerprint!]!
created_at: Time!
@@ -109,3 +113,15 @@ input MoveFilesInput {
"valid only for single file id. If empty, existing basename is used"
destination_basename: String
}
input SetFingerprintsInput {
type: String!
"an null value will remove the fingerprint"
value: String
}
input FileSetFingerprintsInput {
id: ID!
"only supplied fingerprint types will be modified"
fingerprints: [SetFingerprintsInput!]!
}

View File

@@ -61,6 +61,19 @@ input ResolutionCriterionInput {
modifier: CriterionModifier!
}
enum OrientationEnum {
"Landscape"
LANDSCAPE
"Portrait"
PORTRAIT
"Square"
SQUARE
}
input OrientationCriterionInput {
value: [OrientationEnum!]!
}
input PHashDuplicationCriterionInput {
duplicated: Boolean
"Currently unimplemented"
@@ -98,8 +111,6 @@ input PerformerFilterType {
country: StringCriterionInput
"Filter by eye color"
eye_color: StringCriterionInput
"Filter by height"
height: StringCriterionInput @deprecated(reason: "Use height_cm instead")
"Filter by height in cm"
height_cm: IntCriterionInput
"Filter by measurements"
@@ -135,13 +146,7 @@ input PerformerFilterType {
"Filter by o count"
o_counter: IntCriterionInput
"Filter by StashID"
stash_id: StringCriterionInput
@deprecated(reason: "Use stash_id_endpoint instead")
"Filter by StashID"
stash_id_endpoint: StashIDCriterionInput
"Filter by rating"
rating: IntCriterionInput
@deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"Filter by url"
@@ -169,8 +174,6 @@ input PerformerFilterType {
}
input SceneMarkerFilterType {
"Filter to only include scene markers with this tag"
tag_id: ID @deprecated(reason: "use tags filter instead")
"Filter to only include scene markers with these tags"
tags: HierarchicalMultiCriterionInput
"Filter to only include scene markers attached to a scene with these tags"
@@ -212,9 +215,6 @@ input SceneFilterType {
path: StringCriterionInput
"Filter by file count"
file_count: IntCriterionInput
"Filter by rating"
rating: IntCriterionInput
@deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"Filter by organized"
@@ -225,6 +225,10 @@ input SceneFilterType {
duplicated: PHashDuplicationCriterionInput
"Filter by resolution"
resolution: ResolutionCriterionInput
"Filter by orientation"
orientation: OrientationCriterionInput
"Filter by frame rate"
framerate: IntCriterionInput
"Filter by video codec"
video_codec: StringCriterionInput
"Filter by audio codec"
@@ -254,9 +258,6 @@ input SceneFilterType {
"Filter by performer count"
performer_count: IntCriterionInput
"Filter by StashID"
stash_id: StringCriterionInput
@deprecated(reason: "Use stash_id_endpoint instead")
"Filter by StashID"
stash_id_endpoint: StashIDCriterionInput
"Filter by url"
url: StringCriterionInput
@@ -287,9 +288,6 @@ input MovieFilterType {
"Filter by duration (in seconds)"
duration: IntCriterionInput
"Filter by rating"
rating: IntCriterionInput
@deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"Filter to only include movies with this studio"
@@ -318,15 +316,9 @@ input StudioFilterType {
"Filter to only include studios with this parent studio"
parents: MultiCriterionInput
"Filter by StashID"
stash_id: StringCriterionInput
@deprecated(reason: "Use stash_id_endpoint instead")
"Filter by StashID"
stash_id_endpoint: StashIDCriterionInput
"Filter to only include studios missing this property"
is_missing: String
"Filter by rating"
rating: IntCriterionInput
@deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"Filter by scene count"
@@ -339,6 +331,8 @@ input StudioFilterType {
url: StringCriterionInput
"Filter by studio aliases"
aliases: StringCriterionInput
"Filter by subsidiary studio count"
child_count: IntCriterionInput
"Filter by autotag ignore value"
ignore_auto_tag: Boolean
"Filter by creation time"
@@ -366,9 +360,6 @@ input GalleryFilterType {
is_missing: String
"Filter to include/exclude galleries that were created from zip"
is_zip: Boolean
"Filter by rating"
rating: IntCriterionInput
@deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"Filter by organized"
@@ -403,6 +394,10 @@ input GalleryFilterType {
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
"Filter by studio code"
code: StringCriterionInput
"Filter by photographer"
photographer: StringCriterionInput
}
input TagFilterType {
@@ -465,6 +460,7 @@ input ImageFilterType {
NOT: ImageFilterType
title: StringCriterionInput
details: StringCriterionInput
" Filter by image id"
id: IntCriterionInput
@@ -474,9 +470,6 @@ input ImageFilterType {
path: StringCriterionInput
"Filter by file count"
file_count: IntCriterionInput
"Filter by rating"
rating: IntCriterionInput
@deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: IntCriterionInput
"Filter by date"
@@ -489,6 +482,8 @@ input ImageFilterType {
o_counter: IntCriterionInput
"Filter by resolution"
resolution: ResolutionCriterionInput
"Filter by orientation"
orientation: OrientationCriterionInput
"Filter to only include images missing this property"
is_missing: String
"Filter to only include images with this studio"
@@ -505,12 +500,18 @@ input ImageFilterType {
performer_count: IntCriterionInput
"Filter images that have performers that have been favorited"
performer_favorite: Boolean
"Filter images by performer age at time of image"
performer_age: IntCriterionInput
"Filter to only include images with these galleries"
galleries: MultiCriterionInput
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
"Filter by studio code"
code: StringCriterionInput
"Filter by photographer"
photographer: StringCriterionInput
}
enum CriterionModifier {
@@ -565,6 +566,7 @@ input MultiCriterionInput {
input GenderCriterionInput {
value: GenderEnum
value_list: [GenderEnum!]
modifier: CriterionModifier!
}

View File

@@ -1,21 +1,18 @@
"Gallery type"
type Gallery {
id: ID!
checksum: String! @deprecated(reason: "Use files.fingerprints")
path: String @deprecated(reason: "Use files.path")
title: String
code: String
url: String @deprecated(reason: "Use urls")
urls: [String!]!
date: String
details: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
photographer: String
# rating expressed as 1-100
rating100: Int
organized: Boolean!
created_at: Time!
updated_at: Time!
file_mod_time: Time @deprecated(reason: "Use files.mod_time")
files: [GalleryFile!]!
folder: Folder
@@ -27,19 +24,17 @@ type Gallery {
tags: [Tag!]!
performers: [Performer!]!
"The images in the gallery"
images: [Image!]! @deprecated(reason: "Use findImages")
cover: Image
}
input GalleryCreateInput {
title: String!
code: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
date: String
details: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
photographer: String
# rating expressed as 1-100
rating100: Int
organized: Boolean
@@ -53,12 +48,12 @@ input GalleryUpdateInput {
clientMutationId: String
id: ID!
title: String
code: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
date: String
details: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
photographer: String
# rating expressed as 1-100
rating100: Int
organized: Boolean
@@ -73,12 +68,12 @@ input GalleryUpdateInput {
input BulkGalleryUpdateInput {
clientMutationId: String
ids: [ID!]
code: String
url: String @deprecated(reason: "Use urls")
urls: BulkUpdateStrings
date: String
details: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
photographer: String
# rating expressed as 1-100
rating100: Int
organized: Boolean

View File

@@ -1,23 +1,19 @@
type Image {
id: ID!
checksum: String @deprecated(reason: "Use files.fingerprints")
title: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
code: String
# rating expressed as 1-100
rating100: Int
url: String @deprecated(reason: "Use urls")
urls: [String!]!
date: String
details: String
photographer: String
o_counter: Int
organized: Boolean!
path: String! @deprecated(reason: "Use files.path")
created_at: Time!
updated_at: Time!
file_mod_time: Time @deprecated(reason: "Use files.mod_time")
file: ImageFileType! @deprecated(reason: "Use visual_files")
files: [ImageFile!]! @deprecated(reason: "Use visual_files")
visual_files: [VisualFile!]!
paths: ImagePathsType! # Resolver
@@ -44,14 +40,15 @@ input ImageUpdateInput {
clientMutationId: String
id: ID!
title: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
code: String
# rating expressed as 1-100
rating100: Int
organized: Boolean
url: String @deprecated(reason: "Use urls")
urls: [String!]
date: String
details: String
photographer: String
studio_id: ID
performer_ids: [ID!]
@@ -65,14 +62,15 @@ input BulkImageUpdateInput {
clientMutationId: String
ids: [ID!]
title: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
code: String
# rating expressed as 1-100
rating100: Int
organized: Boolean
url: String @deprecated(reason: "Use urls")
urls: BulkUpdateStrings
date: String
details: String
photographer: String
studio_id: ID
performer_ids: BulkUpdateIds

View File

@@ -1,6 +1,3 @@
"Log entries"
scalar Time
enum LogLevel {
Trace
Debug

View File

@@ -1,5 +1,3 @@
scalar Upload
input GenerateMetadataInput {
covers: Boolean
sprites: Boolean
@@ -14,6 +12,7 @@ input GenerateMetadataInput {
forceTranscodes: Boolean
phashes: Boolean
interactiveHeatmapsSpeeds: Boolean
imageThumbnails: Boolean
clipPreviews: Boolean
"scene ids to generate for"
@@ -50,6 +49,7 @@ type GenerateMetadataOptions {
transcodes: Boolean
phashes: Boolean
interactiveHeatmapsSpeeds: Boolean
imageThumbnails: Boolean
clipPreviews: Boolean
}
@@ -75,19 +75,6 @@ input ScanMetaDataFilterInput {
input ScanMetadataInput {
paths: [String!]
# useFileMetadata is deprecated with the new file management system
# if this functionality is desired, then we can make a built in scraper instead.
"Set name, date, details from metadata (if present)"
useFileMetadata: Boolean @deprecated(reason: "Not implemented")
# stripFileExtension is deprecated since we no longer set the title from the
# filename - it is automatically returned if the object has no title. If this
# functionality is desired, then we could make this an option to not include
# the extension in the auto-generated title.
"Strip file extension from title"
stripFileExtension: Boolean @deprecated(reason: "Not implemented")
"Generate covers during scan"
scanGenerateCovers: Boolean
"Generate previews during scan"
@@ -108,10 +95,6 @@ input ScanMetadataInput {
}
type ScanMetadataOptions {
"Set name, date, details from metadata (if present)"
useFileMetadata: Boolean! @deprecated(reason: "Not implemented")
"Strip file extension from title"
stripFileExtension: Boolean! @deprecated(reason: "Not implemented")
"Generate covers during scan"
scanGenerateCovers: Boolean!
"Generate previews during scan"
@@ -135,6 +118,26 @@ input CleanMetadataInput {
dryRun: Boolean!
}
input CleanGeneratedInput {
"Clean blob files without blob entries"
blobFiles: Boolean
"Clean sprite and vtt files without scene entries"
sprites: Boolean
"Clean preview files without scene entries"
screenshots: Boolean
"Clean scene transcodes without scene entries"
transcodes: Boolean
"Clean marker files without marker entries"
markers: Boolean
"Clean image thumbnails/clips without image entries"
imageThumbnails: Boolean
"Do a dry run. Don't delete any files"
dryRun: Boolean
}
input AutoTagMetadataInput {
"Paths to tag, null for all files"
paths: [String!]
@@ -320,6 +323,9 @@ type SystemStatus {
configPath: String
appSchema: Int!
status: SystemStatusEnum!
os: String!
workingDir: String!
homeDir: String!
}
input MigrateInput {

View File

@@ -1,13 +1,10 @@
type Movie {
id: ID!
name: String!
checksum: String! @deprecated(reason: "MD5 hash of name, use name directly")
aliases: String
"Duration in seconds"
duration: Int
date: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
studio: Studio
@@ -29,8 +26,6 @@ input MovieCreateInput {
"Duration in seconds"
duration: Int
date: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
studio_id: ID
@@ -49,8 +44,6 @@ input MovieUpdateInput {
aliases: String
duration: Int
date: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
studio_id: ID
@@ -66,8 +59,6 @@ input MovieUpdateInput {
input BulkMovieUpdateInput {
clientMutationId: String
ids: [ID!]
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
studio_id: ID

View File

@@ -0,0 +1,36 @@
enum PackageType {
Scraper
Plugin
}
type Package {
package_id: String!
name: String!
version: String
date: Timestamp
requires: [Package!]!
sourceURL: String!
"The version of this package currently available from the remote source"
source_package: Package
metadata: Map!
}
input PackageSpecInput {
id: String!
sourceURL: String!
}
type PackageSource {
name: String
url: String!
local_path: String
}
input PackageSourceInput {
name: String
url: String!
local_path: String
}

View File

@@ -14,7 +14,6 @@ enum CircumisedEnum {
type Performer {
id: ID!
checksum: String @deprecated(reason: "Not used")
name: String!
disambiguation: String
url: String
@@ -25,7 +24,6 @@ type Performer {
ethnicity: String
country: String
eye_color: String
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
@@ -34,7 +32,6 @@ type Performer {
career_length: String
tattoos: String
piercings: String
aliases: String @deprecated(reason: "Use alias_list")
alias_list: [String!]!
favorite: Boolean!
tags: [Tag!]!
@@ -49,8 +46,6 @@ type Performer {
o_counter: Int # Resolver
scenes: [Scene!]!
stash_ids: [StashID!]!
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
@@ -71,8 +66,6 @@ input PerformerCreateInput {
ethnicity: String
country: String
eye_color: String
# height must be parsable into an integer
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
@@ -81,7 +74,6 @@ input PerformerCreateInput {
career_length: String
tattoos: String
piercings: String
aliases: String @deprecated(reason: "Use alias_list")
alias_list: [String!]
twitter: String
instagram: String
@@ -90,8 +82,6 @@ input PerformerCreateInput {
"This should be a URL or a base64 encoded data URL"
image: String
stash_ids: [StashIDInput!]
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
@@ -111,8 +101,6 @@ input PerformerUpdateInput {
ethnicity: String
country: String
eye_color: String
# height must be parsable into an integer
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
@@ -121,7 +109,6 @@ input PerformerUpdateInput {
career_length: String
tattoos: String
piercings: String
aliases: String @deprecated(reason: "Use alias_list")
alias_list: [String!]
twitter: String
instagram: String
@@ -130,8 +117,6 @@ input PerformerUpdateInput {
"This should be a URL or a base64 encoded data URL"
image: String
stash_ids: [StashIDInput!]
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
@@ -156,8 +141,6 @@ input BulkPerformerUpdateInput {
ethnicity: String
country: String
eye_color: String
# height must be parsable into an integer
height: String @deprecated(reason: "Use height_cm")
height_cm: Int
measurements: String
fake_tits: String
@@ -166,14 +149,11 @@ input BulkPerformerUpdateInput {
career_length: String
tattoos: String
piercings: String
aliases: String @deprecated(reason: "Use alias_list")
alias_list: BulkUpdateStrings
twitter: String
instagram: String
favorite: Boolean
tag_ids: BulkUpdateIds
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String

View File

@@ -1,3 +1,10 @@
type PluginPaths {
# path to javascript files
javascript: [String!]
# path to css files
css: [String!]
}
type Plugin {
id: ID!
name: String!
@@ -5,8 +12,19 @@ type Plugin {
url: String
version: String
enabled: Boolean!
tasks: [PluginTask!]
hooks: [PluginHook!]
settings: [PluginSetting!]
"""
Plugin IDs of plugins that this plugin depends on.
Applies only for UI plugins to indicate css/javascript load order.
"""
requires: [ID!]
paths: PluginPaths!
}
type PluginTask {
@@ -40,3 +58,16 @@ input PluginValueInput {
o: [PluginArgInput!]
a: [PluginValueInput!]
}
enum PluginSettingTypeEnum {
STRING
NUMBER
BOOLEAN
}
type PluginSetting {
name: String!
display_name: String
description: String
type: PluginSettingTypeEnum!
}

View File

@@ -1,3 +1,6 @@
"An RFC3339 timestamp"
scalar Time
"""
Timestamp is a point in time. It is always output as RFC3339-compatible time points.
It can be input as a RFC3339 string, or as "<4h" for "4 hours in the past" or ">5m"
@@ -5,9 +8,18 @@ for "5 minutes in the future"
"""
scalar Timestamp
# generic JSON object
"A String -> Any map"
scalar Map
"A String -> Boolean map"
scalar BoolMap
"A plugin ID -> Map (String -> Any map) map"
scalar PluginConfigMap
scalar Any
scalar Int64
"A multipart file upload"
scalar Upload

View File

@@ -15,7 +15,6 @@ type ScenePathsType {
stream: String # Resolver
webp: String # Resolver
vtt: String # Resolver
chapters_vtt: String @deprecated
sprite: String # Resolver
funscript: String # Resolver
interactive_heatmap: String # Resolver
@@ -34,8 +33,6 @@ type VideoCaption {
type Scene {
id: ID!
checksum: String @deprecated(reason: "Use files.fingerprints")
oshash: String @deprecated(reason: "Use files.fingerprints")
title: String
code: String
details: String
@@ -43,20 +40,15 @@ type Scene {
url: String @deprecated(reason: "Use urls")
urls: [String!]!
date: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean!
o_counter: Int
path: String! @deprecated(reason: "Use files.path")
phash: String @deprecated(reason: "Use files.fingerprints")
interactive: Boolean!
interactive_speed: Int
captions: [VideoCaption!]
created_at: Time!
updated_at: Time!
file_mod_time: Time
"The last time play count was updated"
last_played_at: Time
"The time index a scene was left at"
@@ -66,7 +58,11 @@ type Scene {
"The number ot times a scene has been played"
play_count: Int
file: SceneFileType! @deprecated(reason: "Use files")
"Times a scene was played"
play_history: [Time!]!
"Times the o counter was incremented"
o_history: [Time!]!
files: [VideoFile!]!
paths: ScenePathsType! # Resolver
scene_markers: [SceneMarker!]!
@@ -94,8 +90,6 @@ input SceneCreateInput {
url: String @deprecated(reason: "Use urls")
urls: [String!]
date: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
@@ -126,11 +120,10 @@ input SceneUpdateInput {
url: String @deprecated(reason: "Use urls")
urls: [String!]
date: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
o_counter: Int
@deprecated(reason: "Unsupported - Use sceneIncrementO/sceneDecrementO")
organized: Boolean
studio_id: ID
gallery_ids: [ID!]
@@ -147,6 +140,9 @@ input SceneUpdateInput {
play_duration: Float
"The number ot times a scene has been played"
play_count: Int
@deprecated(
reason: "Unsupported - Use sceneIncrementPlayCount/sceneDecrementPlayCount"
)
primary_file_id: ID
}
@@ -172,8 +168,6 @@ input BulkSceneUpdateInput {
url: String @deprecated(reason: "Use urls")
urls: BulkUpdateStrings
date: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
organized: Boolean
@@ -266,4 +260,13 @@ input SceneMergeInput {
destination: ID!
# values defined here will override values in the destination
values: SceneUpdateInput
# if true, the source history will be combined with the destination
play_history: Boolean
o_history: Boolean
}
type HistoryMutationResult {
count: Int!
history: [Time!]!
}

View File

@@ -99,7 +99,9 @@ input ScrapedSceneInput {
type ScrapedGallery {
title: String
code: String
details: String
photographer: String
url: String @deprecated(reason: "use urls")
urls: [String!]
date: String
@@ -111,7 +113,9 @@ type ScrapedGallery {
input ScrapedGalleryInput {
title: String
code: String
details: String
photographer: String
url: String @deprecated(reason: "use urls")
urls: [String!]
date: String

View File

@@ -1,7 +1,6 @@
type Studio {
id: ID!
name: String!
checksum: String! @deprecated(reason: "MD5 hash of name, use name directly")
url: String
parent_studio: Studio
child_studios: [Studio!]!
@@ -15,8 +14,6 @@ type Studio {
performer_count(depth: Int): Int! # Resolver
movie_count(depth: Int): Int! # Resolver
stash_ids: [StashID!]!
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
@@ -32,8 +29,6 @@ input StudioCreateInput {
"This should be a URL or a base64 encoded data URL"
image: String
stash_ids: [StashIDInput!]
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String
@@ -49,8 +44,6 @@ input StudioUpdateInput {
"This should be a URL or a base64 encoded data URL"
image: String
stash_ids: [StashIDInput!]
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
details: String

38
internal/api/bool_map.go Normal file
View File

@@ -0,0 +1,38 @@
package api
import (
"encoding/json"
"fmt"
"io"
"github.com/99designs/gqlgen/graphql"
)
func MarshalBoolMap(val map[string]bool) graphql.Marshaler {
return graphql.WriterFunc(func(w io.Writer) {
err := json.NewEncoder(w).Encode(val)
if err != nil {
panic(err)
}
})
}
func UnmarshalBoolMap(v interface{}) (map[string]bool, error) {
m, ok := v.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%T is not a map", v)
}
result := make(map[string]bool)
for k, v := range m {
key := k
val, ok := v.(bool)
if !ok {
return nil, fmt.Errorf("key %s (%T) is not a bool", k, v)
}
result[key] = val
}
return result, nil
}

View File

@@ -153,40 +153,6 @@ func (t changesetTranslator) intPtrFromString(value *string) (*int, error) {
return &vv, nil
}
func (t changesetTranslator) ratingConversion(legacyValue *int, rating100Value *int) *int {
const (
legacyField = "rating"
rating100Field = "rating100"
)
legacyRating := t.optionalInt(legacyValue, legacyField)
if legacyRating.Set && !legacyRating.Null {
ret := models.Rating5To100(legacyRating.Value)
return &ret
}
o := t.optionalInt(rating100Value, rating100Field)
if o.Set && !o.Null {
return &o.Value
}
return nil
}
func (t changesetTranslator) optionalRatingConversion(legacyValue *int, rating100Value *int) models.OptionalInt {
const (
legacyField = "rating"
rating100Field = "rating100"
)
legacyRating := t.optionalInt(legacyValue, legacyField)
if legacyRating.Set && !legacyRating.Null {
legacyRating.Value = models.Rating5To100(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{}

View File

@@ -26,8 +26,8 @@ const defaultSHLength int = 8 // default length of SHA short hash returned by <g
var stashReleases = func() map[string]string {
return map[string]string{
"darwin/amd64": "stash-osx",
"darwin/arm64": "stash-osx-applesilicon",
"darwin/amd64": "stash-macos",
"darwin/arm64": "stash-macos",
"linux/amd64": "stash-linux",
"windows/amd64": "stash-win.exe",
"linux/arm": "stash-linux-arm32v6",

View File

@@ -13,4 +13,5 @@ const (
tagKey
downloadKey
imageKey
pluginKey
)

View File

@@ -4,6 +4,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"
"golang.org/x/text/collate"
)
@@ -25,13 +26,27 @@ func (s dirLister) Bytes(i int) []byte {
// listDir will return the contents of a given directory path as a string slice
func listDir(col *collate.Collator, path string) ([]string, error) {
var dirPaths []string
dirPath := path
files, err := os.ReadDir(path)
if err != nil {
path = filepath.Dir(path)
files, err = os.ReadDir(path)
dirPath = filepath.Dir(path)
dirFiles, err := os.ReadDir(dirPath)
if err != nil {
return dirPaths, err
}
// Filter dir contents by last path fragment if the dir isn't an exact match
base := strings.ToLower(filepath.Base(path))
if base != "." && base != string(filepath.Separator) {
for _, file := range dirFiles {
if strings.HasPrefix(strings.ToLower(file.Name()), base) {
files = append(files, file)
}
}
} else {
files = dirFiles
}
}
if col != nil {
@@ -42,7 +57,7 @@ func listDir(col *collate.Collator, path string) ([]string, error) {
if !file.IsDir() {
continue
}
dirPaths = append(dirPaths, filepath.Join(path, file.Name()))
dirPaths = append(dirPaths, filepath.Join(dirPath, file.Name()))
}
return dirPaths, nil
}

View File

@@ -1,12 +1,13 @@
package api
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"strings"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/hash"
"github.com/stashapp/stash/pkg/logger"
@@ -18,7 +19,7 @@ type imageBox struct {
files []string
}
var imageExtensions = []string{
var imageBoxExts = []string{
".jpg",
".jpeg",
".png",
@@ -42,7 +43,7 @@ func newImageBox(box fs.FS) (*imageBox, error) {
}
baseName := strings.ToLower(d.Name())
for _, ext := range imageExtensions {
for _, ext := range imageBoxExts {
if strings.HasSuffix(baseName, ext) {
ret.files = append(ret.files, path)
break
@@ -55,65 +56,14 @@ func newImageBox(box fs.FS) (*imageBox, error) {
return ret, err
}
var performerBox *imageBox
var performerBoxMale *imageBox
var performerBoxCustom *imageBox
func initialiseImages() {
var err error
performerBox, err = newImageBox(&static.Performer)
if err != nil {
logger.Warnf("error loading performer images: %v", err)
}
performerBoxMale, err = newImageBox(&static.PerformerMale)
if err != nil {
logger.Warnf("error loading male performer images: %v", err)
}
initialiseCustomImages()
}
func initialiseCustomImages() {
customPath := config.GetInstance().GetCustomPerformerImageLocation()
if customPath != "" {
logger.Debugf("Loading custom performer images from %s", customPath)
// We need to set performerBoxCustom at runtime, as this is a custom path, and store it in a pointer.
var err error
performerBoxCustom, err = newImageBox(os.DirFS(customPath))
if err != nil {
logger.Warnf("error loading custom performer from %s: %v", customPath, err)
}
} else {
performerBoxCustom = nil
}
}
func getRandomPerformerImageUsingName(name string, gender *models.GenderEnum, customPath string) ([]byte, error) {
var box *imageBox
// If we have a custom path, we should return a new box in the given path.
if performerBoxCustom != nil && len(performerBoxCustom.files) > 0 {
box = performerBoxCustom
func (box *imageBox) GetRandomImageByName(name string) ([]byte, error) {
files := box.files
if len(files) == 0 {
return nil, errors.New("box is empty")
}
var g models.GenderEnum
if gender != nil {
g = *gender
}
if box == nil {
switch g {
case models.GenderEnumFemale, models.GenderEnumTransgenderFemale:
box = performerBox
case models.GenderEnumMale, models.GenderEnumTransgenderMale:
box = performerBoxMale
default:
box = performerBox
}
}
imageFiles := box.files
index := hash.IntFromString(name) % uint64(len(imageFiles))
img, err := box.box.Open(imageFiles[index])
index := hash.IntFromString(name) % uint64(len(files))
img, err := box.box.Open(files[index])
if err != nil {
return nil, err
}
@@ -121,3 +71,64 @@ func getRandomPerformerImageUsingName(name string, gender *models.GenderEnum, cu
return io.ReadAll(img)
}
var performerBox *imageBox
var performerBoxMale *imageBox
var performerBoxCustom *imageBox
func init() {
var err error
performerBox, err = newImageBox(static.Sub(static.Performer))
if err != nil {
panic(fmt.Sprintf("loading performer images: %v", err))
}
performerBoxMale, err = newImageBox(static.Sub(static.PerformerMale))
if err != nil {
panic(fmt.Sprintf("loading male performer images: %v", err))
}
}
func initCustomPerformerImages(customPath string) {
if customPath != "" {
logger.Debugf("Loading custom performer images from %s", customPath)
var err error
performerBoxCustom, err = newImageBox(os.DirFS(customPath))
if err != nil {
logger.Warnf("error loading custom performer images from %s: %v", customPath, err)
}
} else {
performerBoxCustom = nil
}
}
func getDefaultPerformerImage(name string, gender *models.GenderEnum) []byte {
// try the custom box first if we have one
if performerBoxCustom != nil {
ret, err := performerBoxCustom.GetRandomImageByName(name)
if err == nil {
return ret
}
logger.Warnf("error loading custom default performer image: %v", err)
}
var g models.GenderEnum
if gender != nil {
g = *gender
}
var box *imageBox
switch g {
case models.GenderEnumFemale, models.GenderEnumTransgenderFemale:
box = performerBox
case models.GenderEnumMale, models.GenderEnumTransgenderMale:
box = performerBoxMale
default:
box = performerBox
}
ret, err := box.GetRandomImageByName(name)
if err != nil {
logger.Warnf("error loading default performer image: %v", err)
}
return ret
}

View File

@@ -9,7 +9,11 @@
//go:generate go run github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
//go:generate go run github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
//go:generate go run github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
//go:generate go run github.com/vektah/dataloaden SceneOCountLoader int int
//go:generate go run github.com/vektah/dataloaden ScenePlayCountLoader int int
//go:generate go run github.com/vektah/dataloaden SceneOHistoryLoader int []time.Time
//go:generate go run github.com/vektah/dataloaden ScenePlayHistoryLoader int []time.Time
//go:generate go run github.com/vektah/dataloaden SceneLastPlayedLoader int *time.Time
package loaders
import (
@@ -17,9 +21,7 @@ import (
"net/http"
"time"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
)
type contextKey struct{ name string }
@@ -34,8 +36,14 @@ const (
)
type Loaders struct {
SceneByID *SceneLoader
SceneFiles *SceneFileIDsLoader
SceneByID *SceneLoader
SceneFiles *SceneFileIDsLoader
ScenePlayCount *ScenePlayCountLoader
SceneOCount *SceneOCountLoader
ScenePlayHistory *ScenePlayHistoryLoader
SceneOHistory *SceneOHistoryLoader
SceneLastPlayed *SceneLastPlayedLoader
ImageFiles *ImageFileIDsLoader
GalleryFiles *GalleryFileIDsLoader
@@ -49,8 +57,7 @@ type Loaders struct {
}
type Middleware struct {
DatabaseProvider txn.DatabaseProvider
Repository manager.Repository
Repository models.Repository
}
func (m Middleware) Middleware(next http.Handler) http.Handler {
@@ -112,6 +119,31 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
maxBatch: maxBatch,
fetch: m.fetchGalleriesFileIDs(ctx),
},
ScenePlayCount: &ScenePlayCountLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchScenesPlayCount(ctx),
},
SceneOCount: &SceneOCountLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchScenesOCount(ctx),
},
ScenePlayHistory: &ScenePlayHistoryLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchScenesPlayHistory(ctx),
},
SceneLastPlayed: &SceneLastPlayedLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchScenesLastPlayed(ctx),
},
SceneOHistory: &SceneOHistoryLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchScenesOHistory(ctx),
},
}
newCtx := context.WithValue(r.Context(), loadersCtxKey, ldrs)
@@ -131,13 +163,9 @@ func toErrorSlice(err error) []error {
return nil
}
func (m Middleware) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithDatabase(ctx, m.DatabaseProvider, fn)
}
func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models.Scene, []error) {
return func(keys []int) (ret []*models.Scene, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Scene.FindMany(ctx, keys)
return err
@@ -148,7 +176,7 @@ func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models
func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models.Image, []error) {
return func(keys []int) (ret []*models.Image, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Image.FindMany(ctx, keys)
return err
@@ -160,7 +188,7 @@ func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models
func (m Middleware) fetchGalleries(ctx context.Context) func(keys []int) ([]*models.Gallery, []error) {
return func(keys []int) (ret []*models.Gallery, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Gallery.FindMany(ctx, keys)
return err
@@ -172,7 +200,7 @@ func (m Middleware) fetchGalleries(ctx context.Context) func(keys []int) ([]*mod
func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*models.Performer, []error) {
return func(keys []int) (ret []*models.Performer, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Performer.FindMany(ctx, keys)
return err
@@ -184,7 +212,7 @@ func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*mo
func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*models.Studio, []error) {
return func(keys []int) (ret []*models.Studio, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Studio.FindMany(ctx, keys)
return err
@@ -195,7 +223,7 @@ func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*model
func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.Tag, []error) {
return func(keys []int) (ret []*models.Tag, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Tag.FindMany(ctx, keys)
return err
@@ -206,7 +234,7 @@ func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.T
func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models.Movie, []error) {
return func(keys []int) (ret []*models.Movie, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Movie.FindMany(ctx, keys)
return err
@@ -217,7 +245,7 @@ func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models
func (m Middleware) fetchFiles(ctx context.Context) func(keys []models.FileID) ([]models.File, []error) {
return func(keys []models.FileID) (ret []models.File, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.File.Find(ctx, keys...)
return err
@@ -228,7 +256,7 @@ func (m Middleware) fetchFiles(ctx context.Context) func(keys []models.FileID) (
func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
return func(keys []int) (ret [][]models.FileID, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Scene.GetManyFileIDs(ctx, keys)
return err
@@ -239,7 +267,7 @@ func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([]
func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
return func(keys []int) (ret [][]models.FileID, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Image.GetManyFileIDs(ctx, keys)
return err
@@ -250,7 +278,7 @@ func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([]
func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
return func(keys []int) (ret [][]models.FileID, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Gallery.GetManyFileIDs(ctx, keys)
return err
@@ -258,3 +286,58 @@ func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int)
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchScenesOCount(ctx context.Context) func(keys []int) ([]int, []error) {
return func(keys []int) (ret []int, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Scene.GetManyOCount(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchScenesPlayCount(ctx context.Context) func(keys []int) ([]int, []error) {
return func(keys []int) (ret []int, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Scene.GetManyViewCount(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchScenesOHistory(ctx context.Context) func(keys []int) ([][]time.Time, []error) {
return func(keys []int) (ret [][]time.Time, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Scene.GetManyODates(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchScenesPlayHistory(ctx context.Context) func(keys []int) ([][]time.Time, []error) {
return func(keys []int) (ret [][]time.Time, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Scene.GetManyViewDates(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchScenesLastPlayed(ctx context.Context) func(keys []int) ([]*time.Time, []error) {
return func(keys []int) (ret []*time.Time, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Scene.GetManyLastViewed(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,64 +1,64 @@
package api
import (
"errors"
"fmt"
"io"
"strconv"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
type BaseFile interface{}
type BaseFile interface {
IsBaseFile()
}
type VisualFile interface {
IsVisualFile()
}
func convertVisualFile(f models.File) (VisualFile, error) {
switch f := f.(type) {
case VisualFile:
return f, nil
case *models.VideoFile:
return &VideoFile{VideoFile: f}, nil
case *models.ImageFile:
return &ImageFile{ImageFile: f}, nil
default:
return nil, fmt.Errorf("file %s is not a visual file", f.Base().Path)
}
}
type GalleryFile struct {
*models.BaseFile
}
var ErrTimestamp = errors.New("cannot parse Timestamp")
func (GalleryFile) IsBaseFile() {}
func MarshalTimestamp(t time.Time) graphql.Marshaler {
if t.IsZero() {
return graphql.Null
}
func (GalleryFile) IsVisualFile() {}
return graphql.WriterFunc(func(w io.Writer) {
_, err := io.WriteString(w, strconv.Quote(t.Format(time.RFC3339Nano)))
if err != nil {
logger.Warnf("could not marshal timestamp: %v", err)
}
})
func (f *GalleryFile) Fingerprints() []models.Fingerprint {
return f.BaseFile.Fingerprints
}
func UnmarshalTimestamp(v interface{}) (time.Time, error) {
if tmpStr, ok := v.(string); ok {
if len(tmpStr) == 0 {
return time.Time{}, fmt.Errorf("%w: empty string", ErrTimestamp)
}
switch tmpStr[0] {
case '>', '<':
d, err := time.ParseDuration(tmpStr[1:])
if err != nil {
return time.Time{}, fmt.Errorf("%w: cannot parse %v-duration: %v", ErrTimestamp, tmpStr[0], err)
}
t := time.Now()
// Compute point in time:
if tmpStr[0] == '<' {
t = t.Add(-d)
} else {
t = t.Add(d)
}
return t, nil
}
return utils.ParseDateStringAsTime(tmpStr)
}
return time.Time{}, fmt.Errorf("%w: not a string", ErrTimestamp)
type VideoFile struct {
*models.VideoFile
}
func (VideoFile) IsBaseFile() {}
func (VideoFile) IsVisualFile() {}
func (f *VideoFile) Fingerprints() []models.Fingerprint {
return f.VideoFile.Fingerprints
}
type ImageFile struct {
*models.ImageFile
}
func (ImageFile) IsBaseFile() {}
func (ImageFile) IsVisualFile() {}
func (f *ImageFile) Fingerprints() []models.Fingerprint {
return f.ImageFile.Fingerprints
}

View File

@@ -0,0 +1,37 @@
package api
import (
"encoding/json"
"fmt"
"io"
"github.com/99designs/gqlgen/graphql"
)
func MarshalPluginConfigMap(val map[string]map[string]interface{}) graphql.Marshaler {
return graphql.WriterFunc(func(w io.Writer) {
err := json.NewEncoder(w).Encode(val)
if err != nil {
panic(err)
}
})
}
func UnmarshalPluginConfigMap(v interface{}) (map[string]map[string]interface{}, error) {
m, ok := v.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%T is not a plugin config map", v)
}
result := make(map[string]map[string]interface{})
for k, v := range m {
val, ok := v.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("key %s (%T) is not a map", k, v)
}
result[k] = val
}
return result, nil
}

View File

@@ -11,9 +11,9 @@ import (
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/scraper"
"github.com/stashapp/stash/pkg/txn"
"github.com/stashapp/stash/pkg/scraper/stashbox"
)
var (
@@ -29,12 +29,11 @@ var (
)
type hookExecutor interface {
ExecutePostHooks(ctx context.Context, id int, hookType plugin.HookTriggerEnum, input interface{}, inputFields []string)
ExecutePostHooks(ctx context.Context, id int, hookType hook.TriggerEnum, input interface{}, inputFields []string)
}
type Resolver struct {
txnManager txn.Manager
repository manager.Repository
repository models.Repository
sceneService manager.SceneService
imageService manager.ImageService
galleryService manager.GalleryService
@@ -82,9 +81,24 @@ func (r *Resolver) Subscription() SubscriptionResolver {
func (r *Resolver) Tag() TagResolver {
return &tagResolver{r}
}
func (r *Resolver) GalleryFile() GalleryFileResolver {
return &galleryFileResolver{r}
}
func (r *Resolver) VideoFile() VideoFileResolver {
return &videoFileResolver{r}
}
func (r *Resolver) ImageFile() ImageFileResolver {
return &imageFileResolver{r}
}
func (r *Resolver) SavedFilter() SavedFilterResolver {
return &savedFilterResolver{r}
}
func (r *Resolver) Plugin() PluginResolver {
return &pluginResolver{r}
}
func (r *Resolver) ConfigResult() ConfigResultResolver {
return &configResultResolver{r}
}
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
@@ -99,14 +113,23 @@ type imageResolver struct{ *Resolver }
type studioResolver struct{ *Resolver }
type movieResolver struct{ *Resolver }
type tagResolver struct{ *Resolver }
type galleryFileResolver struct{ *Resolver }
type videoFileResolver struct{ *Resolver }
type imageFileResolver struct{ *Resolver }
type savedFilterResolver struct{ *Resolver }
type pluginResolver struct{ *Resolver }
type configResultResolver struct{ *Resolver }
func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithTxn(ctx, r.txnManager, fn)
return r.repository.WithTxn(ctx, fn)
}
func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithReadTxn(ctx, r.txnManager, fn)
return r.repository.WithReadTxn(ctx, fn)
}
func (r *Resolver) stashboxRepository() stashbox.Repository {
return stashbox.NewRepository(r.repository)
}
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
@@ -145,27 +168,90 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
var ret StatsResultType
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
repo := r.repository
scenesQB := repo.Scene
sceneQB := repo.Scene
imageQB := repo.Image
galleryQB := repo.Gallery
studiosQB := repo.Studio
performersQB := repo.Performer
moviesQB := repo.Movie
tagsQB := repo.Tag
scenesCount, _ := scenesQB.Count(ctx)
scenesSize, _ := scenesQB.Size(ctx)
scenesDuration, _ := scenesQB.Duration(ctx)
imageCount, _ := imageQB.Count(ctx)
imageSize, _ := imageQB.Size(ctx)
galleryCount, _ := galleryQB.Count(ctx)
performersCount, _ := performersQB.Count(ctx)
studiosCount, _ := studiosQB.Count(ctx)
moviesCount, _ := moviesQB.Count(ctx)
tagsCount, _ := tagsQB.Count(ctx)
totalOCount, _ := scenesQB.OCount(ctx)
totalPlayDuration, _ := scenesQB.PlayDuration(ctx)
totalPlayCount, _ := scenesQB.PlayCount(ctx)
uniqueScenePlayCount, _ := scenesQB.UniqueScenePlayCount(ctx)
studioQB := repo.Studio
performerQB := repo.Performer
movieQB := repo.Movie
tagQB := repo.Tag
// embrace the error
scenesCount, err := sceneQB.Count(ctx)
if err != nil {
return err
}
scenesSize, err := sceneQB.Size(ctx)
if err != nil {
return err
}
scenesDuration, err := sceneQB.Duration(ctx)
if err != nil {
return err
}
imageCount, err := imageQB.Count(ctx)
if err != nil {
return err
}
imageSize, err := imageQB.Size(ctx)
if err != nil {
return err
}
galleryCount, err := galleryQB.Count(ctx)
if err != nil {
return err
}
performersCount, err := performerQB.Count(ctx)
if err != nil {
return err
}
studiosCount, err := studioQB.Count(ctx)
if err != nil {
return err
}
moviesCount, err := movieQB.Count(ctx)
if err != nil {
return err
}
tagsCount, err := tagQB.Count(ctx)
if err != nil {
return err
}
scenesTotalOCount, err := sceneQB.GetAllOCount(ctx)
if err != nil {
return err
}
imagesTotalOCount, err := imageQB.OCount(ctx)
if err != nil {
return err
}
totalOCount := scenesTotalOCount + imagesTotalOCount
totalPlayDuration, err := sceneQB.PlayDuration(ctx)
if err != nil {
return err
}
totalPlayCount, err := sceneQB.CountAllViews(ctx)
if err != nil {
return err
}
uniqueScenePlayCount, err := sceneQB.CountUniqueViews(ctx)
if err != nil {
return err
}
ret = StatsResultType{
SceneCount: scenesCount,

View File

@@ -0,0 +1,25 @@
package api
import (
"context"
"github.com/stashapp/stash/internal/manager/config"
)
func (r *configResultResolver) Plugins(ctx context.Context, obj *ConfigResult, include []string) (map[string]map[string]interface{}, error) {
if len(include) == 0 {
ret := config.GetInstance().GetAllPluginConfiguration()
return ret, nil
}
ret := make(map[string]map[string]interface{})
for _, plugin := range include {
c := config.GetInstance().GetPluginConfiguration(plugin)
if len(c) > 0 {
ret[plugin] = c
}
}
return ret, nil
}

View File

@@ -0,0 +1,30 @@
package api
import "context"
func (r *galleryFileResolver) Fingerprint(ctx context.Context, obj *GalleryFile, type_ string) (*string, error) {
fp := obj.BaseFile.Fingerprints.For(type_)
if fp != nil {
v := fp.Value()
return &v, nil
}
return nil, nil
}
func (r *imageFileResolver) Fingerprint(ctx context.Context, obj *ImageFile, type_ string) (*string, error) {
fp := obj.ImageFile.Fingerprints.For(type_)
if fp != nil {
v := fp.Value()
return &v, nil
}
return nil, nil
}
func (r *videoFileResolver) Fingerprint(ctx context.Context, obj *VideoFile, type_ string) (*string, error) {
fp := obj.VideoFile.Fingerprints.For(type_)
if fp != nil {
v := fp.Value()
return &v, nil
}
return nil, nil
}

View File

@@ -2,7 +2,6 @@ package api
import (
"context"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/manager/config"
@@ -11,19 +10,6 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *galleryResolver) getPrimaryFile(ctx context.Context, obj *models.Gallery) (models.File, error) {
if obj.PrimaryFileID != nil {
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
if err != nil {
return nil, err
}
return f, nil
}
return nil, nil
}
func (r *galleryResolver) getFiles(ctx context.Context, obj *models.Gallery) ([]models.File, error) {
fileIDs, err := loaders.From(ctx).GalleryFiles.Load(obj.ID)
if err != nil {
@@ -78,38 +64,6 @@ func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*mod
return ret, nil
}
func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f != nil {
return &f.Base().ModTime, nil
}
return nil, nil
}
// Images is deprecated, slow and shouldn't be used
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret []*models.Image, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
// #2376 - sort images by path
// doing this via Query is really slow, so stick with FindByGalleryID
ret, err = r.repository.Image.FindByGalleryID(ctx, obj.ID)
if err != nil {
return err
}
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *models.Image, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
// Find cover image first
@@ -130,26 +84,6 @@ func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*strin
return nil, nil
}
func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (string, error) {
if !obj.Files.PrimaryLoaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadPrimaryFile(ctx, r.repository.File)
}); err != nil {
return "", err
}
}
return obj.PrimaryChecksum(), nil
}
func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *galleryResolver) Rating100(ctx context.Context, obj *models.Gallery) (*int, error) {
return obj.Rating, nil
}

View File

@@ -2,35 +2,12 @@ package api
import (
"context"
"fmt"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/models"
)
func convertVisualFile(f models.File) (models.VisualFile, error) {
vf, ok := f.(models.VisualFile)
if !ok {
return nil, fmt.Errorf("file %s is not a visual file", f.Base().Path)
}
return vf, nil
}
func (r *imageResolver) getPrimaryFile(ctx context.Context, obj *models.Image) (models.VisualFile, error) {
if obj.PrimaryFileID != nil {
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
if err != nil {
return nil, err
}
return convertVisualFile(f)
}
return nil, nil
}
func (r *imageResolver) getFiles(ctx context.Context, obj *models.Image) ([]models.File, error) {
fileIDs, err := loaders.From(ctx).ImageFiles.Load(obj.ID)
if err != nil {
@@ -46,33 +23,13 @@ func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string,
return &ret, nil
}
func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFileType, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f == nil {
return nil, nil
}
width := f.GetWidth()
height := f.GetHeight()
size := f.Base().Size
return &ImageFileType{
Size: int(size),
Width: width,
Height: height,
}, nil
}
func (r *imageResolver) VisualFiles(ctx context.Context, obj *models.Image) ([]models.VisualFile, error) {
func (r *imageResolver) VisualFiles(ctx context.Context, obj *models.Image) ([]VisualFile, error) {
files, err := r.getFiles(ctx, obj)
if err != nil {
return nil, err
}
ret := make([]models.VisualFile, len(files))
ret := make([]VisualFile, len(files))
for i, f := range files {
ret[i], err = convertVisualFile(f)
if err != nil {
@@ -91,13 +48,13 @@ func (r *imageResolver) Date(ctx context.Context, obj *models.Image) (*string, e
return nil, nil
}
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*models.ImageFile, error) {
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) {
files, err := r.getFiles(ctx, obj)
if err != nil {
return nil, err
}
var ret []*models.ImageFile
var ret []*ImageFile
for _, f := range files {
// filter out non-image files
@@ -106,24 +63,14 @@ func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*models
continue
}
ret = append(ret, imageFile)
ret = append(ret, &ImageFile{
ImageFile: imageFile,
})
}
return ret, nil
}
func (r *imageResolver) FileModTime(ctx context.Context, obj *models.Image) (*time.Time, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f != nil {
return &f.Base().ModTime, nil
}
return nil, nil
}
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*ImagePathsType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewImageURLBuilder(baseURL, obj)
@@ -151,14 +98,6 @@ func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret [
return ret, firstError(errs)
}
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *imageResolver) Rating100(ctx context.Context, obj *models.Image) (*int, error) {
return obj.Rating, nil
}

View File

@@ -5,15 +5,9 @@ import (
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/hash/md5"
"github.com/stashapp/stash/pkg/models"
)
func (r *movieResolver) Checksum(ctx context.Context, obj *models.Movie) (string, error) {
// generate checksum from movie name
return md5.FromString(obj.Name), nil
}
func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, error) {
if obj.Date != nil {
result := obj.Date.String()
@@ -22,14 +16,6 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e
return nil, nil
}
func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) {
return obj.Rating, nil
}

View File

@@ -3,7 +3,6 @@ package api
import (
"context"
"strconv"
"strings"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
@@ -13,24 +12,6 @@ import (
"github.com/stashapp/stash/pkg/performer"
)
// Checksum is deprecated
func (r *performerResolver) Checksum(ctx context.Context, obj *models.Performer) (*string, error) {
return nil, nil
}
func (r *performerResolver) Aliases(ctx context.Context, obj *models.Performer) (*string, error) {
if !obj.Aliases.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadAliases(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
ret := strings.Join(obj.Aliases.List(), ", ")
return &ret, nil
}
func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer) ([]string, error) {
if !obj.Aliases.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
@@ -186,14 +167,6 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer)
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
}
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *performerResolver) Rating100(ctx context.Context, obj *models.Performer) (*int, error) {
return obj.Rating, nil
}

View File

@@ -0,0 +1,61 @@
package api
import (
"context"
"github.com/stashapp/stash/pkg/plugin"
)
type pluginURLBuilder struct {
BaseURL string
Plugin *plugin.Plugin
}
func (b pluginURLBuilder) javascript() []string {
ui := b.Plugin.UI
if len(ui.Javascript) == 0 && len(ui.ExternalScript) == 0 {
return nil
}
var ret []string
ret = append(ret, ui.ExternalScript...)
ret = append(ret, b.BaseURL+"/plugin/"+b.Plugin.ID+"/javascript")
return ret
}
func (b pluginURLBuilder) css() []string {
ui := b.Plugin.UI
if len(ui.CSS) == 0 && len(ui.ExternalCSS) == 0 {
return nil
}
var ret []string
ret = append(ret, b.Plugin.UI.ExternalCSS...)
ret = append(ret, b.BaseURL+"/plugin/"+b.Plugin.ID+"/css")
return ret
}
func (b *pluginURLBuilder) paths() *PluginPaths {
return &PluginPaths{
Javascript: b.javascript(),
CSS: b.css(),
}
}
func (r *pluginResolver) Paths(ctx context.Context, obj *plugin.Plugin) (*PluginPaths, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
b := pluginURLBuilder{
BaseURL: baseURL,
Plugin: obj,
}
return b.paths(), nil
}
func (r *pluginResolver) Requires(ctx context.Context, obj *plugin.Plugin) ([]string, error) {
return obj.UI.Requires, nil
}

View File

@@ -3,14 +3,12 @@ package api
import (
"context"
"fmt"
"strconv"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func convertVideoFile(f models.File) (*models.VideoFile, error) {
@@ -68,18 +66,6 @@ func (r *sceneResolver) getFiles(ctx context.Context, obj *models.Scene) ([]*mod
return ret, nil
}
func (r *sceneResolver) FileModTime(ctx context.Context, obj *models.Scene) (*time.Time, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f != nil {
return &f.ModTime, nil
}
return nil, nil
}
func (r *sceneResolver) Date(ctx context.Context, obj *models.Scene) (*string, error) {
if obj.Date != nil {
result := obj.Date.String()
@@ -88,38 +74,21 @@ func (r *sceneResolver) Date(ctx context.Context, obj *models.Scene) (*string, e
return nil, nil
}
// File is deprecated
func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.SceneFileType, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f == nil {
return nil, nil
}
bitrate := int(f.BitRate)
size := strconv.FormatInt(f.Size, 10)
return &models.SceneFileType{
Size: &size,
Duration: handleFloat64(f.Duration),
VideoCodec: &f.VideoCodec,
AudioCodec: &f.AudioCodec,
Width: &f.Width,
Height: &f.Height,
Framerate: handleFloat64(f.FrameRate),
Bitrate: &bitrate,
}, nil
}
func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*models.VideoFile, error) {
func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoFile, error) {
files, err := r.getFiles(ctx, obj)
if err != nil {
return nil, err
}
return files, nil
ret := make([]*VideoFile, len(files))
for i, f := range files {
ret[i] = &VideoFile{
VideoFile: f,
}
}
return ret, nil
}
func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, error) {
@@ -145,7 +114,6 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
objHash := obj.GetHash(config.GetVideoFileNamingAlgorithm())
vttPath := builder.GetSpriteVTTURL(objHash)
spritePath := builder.GetSpriteURL(objHash)
chaptersVttPath := builder.GetChaptersVTTURL()
funscriptPath := builder.GetFunscriptURL()
captionBasePath := builder.GetCaptionURL()
interactiveHeatmap := builder.GetInteractiveHeatmapURL()
@@ -156,7 +124,6 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePat
Stream: &streamPath,
Webp: &webpPath,
Vtt: &vttPath,
ChaptersVtt: &chaptersVttPath,
Sprite: &spritePath,
Funscript: &funscriptPath,
InteractiveHeatmap: &interactiveHeatmap,
@@ -285,30 +252,6 @@ func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
}
func (r *sceneResolver) Phash(ctx context.Context, obj *models.Scene) (*string, error) {
f, err := r.getPrimaryFile(ctx, obj)
if err != nil {
return nil, err
}
if f == nil {
return nil, nil
}
val := f.Fingerprints.Get(models.FingerprintTypePhash)
if val == nil {
return nil, nil
}
phash, _ := val.(int64)
if phash != 0 {
hexval := utils.PhashToString(phash)
return &hexval, nil
}
return nil, nil
}
func (r *sceneResolver) SceneStreams(ctx context.Context, obj *models.Scene) ([]*manager.SceneStreamEndpoint, error) {
// load the primary file into the scene
_, err := r.getPrimaryFile(ctx, obj)
@@ -377,3 +320,62 @@ func (r *sceneResolver) Urls(ctx context.Context, obj *models.Scene) ([]string,
return obj.URLs.List(), nil
}
func (r *sceneResolver) OCounter(ctx context.Context, obj *models.Scene) (*int, error) {
ret, err := loaders.From(ctx).SceneOCount.Load(obj.ID)
if err != nil {
return nil, err
}
return &ret, nil
}
func (r *sceneResolver) LastPlayedAt(ctx context.Context, obj *models.Scene) (*time.Time, error) {
ret, err := loaders.From(ctx).SceneLastPlayed.Load(obj.ID)
if err != nil {
return nil, err
}
return ret, nil
}
func (r *sceneResolver) PlayCount(ctx context.Context, obj *models.Scene) (*int, error) {
ret, err := loaders.From(ctx).ScenePlayCount.Load(obj.ID)
if err != nil {
return nil, err
}
return &ret, nil
}
func (r *sceneResolver) PlayHistory(ctx context.Context, obj *models.Scene) ([]*time.Time, error) {
ret, err := loaders.From(ctx).ScenePlayHistory.Load(obj.ID)
if err != nil {
return nil, err
}
// convert to pointer slice
ptrRet := make([]*time.Time, len(ret))
for i, t := range ret {
tt := t
ptrRet[i] = &tt
}
return ptrRet, nil
}
func (r *sceneResolver) OHistory(ctx context.Context, obj *models.Scene) ([]*time.Time, error) {
ret, err := loaders.From(ctx).SceneOHistory.Load(obj.ID)
if err != nil {
return nil, err
}
// convert to pointer slice
ptrRet := make([]*time.Time, len(ret))
for i, t := range ret {
tt := t
ptrRet[i] = &tt
}
return ptrRet, nil
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/hash/md5"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/movie"
@@ -14,11 +13,6 @@ import (
"github.com/stashapp/stash/pkg/scene"
)
func (r *studioResolver) Checksum(ctx context.Context, obj *models.Studio) (string, error) {
// generate checksum from studio name
return md5.FromString(obj.Name), nil
}
func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*string, error) {
var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
@@ -132,14 +126,6 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*m
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
}
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
if obj.Rating != nil {
rating := models.Rating100To5(*obj.Rating)
return &rating, nil
}
return nil, nil
}
func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*int, error) {
return obj.Rating, nil
}

View File

@@ -104,6 +104,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
refreshScraperCache := false
refreshScraperSource := false
existingScrapersPath := c.GetScrapersPath()
if input.ScrapersPath != nil && existingScrapersPath != *input.ScrapersPath {
if err := validateDir(config.ScrapersPath, *input.ScrapersPath, false); err != nil {
@@ -111,9 +112,23 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
refreshScraperCache = true
refreshScraperSource = true
c.Set(config.ScrapersPath, input.ScrapersPath)
}
refreshPluginCache := false
refreshPluginSource := false
existingPluginsPath := c.GetPluginsPath()
if input.PluginsPath != nil && existingPluginsPath != *input.PluginsPath {
if err := validateDir(config.PluginsPath, *input.PluginsPath, false); err != nil {
return makeConfigGeneralResult(), err
}
refreshPluginCache = true
refreshPluginSource = true
c.Set(config.PluginsPath, input.PluginsPath)
}
existingMetadataPath := c.GetMetadataPath()
if input.MetadataPath != nil && existingMetadataPath != *input.MetadataPath {
if err := validateDir(config.Metadata, *input.MetadataPath, true); err != nil {
@@ -316,21 +331,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
if input.CustomPerformerImageLocation != nil {
c.Set(config.CustomPerformerImageLocation, *input.CustomPerformerImageLocation)
initialiseCustomImages()
}
if input.ScraperUserAgent != nil {
c.Set(config.ScraperUserAgent, input.ScraperUserAgent)
refreshScraperCache = true
}
if input.ScraperCDPPath != nil {
c.Set(config.ScraperCDPPath, input.ScraperCDPPath)
refreshScraperCache = true
}
if input.ScraperCertCheck != nil {
c.Set(config.ScraperCertCheck, input.ScraperCertCheck)
initCustomPerformerImages(*input.CustomPerformerImageLocation)
}
if input.StashBoxes != nil {
@@ -361,6 +362,16 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
c.Set(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
}
if input.ScraperPackageSources != nil {
c.Set(config.ScraperPackageSources, input.ScraperPackageSources)
refreshScraperSource = true
}
if input.PluginPackageSources != nil {
c.Set(config.PluginPackageSources, input.PluginPackageSources)
refreshPluginSource = true
}
if err := c.Write(); err != nil {
return makeConfigGeneralResult(), err
}
@@ -369,12 +380,21 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
if refreshScraperCache {
manager.GetInstance().RefreshScraperCache()
}
if refreshPluginCache {
manager.GetInstance().RefreshPluginCache()
}
if refreshStreamManager {
manager.GetInstance().RefreshStreamManager()
}
if refreshBlobStorage {
manager.GetInstance().SetBlobStoreOptions()
}
if refreshScraperSource {
manager.GetInstance().RefreshScraperSourceManager()
}
if refreshPluginSource {
manager.GetInstance().RefreshPluginSourceManager()
}
return makeConfigGeneralResult(), nil
}
@@ -424,11 +444,6 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
c.Set(config.Language, *input.Language)
}
// deprecated field
if input.SlideshowDelay != nil {
c.Set(config.ImageLightboxSlideshowDelay, *input.SlideshowDelay)
}
if input.ImageLightbox != nil {
options := input.ImageLightbox
@@ -506,19 +521,10 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAIn
c.Set(config.DLNAVideoSortOrder, input.VideoSortOrder)
}
currentDLNAEnabled := c.GetDLNADefaultEnabled()
if input.Enabled != nil && *input.Enabled != currentDLNAEnabled {
refresh := false
if input.Enabled != nil {
c.Set(config.DLNADefaultEnabled, *input.Enabled)
// start/stop the DLNA service as needed
dlnaService := manager.GetInstance().DLNAService
if !*input.Enabled && dlnaService.IsRunning() {
dlnaService.Stop(nil)
} else if *input.Enabled && !dlnaService.IsRunning() {
if err := dlnaService.Start(nil); err != nil {
logger.Warnf("error starting DLNA service: %v", err)
}
}
refresh = true
}
if input.Interfaces != nil {
@@ -529,6 +535,10 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAIn
return makeConfigDLNAResult(), err
}
if refresh {
manager.GetInstance().RefreshDLNA()
}
return makeConfigDLNAResult(), nil
}
@@ -648,3 +658,14 @@ func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, v
return r.ConfigureUI(ctx, cfg)
}
func (r *mutationResolver) ConfigurePlugin(ctx context.Context, pluginID string, input map[string]interface{}) (map[string]interface{}, error) {
c := config.GetInstance()
c.SetPluginConfiguration(pluginID, input)
if err := c.Write(); err != nil {
return c.GetPluginConfiguration(pluginID), err
}
return c.GetPluginConfiguration(pluginID), nil
}

View File

@@ -17,7 +17,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
fileStore := r.repository.File
folderStore := r.repository.Folder
mover := file.NewMover(fileStore, folderStore)
mover.RegisterHooks(ctx, r.txnManager)
mover.RegisterHooks(ctx)
var (
folder *models.Folder
@@ -207,3 +207,68 @@ func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret b
return true, nil
}
func (r *mutationResolver) FileSetFingerprints(ctx context.Context, input FileSetFingerprintsInput) (bool, error) {
fileIDInt, err := strconv.Atoi(input.ID)
if err != nil {
return false, fmt.Errorf("converting id: %w", err)
}
fileID := models.FileID(fileIDInt)
// determine what we're doing
var (
fingerprints []models.Fingerprint
toDelete []string
)
for _, i := range input.Fingerprints {
if i.Type == models.FingerprintTypeMD5 || i.Type == models.FingerprintTypeOshash {
return false, fmt.Errorf("cannot modify %s fingerprint", i.Type)
}
if i.Value == nil {
toDelete = append(toDelete, i.Type)
} else {
// phashes need to be converted from string into uint64
var v interface{}
v = *i.Value
if i.Type == models.FingerprintTypePhash {
vInt, err := strconv.ParseUint(*i.Value, 16, 64)
if err != nil {
return false, fmt.Errorf("converting phash %s: %w", *i.Value, err)
}
v = vInt
}
fingerprints = append(fingerprints, models.Fingerprint{
Type: i.Type,
Fingerprint: v,
})
}
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.File
if len(fingerprints) > 0 {
if err := qb.ModifyFingerprints(ctx, fileID, fingerprints); err != nil {
return fmt.Errorf("modifying fingerprints: %w", err)
}
}
if len(toDelete) > 0 {
if err := qb.DestroyFingerprints(ctx, fileID, toDelete); err != nil {
return fmt.Errorf("destroying fingerprints: %w", err)
}
}
return nil
}); err != nil {
return false, err
}
return true, nil
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/utils"
)
@@ -43,8 +44,10 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
newGallery := models.NewGallery()
newGallery.Title = input.Title
newGallery.Code = translator.string(input.Code)
newGallery.Details = translator.string(input.Details)
newGallery.Rating = translator.ratingConversion(input.Rating, input.Rating100)
newGallery.Photographer = translator.string(input.Photographer)
newGallery.Rating = input.Rating100
var err error
@@ -88,7 +91,7 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, newGallery.ID, plugin.GalleryCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newGallery.ID, hook.GalleryCreatePost, input, nil)
return r.getGallery(ctx, newGallery.ID)
}
@@ -106,7 +109,7 @@ func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.Galle
}
// execute post hooks outside txn
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.GalleryUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, hook.GalleryUpdatePost, input, translator.getFields())
return r.getGallery(ctx, ret.ID)
}
@@ -140,7 +143,7 @@ func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.
inputMap: inputMaps[i],
}
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, hook.GalleryUpdatePost, input, translator.getFields())
gallery, err = r.getGallery(ctx, gallery.ID)
if err != nil {
@@ -182,8 +185,10 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
updatedGallery.Title = models.NewOptionalString(*input.Title)
}
updatedGallery.Code = translator.optionalString(input.Code, "code")
updatedGallery.Details = translator.optionalString(input.Details, "details")
updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedGallery.Photographer = translator.optionalString(input.Photographer, "photographer")
updatedGallery.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
updatedGallery.Date, err = translator.optionalDate(input.Date, "date")
@@ -257,8 +262,10 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
// Populate gallery from the input
updatedGallery := models.NewGalleryPartial()
updatedGallery.Code = translator.optionalString(input.Code, "code")
updatedGallery.Details = translator.optionalString(input.Details, "details")
updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedGallery.Photographer = translator.optionalString(input.Photographer, "photographer")
updatedGallery.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
updatedGallery.URLs = translator.optionalURLsBulk(input.Urls, input.URL)
@@ -307,7 +314,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
// execute post hooks outside of txn
var newRet []*models.Gallery
for _, gallery := range ret {
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, hook.GalleryUpdatePost, input, translator.getFields())
gallery, err := r.getGallery(ctx, gallery.ID)
if err != nil {
@@ -380,9 +387,9 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
}
}
// call post hook after performing the other actions
// call post hook after performing the other actionsa
for _, gallery := range galleries {
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryDestroyPost, plugin.GalleryDestroyInput{
r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, hook.GalleryDestroyPost, plugin.GalleryDestroyInput{
GalleryDestroyInput: input,
Checksum: gallery.PrimaryChecksum(),
Path: gallery.Path,
@@ -391,7 +398,7 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
// call image destroy post hook as well
for _, img := range imgsDestroyed {
r.hookExecutor.ExecutePostHooks(ctx, img.ID, plugin.ImageDestroyPost, plugin.ImageDestroyInput{
r.hookExecutor.ExecutePostHooks(ctx, img.ID, hook.ImageDestroyPost, plugin.ImageDestroyInput{
Checksum: img.Checksum,
Path: img.Path,
}, nil)
@@ -512,7 +519,7 @@ func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input Galle
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, newChapter.ID, plugin.GalleryChapterCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newChapter.ID, hook.GalleryChapterCreatePost, input, nil)
return r.getGalleryChapter(ctx, newChapter.ID)
}
@@ -578,7 +585,7 @@ func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input Galle
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, chapterID, plugin.GalleryChapterUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, chapterID, hook.GalleryChapterUpdatePost, input, translator.getFields())
return r.getGalleryChapter(ctx, chapterID)
}
@@ -606,7 +613,7 @@ func (r *mutationResolver) GalleryChapterDestroy(ctx context.Context, id string)
return false, err
}
r.hookExecutor.ExecutePostHooks(ctx, chapterID, plugin.GalleryChapterDestroyPost, id, nil)
r.hookExecutor.ExecutePostHooks(ctx, chapterID, hook.GalleryChapterDestroyPost, id, nil)
return true, nil
}

View File

@@ -10,7 +10,8 @@ import (
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/utils"
)
@@ -41,7 +42,7 @@ func (r *mutationResolver) ImageUpdate(ctx context.Context, input ImageUpdateInp
}
// execute post hooks outside txn
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.ImageUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, hook.ImageUpdatePost, input, translator.getFields())
return r.getImage(ctx, ret.ID)
}
@@ -75,7 +76,7 @@ func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdat
inputMap: inputMaps[i],
}
r.hookExecutor.ExecutePostHooks(ctx, image.ID, plugin.ImageUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, image.ID, hook.ImageUpdatePost, input, translator.getFields())
image, err = r.getImage(ctx, image.ID)
if err != nil {
@@ -107,7 +108,10 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
updatedImage := models.NewImagePartial()
updatedImage.Title = translator.optionalString(input.Title, "title")
updatedImage.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedImage.Code = translator.optionalString(input.Code, "code")
updatedImage.Details = translator.optionalString(input.Details, "details")
updatedImage.Photographer = translator.optionalString(input.Photographer, "photographer")
updatedImage.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
updatedImage.Date, err = translator.optionalDate(input.Date, "date")
@@ -203,7 +207,10 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
updatedImage := models.NewImagePartial()
updatedImage.Title = translator.optionalString(input.Title, "title")
updatedImage.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedImage.Code = translator.optionalString(input.Code, "code")
updatedImage.Details = translator.optionalString(input.Details, "details")
updatedImage.Photographer = translator.optionalString(input.Photographer, "photographer")
updatedImage.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")
updatedImage.Date, err = translator.optionalDate(input.Date, "date")
@@ -256,7 +263,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
}
thisUpdatedGalleryIDs := updatedImage.GalleryIDs.ImpactedIDs(i.GalleryIDs.List())
updatedGalleryIDs = intslice.IntAppendUniques(updatedGalleryIDs, thisUpdatedGalleryIDs)
updatedGalleryIDs = sliceutil.AppendUniques(updatedGalleryIDs, thisUpdatedGalleryIDs)
}
image, err := qb.UpdatePartial(ctx, imageID, updatedImage)
@@ -282,7 +289,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
// execute post hooks outside of txn
var newRet []*models.Image
for _, image := range ret {
r.hookExecutor.ExecutePostHooks(ctx, image.ID, plugin.ImageUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, image.ID, hook.ImageUpdatePost, input, translator.getFields())
image, err = r.getImage(ctx, image.ID)
if err != nil {
@@ -326,7 +333,7 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD
fileDeleter.Commit()
// call post hook after performing the other actions
r.hookExecutor.ExecutePostHooks(ctx, i.ID, plugin.ImageDestroyPost, plugin.ImageDestroyInput{
r.hookExecutor.ExecutePostHooks(ctx, i.ID, hook.ImageDestroyPost, plugin.ImageDestroyInput{
ImageDestroyInput: input,
Checksum: i.Checksum,
Path: i.Path,
@@ -377,7 +384,7 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image
for _, image := range images {
// call post hook after performing the other actions
r.hookExecutor.ExecutePostHooks(ctx, image.ID, plugin.ImageDestroyPost, plugin.ImagesDestroyInput{
r.hookExecutor.ExecutePostHooks(ctx, image.ID, hook.ImageDestroyPost, plugin.ImagesDestroyInput{
ImagesDestroyInput: input,
Checksum: image.Checksum,
Path: image.Path,

View File

@@ -3,8 +3,6 @@ package api
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"sync"
"time"
@@ -12,7 +10,7 @@ import (
"github.com/stashapp/stash/internal/identify"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/internal/manager/task"
"github.com/stashapp/stash/pkg/logger"
)
@@ -101,6 +99,21 @@ func (r *mutationResolver) MetadataClean(ctx context.Context, input manager.Clea
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) MetadataCleanGenerated(ctx context.Context, input task.CleanGeneratedOptions) (string, error) {
mgr := manager.GetInstance()
t := &task.CleanGeneratedJob{
Options: input,
Paths: mgr.Paths,
BlobsStorageType: mgr.Config.GetBlobsStorage(),
VideoFileNamingAlgorithm: mgr.Config.GetVideoFileNamingAlgorithm(),
Repository: mgr.Repository,
BlobCleaner: mgr.Repository.Blob,
}
jobID := mgr.JobManager.Add(ctx, "Cleaning generated files...", t)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) MigrateHashNaming(ctx context.Context) (string, error) {
jobID := manager.GetInstance().MigrateHash(ctx)
return strconv.Itoa(jobID), nil
@@ -110,31 +123,10 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
// if download is true, then backup to temporary file and return a link
download := input.Download != nil && *input.Download
mgr := manager.GetInstance()
database := mgr.Database
var backupPath string
if download {
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
}
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "backup*.sqlite")
if err != nil {
return nil, err
}
backupPath = f.Name()
f.Close()
} else {
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
if backupDirectoryPath != "" {
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
}
}
backupPath = database.DatabaseBackupPath(backupDirectoryPath)
}
err := database.Backup(backupPath)
backupPath, backupName, err := mgr.BackupDatabase(download)
if err != nil {
logger.Errorf("Error backing up database: %v", err)
return nil, err
}
@@ -147,8 +139,7 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
fn := filepath.Base(database.DatabaseBackupPath(""))
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
ret := baseURL + "/downloads/" + downloadHash + "/" + backupName
return &ret, nil
} else {
logger.Infof("Successfully backed up database to: %s", backupPath)
@@ -158,33 +149,11 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
}
func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input AnonymiseDatabaseInput) (*string, error) {
// if download is true, then backup to temporary file and return a link
// if download is true, then save to temporary file and return a link
download := input.Download != nil && *input.Download
mgr := manager.GetInstance()
database := mgr.Database
var outPath string
if download {
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
}
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "anonymous*.sqlite")
if err != nil {
return nil, err
}
outPath = f.Name()
f.Close()
} else {
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
if backupDirectoryPath != "" {
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
}
}
outPath = database.AnonymousDatabasePath(backupDirectoryPath)
}
err := database.Anonymise(outPath)
outPath, outName, err := mgr.AnonymiseDatabase(download)
if err != nil {
logger.Errorf("Error anonymising database: %v", err)
return nil, err
@@ -199,8 +168,7 @@ func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input Anonymis
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
fn := filepath.Base(database.DatabaseBackupPath(""))
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
ret := baseURL + "/downloads/" + downloadHash + "/" + outName
return &ret, nil
} else {
logger.Infof("Successfully anonymised database to: %s", outPath)

View File

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

View File

@@ -5,8 +5,9 @@ import (
"fmt"
"strconv"
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/utils"
)
@@ -34,7 +35,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
newMovie.Name = input.Name
newMovie.Aliases = translator.string(input.Aliases)
newMovie.Duration = input.Duration
newMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100)
newMovie.Rating = input.Rating100
newMovie.Director = translator.string(input.Director)
newMovie.Synopsis = translator.string(input.Synopsis)
newMovie.URL = translator.string(input.URL)
@@ -50,12 +51,6 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
return nil, fmt.Errorf("converting studio id: %w", err)
}
// HACK: if back image is being set, set the front image to the default.
// This is because we can't have a null front image with a non-null back image.
if input.FrontImage == nil && input.BackImage != nil {
input.FrontImage = &models.DefaultMovieImage
}
// Process the base 64 encoded image string
var frontimageData []byte
if input.FrontImage != nil {
@@ -74,6 +69,12 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
}
}
// HACK: if back image is being set, set the front image to the default.
// This is because we can't have a null front image with a non-null back image.
if len(frontimageData) == 0 && len(backimageData) != 0 {
frontimageData = static.ReadAll(static.DefaultMovieImage)
}
// Start the transaction and save the movie
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Movie
@@ -101,7 +102,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, plugin.MovieCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, hook.MovieCreatePost, input, nil)
return r.getMovie(ctx, newMovie.ID)
}
@@ -121,7 +122,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
updatedMovie.Name = translator.optionalString(input.Name, "name")
updatedMovie.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedMovie.Duration = translator.optionalInt(input.Duration, "duration")
updatedMovie.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedMovie.Director = translator.optionalString(input.Director, "director")
updatedMovie.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
updatedMovie.URL = translator.optionalString(input.URL, "url")
@@ -180,7 +181,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, plugin.MovieUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
return r.getMovie(ctx, movie.ID)
}
@@ -197,7 +198,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
// Populate movie from the input
updatedMovie := models.NewMoviePartial()
updatedMovie.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedMovie.Director = translator.optionalString(input.Director, "director")
updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
@@ -226,7 +227,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
var newRet []*models.Movie
for _, movie := range ret {
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, plugin.MovieUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
movie, err = r.getMovie(ctx, movie.ID)
if err != nil {
@@ -251,7 +252,7 @@ func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyI
return false, err
}
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.MovieDestroyPost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
return true, nil
}
@@ -276,7 +277,7 @@ func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string)
}
for _, id := range ids {
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.MovieDestroyPost, movieIDs, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, movieIDs, nil)
}
return true, nil

View File

@@ -0,0 +1,77 @@
package api
import (
"context"
"strconv"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/task"
"github.com/stashapp/stash/pkg/models"
)
func refreshPackageType(typeArg PackageType) {
mgr := manager.GetInstance()
if typeArg == PackageTypePlugin {
mgr.RefreshPluginCache()
} else if typeArg == PackageTypeScraper {
mgr.RefreshScraperCache()
}
}
func (r *mutationResolver) InstallPackages(ctx context.Context, typeArg PackageType, packages []*models.PackageSpecInput) (string, error) {
pm, err := getPackageManager(typeArg)
if err != nil {
return "", err
}
mgr := manager.GetInstance()
t := &task.InstallPackagesJob{
PackagesJob: task.PackagesJob{
PackageManager: pm,
OnComplete: func() { refreshPackageType(typeArg) },
},
Packages: packages,
}
jobID := mgr.JobManager.Add(ctx, "Installing packages...", t)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) UpdatePackages(ctx context.Context, typeArg PackageType, packages []*models.PackageSpecInput) (string, error) {
pm, err := getPackageManager(typeArg)
if err != nil {
return "", err
}
mgr := manager.GetInstance()
t := &task.UpdatePackagesJob{
PackagesJob: task.PackagesJob{
PackageManager: pm,
OnComplete: func() { refreshPackageType(typeArg) },
},
Packages: packages,
}
jobID := mgr.JobManager.Add(ctx, "Updating packages...", t)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) UninstallPackages(ctx context.Context, typeArg PackageType, packages []*models.PackageSpecInput) (string, error) {
pm, err := getPackageManager(typeArg)
if err != nil {
return "", err
}
mgr := manager.GetInstance()
t := &task.UninstallPackagesJob{
PackagesJob: task.PackagesJob{
PackageManager: pm,
OnComplete: func() { refreshPackageType(typeArg) },
},
Packages: packages,
}
jobID := mgr.JobManager.Add(ctx, "Updating packages...", t)
return strconv.Itoa(jobID), nil
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/performer"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/utils"
)
@@ -34,6 +34,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
newPerformer.Name = input.Name
newPerformer.Disambiguation = translator.string(input.Disambiguation)
newPerformer.Aliases = models.NewRelatedStrings(input.AliasList)
newPerformer.URL = translator.string(input.URL)
newPerformer.Gender = input.Gender
newPerformer.Ethnicity = translator.string(input.Ethnicity)
@@ -49,9 +50,10 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
newPerformer.Twitter = translator.string(input.Twitter)
newPerformer.Instagram = translator.string(input.Instagram)
newPerformer.Favorite = translator.bool(input.Favorite)
newPerformer.Rating = translator.ratingConversion(input.Rating, input.Rating100)
newPerformer.Rating = input.Rating100
newPerformer.Details = translator.string(input.Details)
newPerformer.HairColor = translator.string(input.HairColor)
newPerformer.Height = input.HeightCm
newPerformer.Weight = input.Weight
newPerformer.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds)
@@ -67,34 +69,11 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
return nil, fmt.Errorf("converting death date: %w", err)
}
// prefer height_cm over height
if input.HeightCm != nil {
newPerformer.Height = input.HeightCm
} else {
newPerformer.Height, err = translator.intPtrFromString(input.Height)
if err != nil {
return nil, fmt.Errorf("converting height: %w", err)
}
}
// prefer alias_list over aliases
if input.AliasList != nil {
newPerformer.Aliases = models.NewRelatedStrings(input.AliasList)
} else if input.Aliases != nil {
newPerformer.Aliases = models.NewRelatedStrings(stringslice.FromString(*input.Aliases, ","))
}
newPerformer.TagIDs, err = translator.relatedIds(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
if err := performer.ValidateDeathDate(nil, input.Birthdate, input.DeathDate); err != nil {
if err != nil {
return nil, err
}
}
// Process the base 64 encoded image string
var imageData []byte
if input.Image != nil {
@@ -108,6 +87,10 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
if err := performer.ValidateCreate(ctx, newPerformer, qb); err != nil {
return err
}
err = qb.Create(ctx, &newPerformer)
if err != nil {
return err
@@ -125,7 +108,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, newPerformer.ID, plugin.PerformerCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newPerformer.ID, hook.PerformerCreatePost, input, nil)
return r.getPerformer(ctx, newPerformer.ID)
}
@@ -159,7 +142,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedPerformer.Details = translator.optionalString(input.Details, "details")
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
@@ -178,22 +161,11 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
// 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, fmt.Errorf("converting height: %w", err)
}
}
// prefer alias_list over aliases
if translator.hasField("alias_list") {
updatedPerformer.Aliases = translator.updateStrings(input.AliasList, "alias_list")
} else if translator.hasField("aliases") {
var aliasList []string
if input.Aliases != nil {
aliasList = stringslice.FromString(*input.Aliases, ",")
}
updatedPerformer.Aliases = translator.updateStrings(aliasList, "aliases")
}
updatedPerformer.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
@@ -214,17 +186,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
// need to get existing performer
existing, err := qb.Find(ctx, performerID)
if err != nil {
return err
}
if existing == nil {
return fmt.Errorf("performer with id %d not found", performerID)
}
if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil {
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
return err
}
@@ -245,7 +207,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, performerID, plugin.PerformerUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, performerID, hook.PerformerUpdatePost, input, translator.getFields())
return r.getPerformer(ctx, performerID)
}
@@ -278,7 +240,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedPerformer.Details = translator.optionalString(input.Details, "details")
updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
@@ -296,22 +258,11 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
// 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, fmt.Errorf("converting height: %w", err)
}
}
// prefer alias_list over aliases
if translator.hasField("alias_list") {
updatedPerformer.Aliases = translator.updateStringsBulk(input.AliasList, "alias_list")
} else if translator.hasField("aliases") {
var aliasList []string
if input.Aliases != nil {
aliasList = stringslice.FromString(*input.Aliases, ",")
}
updatedPerformer.Aliases = translator.updateStrings(aliasList, "aliases")
}
updatedPerformer.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
@@ -326,18 +277,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
qb := r.repository.Performer
for _, performerID := range performerIDs {
// need to get existing performer
existing, err := qb.Find(ctx, performerID)
if err != nil {
return err
}
if existing == nil {
return fmt.Errorf("performer with id %d not found", performerID)
}
err = performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate)
if err != nil {
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
return err
}
@@ -357,7 +297,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
// execute post hooks outside of txn
var newRet []*models.Performer
for _, performer := range ret {
r.hookExecutor.ExecutePostHooks(ctx, performer.ID, plugin.PerformerUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, performer.ID, hook.PerformerUpdatePost, input, translator.getFields())
performer, err = r.getPerformer(ctx, performer.ID)
if err != nil {
@@ -382,7 +322,7 @@ func (r *mutationResolver) PerformerDestroy(ctx context.Context, input Performer
return false, err
}
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.PerformerDestroyPost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.PerformerDestroyPost, input, nil)
return true, nil
}
@@ -407,7 +347,7 @@ func (r *mutationResolver) PerformersDestroy(ctx context.Context, performerIDs [
}
for _, id := range ids {
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.PerformerDestroyPost, performerIDs, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.PerformerDestroyPost, performerIDs, nil)
}
return true, nil

View File

@@ -2,22 +2,111 @@ package api
import (
"context"
"strconv"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/sliceutil"
)
func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, taskName string, args []*plugin.PluginArgInput) (string, error) {
func toPluginArgs(args []*plugin.PluginArgInput) plugin.OperationInput {
ret := make(plugin.OperationInput)
for _, a := range args {
ret[a.Key] = toPluginArgValue(a.Value)
}
return ret
}
func toPluginArgValue(arg *plugin.PluginValueInput) interface{} {
if arg == nil {
return nil
}
switch {
case arg.Str != nil:
return *arg.Str
case arg.I != nil:
return *arg.I
case arg.B != nil:
return *arg.B
case arg.F != nil:
return *arg.F
case arg.O != nil:
return toPluginArgs(arg.O)
case arg.A != nil:
var ret []interface{}
for _, v := range arg.A {
ret = append(ret, toPluginArgValue(v))
}
return ret
}
return nil
}
func (r *mutationResolver) RunPluginTask(
ctx context.Context,
pluginID string,
taskName *string,
description *string,
args []*plugin.PluginArgInput,
argsMap map[string]interface{},
) (string, error) {
if argsMap == nil {
// convert args to map
// otherwise ignore args in favour of args map
argsMap = toPluginArgs(args)
}
m := manager.GetInstance()
m.RunPluginTask(ctx, pluginID, taskName, args)
return "todo", nil
jobID := m.RunPluginTask(ctx, pluginID, taskName, description, argsMap)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) RunPluginOperation(
ctx context.Context,
pluginID string,
args map[string]interface{},
) (interface{}, error) {
if args == nil {
args = make(map[string]interface{})
}
m := manager.GetInstance()
return m.PluginCache.RunPlugin(ctx, pluginID, args)
}
func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) {
err := manager.GetInstance().PluginCache.LoadPlugins()
if err != nil {
logger.Errorf("Error reading plugin configs: %v", err)
manager.GetInstance().RefreshPluginCache()
return true, nil
}
func (r *mutationResolver) SetPluginsEnabled(ctx context.Context, enabledMap map[string]bool) (bool, error) {
c := config.GetInstance()
existingDisabled := c.GetDisabledPlugins()
var newDisabled []string
// remove plugins that are no longer disabled
for _, disabledID := range existingDisabled {
if enabled, found := enabledMap[disabledID]; !enabled || !found {
newDisabled = append(newDisabled, disabledID)
}
}
// add plugins that are newly disabled
for pluginID, enabled := range enabledMap {
if !enabled {
newDisabled = sliceutil.AppendUnique(newDisabled, pluginID)
}
}
c.Set(config.DisabledPlugins, newDisabled)
if err := c.Write(); err != nil {
return false, err
}
return true, nil

View File

@@ -5,13 +5,16 @@ import (
"errors"
"fmt"
"strconv"
"time"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/utils"
)
@@ -45,7 +48,7 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr
newScene.Code = translator.string(input.Code)
newScene.Details = translator.string(input.Details)
newScene.Director = translator.string(input.Director)
newScene.Rating = translator.ratingConversion(input.Rating, input.Rating100)
newScene.Rating = input.Rating100
newScene.Organized = translator.bool(input.Organized)
newScene.StashIDs = models.NewRelatedStashIDs(input.StashIds)
@@ -114,7 +117,7 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, plugin.SceneUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, ret.ID, hook.SceneUpdatePost, input, translator.getFields())
return r.getScene(ctx, ret.ID)
}
@@ -148,7 +151,7 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
inputMap: inputMaps[i],
}
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, plugin.SceneUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, hook.SceneUpdatePost, input, translator.getFields())
scene, err = r.getScene(ctx, scene.ID)
if err != nil {
@@ -168,9 +171,16 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
updatedScene.Code = translator.optionalString(input.Code, "code")
updatedScene.Details = translator.optionalString(input.Details, "details")
updatedScene.Director = translator.optionalString(input.Director, "director")
updatedScene.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedScene.OCounter = translator.optionalInt(input.OCounter, "o_counter")
updatedScene.PlayCount = translator.optionalInt(input.PlayCount, "play_count")
updatedScene.Rating = translator.optionalInt(input.Rating100, "rating100")
if input.OCounter != nil {
logger.Warnf("o_counter is deprecated and no longer supported, use sceneIncrementO/sceneDecrementO instead")
}
if input.PlayCount != nil {
logger.Warnf("play_count is deprecated and no longer supported, use sceneIncrementPlayCount/sceneDecrementPlayCount instead")
}
updatedScene.PlayDuration = translator.optionalFloat64(input.PlayDuration, "play_duration")
updatedScene.Organized = translator.optionalBool(input.Organized, "organized")
updatedScene.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
@@ -321,7 +331,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
updatedScene.Code = translator.optionalString(input.Code, "code")
updatedScene.Details = translator.optionalString(input.Details, "details")
updatedScene.Director = translator.optionalString(input.Director, "director")
updatedScene.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedScene.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedScene.Organized = translator.optionalBool(input.Organized, "organized")
updatedScene.Date, err = translator.optionalDate(input.Date, "date")
@@ -376,7 +386,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
// execute post hooks outside of txn
var newRet []*models.Scene
for _, scene := range ret {
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, plugin.SceneUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, hook.SceneUpdatePost, input, translator.getFields())
scene, err = r.getScene(ctx, scene.ID)
if err != nil {
@@ -432,7 +442,7 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
fileDeleter.Commit()
// call post hook after performing the other actions
r.hookExecutor.ExecutePostHooks(ctx, s.ID, plugin.SceneDestroyPost, plugin.SceneDestroyInput{
r.hookExecutor.ExecutePostHooks(ctx, s.ID, hook.SceneDestroyPost, plugin.SceneDestroyInput{
SceneDestroyInput: input,
Checksum: s.Checksum,
OSHash: s.OSHash,
@@ -493,7 +503,7 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
for _, scene := range scenes {
// call post hook after performing the other actions
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, plugin.SceneDestroyPost, plugin.ScenesDestroyInput{
r.hookExecutor.ExecutePostHooks(ctx, scene.ID, hook.SceneDestroyPost, plugin.ScenesDestroyInput{
ScenesDestroyInput: input,
Checksum: scene.Checksum,
OSHash: scene.OSHash,
@@ -560,9 +570,20 @@ func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput
values = &v
}
mgr := manager.GetInstance()
fileDeleter := &scene.FileDeleter{
Deleter: file.NewDeleter(),
FileNamingAlgo: mgr.Config.GetVideoFileNamingAlgorithm(),
Paths: mgr.Paths,
}
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 {
if err := r.Resolver.sceneService.Merge(ctx, srcIDs, destID, fileDeleter, scene.MergeOptions{
ScenePartial: *values,
IncludePlayHistory: utils.IsTrue(input.PlayHistory),
IncludeOHistory: utils.IsTrue(input.OHistory),
}); err != nil {
return err
}
@@ -627,13 +648,13 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
// Save the marker tags
// If this tag is the primary tag, then let's not add it.
tagIDs = intslice.IntExclude(tagIDs, []int{newMarker.PrimaryTagID})
tagIDs = sliceutil.Exclude(tagIDs, []int{newMarker.PrimaryTagID})
return qb.UpdateTags(ctx, newMarker.ID, tagIDs)
}); err != nil {
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, newMarker.ID, plugin.SceneMarkerCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newMarker.ID, hook.SceneMarkerCreatePost, input, nil)
return r.getSceneMarker(ctx, newMarker.ID)
}
@@ -716,7 +737,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
if tagIdsIncluded {
// Save the marker tags
// If this tag is the primary tag, then let's not add it.
tagIDs = intslice.IntExclude(tagIDs, []int{newMarker.PrimaryTagID})
tagIDs = sliceutil.Exclude(tagIDs, []int{newMarker.PrimaryTagID})
if err := qb.UpdateTags(ctx, markerID, tagIDs); err != nil {
return err
}
@@ -731,7 +752,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
// perform the post-commit actions
fileDeleter.Commit()
r.hookExecutor.ExecutePostHooks(ctx, markerID, plugin.SceneMarkerUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerUpdatePost, input, translator.getFields())
return r.getSceneMarker(ctx, markerID)
}
@@ -781,7 +802,7 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
// perform the post-commit actions
fileDeleter.Commit()
r.hookExecutor.ExecutePostHooks(ctx, markerID, plugin.SceneMarkerDestroyPost, id, nil)
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerDestroyPost, id, nil)
return true, nil
}
@@ -804,16 +825,96 @@ func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, res
return ret, nil
}
// deprecated
func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id string) (ret int, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return 0, fmt.Errorf("converting id: %w", err)
}
var updatedTimes []time.Time
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.IncrementWatchCount(ctx, sceneID)
updatedTimes, err = qb.AddViews(ctx, sceneID, nil)
return err
}); err != nil {
return 0, err
}
return len(updatedTimes), nil
}
func (r *mutationResolver) SceneAddPlay(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return nil, fmt.Errorf("converting id: %w", err)
}
var times []time.Time
// convert time to local time, so that sorting is consistent
for _, tt := range t {
times = append(times, tt.Local())
}
var updatedTimes []time.Time
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
updatedTimes, err = qb.AddViews(ctx, sceneID, times)
return err
}); err != nil {
return nil, err
}
return &HistoryMutationResult{
Count: len(updatedTimes),
History: sliceutil.ValuesToPtrs(updatedTimes),
}, nil
}
func (r *mutationResolver) SceneDeletePlay(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
var times []time.Time
for _, tt := range t {
times = append(times, *tt)
}
var updatedTimes []time.Time
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
updatedTimes, err = qb.DeleteViews(ctx, sceneID, times)
return err
}); err != nil {
return nil, err
}
return &HistoryMutationResult{
Count: len(updatedTimes),
History: sliceutil.ValuesToPtrs(updatedTimes),
}, nil
}
func (r *mutationResolver) SceneResetPlayCount(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.DeleteAllViews(ctx, sceneID)
return err
}); err != nil {
return 0, err
@@ -822,40 +923,46 @@ func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id strin
return ret, nil
}
// deprecated
func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (ret int, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return 0, fmt.Errorf("converting id: %w", err)
}
var updatedTimes []time.Time
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.IncrementOCounter(ctx, sceneID)
updatedTimes, err = qb.AddO(ctx, sceneID, nil)
return err
}); err != nil {
return 0, err
}
return ret, nil
return len(updatedTimes), nil
}
// deprecated
func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (ret int, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return 0, fmt.Errorf("converting id: %w", err)
}
var updatedTimes []time.Time
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.DecrementOCounter(ctx, sceneID)
updatedTimes, err = qb.DeleteO(ctx, sceneID, nil)
return err
}); err != nil {
return 0, err
}
return ret, nil
return len(updatedTimes), nil
}
func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int, err error) {
@@ -867,7 +974,7 @@ func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int,
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.ResetOCounter(ctx, sceneID)
ret, err = qb.ResetO(ctx, sceneID)
return err
}); err != nil {
return 0, err
@@ -876,6 +983,65 @@ func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int,
return ret, nil
}
func (r *mutationResolver) SceneAddO(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return nil, fmt.Errorf("converting id: %w", err)
}
var times []time.Time
// convert time to local time, so that sorting is consistent
for _, tt := range t {
times = append(times, tt.Local())
}
var updatedTimes []time.Time
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
updatedTimes, err = qb.AddO(ctx, sceneID, times)
return err
}); err != nil {
return nil, err
}
return &HistoryMutationResult{
Count: len(updatedTimes),
History: sliceutil.ValuesToPtrs(updatedTimes),
}, nil
}
func (r *mutationResolver) SceneDeleteO(ctx context.Context, id string, t []*time.Time) (*HistoryMutationResult, error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return nil, fmt.Errorf("converting id: %w", err)
}
var times []time.Time
for _, tt := range t {
times = append(times, *tt)
}
var updatedTimes []time.Time
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
updatedTimes, err = qb.DeleteO(ctx, sceneID, times)
return err
}); err != nil {
return nil, err
}
return &HistoryMutationResult{
Count: len(updatedTimes),
History: sliceutil.ValuesToPtrs(updatedTimes),
}, nil
}
func (r *mutationResolver) SceneGenerateScreenshot(ctx context.Context, id string, at *float64) (string, error) {
if at != nil {
manager.GetInstance().GenerateScreenshot(ctx, id, *at)

View File

@@ -7,11 +7,6 @@ import (
)
func (r *mutationResolver) ReloadScrapers(ctx context.Context) (bool, error) {
err := manager.GetInstance().ScraperCache.ReloadScrapers()
if err != nil {
return false, err
}
manager.GetInstance().RefreshScraperCache()
return true, nil
}

View File

@@ -11,15 +11,6 @@ import (
"github.com/stashapp/stash/pkg/scraper/stashbox"
)
func (r *Resolver) stashboxRepository() stashbox.Repository {
return stashbox.Repository{
Scene: r.repository.Scene,
Performer: r.repository.Performer,
Tag: r.repository.Tag,
Studio: r.repository.Studio,
}
}
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input StashBoxFingerprintSubmissionInput) (bool, error) {
boxes := config.GetInstance().GetStashBoxes()
@@ -27,7 +18,7 @@ func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input
return false, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
return client.SubmitStashBoxFingerprints(ctx, input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
}
@@ -49,7 +40,7 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
id, err := strconv.Atoi(input.ID)
if err != nil {
@@ -91,7 +82,7 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager, r.stashboxRepository())
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
id, err := strconv.Atoi(input.ID)
if err != nil {

View File

@@ -6,7 +6,7 @@ import (
"strconv"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/studio"
"github.com/stashapp/stash/pkg/utils"
@@ -34,7 +34,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
newStudio.Name = input.Name
newStudio.URL = translator.string(input.URL)
newStudio.Rating = translator.ratingConversion(input.Rating, input.Rating100)
newStudio.Rating = input.Rating100
newStudio.Details = translator.string(input.Details)
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
newStudio.Aliases = models.NewRelatedStrings(input.Aliases)
@@ -61,10 +61,8 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Studio
if len(input.Aliases) > 0 {
if err := studio.EnsureAliasesUnique(ctx, 0, input.Aliases, qb); err != nil {
return err
}
if err := studio.ValidateCreate(ctx, newStudio, qb); err != nil {
return err
}
err = qb.Create(ctx, &newStudio)
@@ -83,7 +81,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, newStudio.ID, plugin.StudioCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newStudio.ID, hook.StudioCreatePost, input, nil)
return r.getStudio(ctx, newStudio.ID)
}
@@ -104,7 +102,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
updatedStudio.Name = translator.optionalString(input.Name, "name")
updatedStudio.URL = translator.optionalString(input.URL, "url")
updatedStudio.Details = translator.optionalString(input.Details, "details")
updatedStudio.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedStudio.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases")
updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
@@ -149,7 +147,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, studioID, plugin.StudioUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, studioID, hook.StudioUpdatePost, input, translator.getFields())
return r.getStudio(ctx, studioID)
}
@@ -165,7 +163,7 @@ func (r *mutationResolver) StudioDestroy(ctx context.Context, input StudioDestro
return false, err
}
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.StudioDestroyPost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.StudioDestroyPost, input, nil)
return true, nil
}
@@ -190,7 +188,7 @@ func (r *mutationResolver) StudiosDestroy(ctx context.Context, studioIDs []strin
}
for _, id := range ids {
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.StudioDestroyPost, studioIDs, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.StudioDestroyPost, studioIDs, nil)
}
return true, nil

View File

@@ -7,7 +7,7 @@ import (
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/tag"
"github.com/stashapp/stash/pkg/utils"
@@ -119,7 +119,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, newTag.ID, plugin.TagCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newTag.ID, hook.TagCreatePost, input, nil)
return r.getTag(ctx, newTag.ID)
}
@@ -235,7 +235,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, t.ID, plugin.TagUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, t.ID, hook.TagUpdatePost, input, translator.getFields())
return r.getTag(ctx, t.ID)
}
@@ -251,7 +251,7 @@ func (r *mutationResolver) TagDestroy(ctx context.Context, input TagDestroyInput
return false, err
}
r.hookExecutor.ExecutePostHooks(ctx, tagID, plugin.TagDestroyPost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, tagID, hook.TagDestroyPost, input, nil)
return true, nil
}
@@ -276,7 +276,7 @@ func (r *mutationResolver) TagsDestroy(ctx context.Context, tagIDs []string) (bo
}
for _, id := range ids {
r.hookExecutor.ExecutePostHooks(ctx, id, plugin.TagDestroyPost, tagIDs, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.TagDestroyPost, tagIDs, nil)
}
return true, nil
@@ -340,7 +340,7 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput)
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, t.ID, plugin.TagMergePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, t.ID, hook.TagMergePost, input, nil)
return t, nil
}

View File

@@ -5,24 +5,18 @@ import (
"errors"
"testing"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// TODO - move this into a common area
func newResolver() *Resolver {
txnMgr := &mocks.TxnManager{}
func newResolver(db *mocks.Database) *Resolver {
return &Resolver{
txnManager: txnMgr,
repository: manager.Repository{
TxnManager: txnMgr,
Tag: &mocks.TagReaderWriter{},
},
repository: db.Repository(),
hookExecutor: &mockHookExecutor{},
}
}
@@ -41,13 +35,12 @@ var testCtx = context.Background()
type mockHookExecutor struct{}
func (*mockHookExecutor) ExecutePostHooks(ctx context.Context, id int, hookType plugin.HookTriggerEnum, input interface{}, inputFields []string) {
func (*mockHookExecutor) ExecutePostHooks(ctx context.Context, id int, hookType hook.TriggerEnum, input interface{}, inputFields []string) {
}
func TestTagCreate(t *testing.T) {
r := newResolver()
tagRW := r.repository.Tag.(*mocks.TagReaderWriter)
db := mocks.NewDatabase()
r := newResolver(db)
pp := 1
findFilter := &models.FindFilterType{
@@ -72,17 +65,17 @@ func TestTagCreate(t *testing.T) {
}
}
tagRW.On("Query", mock.Anything, tagFilterForName(existingTagName), findFilter).Return([]*models.Tag{
db.Tag.On("Query", mock.Anything, tagFilterForName(existingTagName), findFilter).Return([]*models.Tag{
{
ID: existingTagID,
Name: existingTagName,
},
}, 1, nil).Once()
tagRW.On("Query", mock.Anything, tagFilterForName(errTagName), findFilter).Return(nil, 0, nil).Once()
tagRW.On("Query", mock.Anything, tagFilterForAlias(errTagName), findFilter).Return(nil, 0, nil).Once()
db.Tag.On("Query", mock.Anything, tagFilterForName(errTagName), findFilter).Return(nil, 0, nil).Once()
db.Tag.On("Query", mock.Anything, tagFilterForAlias(errTagName), findFilter).Return(nil, 0, nil).Once()
expectedErr := errors.New("TagCreate error")
tagRW.On("Create", mock.Anything, mock.AnythingOfType("*models.Tag")).Return(expectedErr)
db.Tag.On("Create", mock.Anything, mock.AnythingOfType("*models.Tag")).Return(expectedErr)
// fails here because testCtx is empty
// TODO: Fix this
@@ -101,22 +94,22 @@ func TestTagCreate(t *testing.T) {
})
assert.Equal(t, expectedErr, err)
tagRW.AssertExpectations(t)
db.AssertExpectations(t)
r = newResolver()
tagRW = r.repository.Tag.(*mocks.TagReaderWriter)
db = mocks.NewDatabase()
r = newResolver(db)
tagRW.On("Query", mock.Anything, tagFilterForName(tagName), findFilter).Return(nil, 0, nil).Once()
tagRW.On("Query", mock.Anything, tagFilterForAlias(tagName), findFilter).Return(nil, 0, nil).Once()
db.Tag.On("Query", mock.Anything, tagFilterForName(tagName), findFilter).Return(nil, 0, nil).Once()
db.Tag.On("Query", mock.Anything, tagFilterForAlias(tagName), findFilter).Return(nil, 0, nil).Once()
newTag := &models.Tag{
ID: newTagID,
Name: tagName,
}
tagRW.On("Create", mock.Anything, mock.AnythingOfType("*models.Tag")).Run(func(args mock.Arguments) {
db.Tag.On("Create", mock.Anything, mock.AnythingOfType("*models.Tag")).Run(func(args mock.Arguments) {
arg := args.Get(1).(*models.Tag)
arg.ID = newTagID
}).Return(nil)
tagRW.On("Find", mock.Anything, newTagID).Return(newTag, nil)
db.Tag.On("Find", mock.Anything, newTagID).Return(newTag, nil)
tag, err := r.Mutation().TagCreate(testCtx, TagCreateInput{
Name: tagName,
@@ -124,4 +117,5 @@ func TestTagCreate(t *testing.T) {
assert.Nil(t, err)
assert.NotNil(t, tag)
db.AssertExpectations(t)
}

View File

@@ -50,7 +50,7 @@ func getDir(path string) string {
}
func getParent(path string) *string {
isRoot := path[len(path)-1:] == "/"
isRoot := path == "/"
if isRoot {
return nil
} else {
@@ -79,9 +79,6 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
customPerformerImageLocation := config.GetCustomPerformerImageLocation()
scraperUserAgent := config.GetScraperUserAgent()
scraperCDPPath := config.GetScraperCDPPath()
return &ConfigGeneralResult{
Stashes: config.GetStashPaths(),
DatabasePath: config.GetDatabasePath(),
@@ -90,6 +87,7 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
MetadataPath: config.GetMetadataPath(),
ConfigFilePath: config.GetConfigFile(),
ScrapersPath: config.GetScrapersPath(),
PluginsPath: config.GetPluginsPath(),
CachePath: config.GetCachePath(),
BlobsPath: config.GetBlobsPath(),
BlobsStorage: config.GetBlobsStorage(),
@@ -123,9 +121,6 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
Excludes: config.GetExcludes(),
ImageExcludes: config.GetImageExcludes(),
CustomPerformerImageLocation: &customPerformerImageLocation,
ScraperUserAgent: &scraperUserAgent,
ScraperCertCheck: config.GetScraperCertCheck(),
ScraperCDPPath: &scraperCDPPath,
StashBoxes: config.GetStashBoxes(),
PythonPath: config.GetPythonPath(),
TranscodeInputArgs: config.GetTranscodeInputArgs(),
@@ -133,6 +128,8 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
DrawFunscriptHeatmapRange: config.GetDrawFunscriptHeatmapRange(),
ScraperPackageSources: config.GetScraperPackageSources(),
PluginPackageSources: config.GetPluginPackageSources(),
}
}
@@ -161,7 +158,6 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
scriptOffset := config.GetFunscriptOffset()
useStashHostedFunscript := config.GetUseStashHostedFunscript()
imageLightboxOptions := config.GetImageLightboxOptions()
// FIXME - misnamed output field means we have redundant fields
disableDropdownCreate := config.GetDisableDropdownCreate()
return &ConfigInterfaceResult{
@@ -187,9 +183,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
ImageLightbox: &imageLightboxOptions,
// FIXME - see above
DisabledDropdownCreate: disableDropdownCreate,
DisableDropdownCreate: disableDropdownCreate,
DisableDropdownCreate: disableDropdownCreate,
HandyKey: &handyKey,
FunscriptOffset: &scriptOffset,
@@ -243,7 +237,9 @@ func makeConfigUIResult() map[string]interface{} {
}
func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input config.StashBoxInput) (*StashBoxValidationResult, error) {
client := stashbox.NewClient(models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}, r.txnManager, r.stashboxRepository())
box := models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}
client := stashbox.NewClient(box, r.stashboxRepository())
user, err := client.GetUser(ctx)
valid := user != nil && user.Me != nil

View File

@@ -5,6 +5,7 @@ import (
"strconv"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models.Gallery, err error) {
@@ -23,9 +24,24 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
return ret, nil
}
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *FindGalleriesResultType, err error) {
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType, ids []string) (ret *FindGalleriesResultType, err error) {
idInts, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
galleries, total, err := r.repository.Gallery.Query(ctx, galleryFilter, filter)
var galleries []*models.Gallery
var err error
var total int
if len(idInts) > 0 {
galleries, err = r.repository.Gallery.FindMany(ctx, idInts)
total = len(galleries)
} else {
galleries, total, err = r.repository.Gallery.Query(ctx, galleryFilter, filter)
}
if err != nil {
return err
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
@@ -46,28 +47,63 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
return image, nil
}
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *FindImagesResultType, err error) {
func (r *queryResolver) FindImages(
ctx context.Context,
imageFilter *models.ImageFilterType,
imageIds []int,
ids []string,
filter *models.FindFilterType,
) (ret *FindImagesResultType, err error) {
if len(ids) > 0 {
imageIds, err = stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Image
var images []*models.Image
fields := graphql.CollectAllFields(ctx)
result := &models.ImageQueryResult{}
result, err := qb.Query(ctx, models.ImageQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,
Count: stringslice.StrInclude(fields, "count"),
},
ImageFilter: imageFilter,
Megapixels: stringslice.StrInclude(fields, "megapixels"),
TotalSize: stringslice.StrInclude(fields, "filesize"),
})
if err != nil {
return err
}
if len(imageIds) > 0 {
images, err = r.repository.Image.FindMany(ctx, imageIds)
if err == nil {
result.Count = len(images)
for _, s := range images {
if err = s.LoadPrimaryFile(ctx, r.repository.File); err != nil {
break
}
images, err := result.Resolve(ctx)
if err != nil {
return err
f := s.Files.Primary()
if f == nil {
continue
}
imageFile, ok := f.(*models.ImageFile)
if !ok {
continue
}
result.Megapixels += float64(imageFile.Width*imageFile.Height) / float64(1000000)
result.TotalSize += float64(f.Base().Size)
}
}
} else {
result, err = qb.Query(ctx, models.ImageQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,
Count: sliceutil.Contains(fields, "count"),
},
ImageFilter: imageFilter,
Megapixels: sliceutil.Contains(fields, "megapixels"),
TotalSize: sliceutil.Contains(fields, "filesize"),
})
if err == nil {
images, err = result.Resolve(ctx)
}
}
ret = &FindImagesResultType{

View File

@@ -5,6 +5,7 @@ import (
"strconv"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Movie, err error) {
@@ -23,9 +24,24 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
return ret, nil
}
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *FindMoviesResultType, err error) {
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType, ids []string) (ret *FindMoviesResultType, err error) {
idInts, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
movies, total, err := r.repository.Movie.Query(ctx, movieFilter, filter)
var movies []*models.Movie
var err error
var total int
if len(idInts) > 0 {
movies, err = r.repository.Movie.FindMany(ctx, idInts)
total = len(movies)
} else {
movies, total, err = r.repository.Movie.Query(ctx, movieFilter, filter)
}
if err != nil {
return err
}

View File

@@ -5,6 +5,7 @@ import (
"strconv"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *models.Performer, err error) {
@@ -23,7 +24,14 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
return ret, nil
}
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType, performerIDs []int) (ret *FindPerformersResultType, err error) {
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType, performerIDs []int, ids []string) (ret *FindPerformersResultType, err error) {
if len(ids) > 0 {
performerIDs, err = stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var performers []*models.Performer
var err error

View File

@@ -8,6 +8,7 @@ import (
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
@@ -74,7 +75,20 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInpu
return scene, nil
}
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
func (r *queryResolver) FindScenes(
ctx context.Context,
sceneFilter *models.SceneFilterType,
sceneIDs []int,
ids []string,
filter *models.FindFilterType,
) (ret *FindScenesResultType, err error) {
if len(ids) > 0 {
sceneIDs, err = stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var scenes []*models.Scene
var err error
@@ -105,11 +119,11 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
result, err = r.repository.Scene.Query(ctx, models.SceneQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,
Count: stringslice.StrInclude(fields, "count"),
Count: sliceutil.Contains(fields, "count"),
},
SceneFilter: sceneFilter,
TotalDuration: stringslice.StrInclude(fields, "duration"),
TotalSize: stringslice.StrInclude(fields, "filesize"),
TotalDuration: sliceutil.Contains(fields, "duration"),
TotalSize: sliceutil.Contains(fields, "filesize"),
})
if err == nil {
scenes, err = result.Resolve(ctx)
@@ -160,11 +174,11 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
result, err := r.repository.Scene.Query(ctx, models.SceneQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: queryFilter,
Count: stringslice.StrInclude(fields, "count"),
Count: sliceutil.Contains(fields, "count"),
},
SceneFilter: sceneFilter,
TotalDuration: stringslice.StrInclude(fields, "duration"),
TotalSize: stringslice.StrInclude(fields, "filesize"),
TotalDuration: sliceutil.Contains(fields, "duration"),
TotalSize: sliceutil.Contains(fields, "filesize"),
})
if err != nil {
return err
@@ -191,16 +205,11 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
}
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config models.SceneParserInput) (ret *SceneParserResultType, err error) {
parser := scene.NewFilenameParser(filter, config)
repo := scene.NewFilenameParserRepository(r.repository)
parser := scene.NewFilenameParser(filter, config, repo)
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
result, count, err := parser.Parse(ctx, scene.FilenameParserRepository{
Scene: r.repository.Scene,
Performer: r.repository.Performer,
Studio: r.repository.Studio,
Movie: r.repository.Movie,
Tag: r.repository.Tag,
})
result, count, err := parser.Parse(ctx)
if err != nil {
return err

View File

@@ -5,6 +5,7 @@ import (
"strconv"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.Studio, err error) {
@@ -24,9 +25,23 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
return ret, nil
}
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (ret *FindStudiosResultType, err error) {
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType, ids []string) (ret *FindStudiosResultType, err error) {
idInts, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
studios, total, err := r.repository.Studio.Query(ctx, studioFilter, filter)
var studios []*models.Studio
var err error
var total int
if len(idInts) > 0 {
studios, err = r.repository.Studio.FindMany(ctx, idInts)
total = len(studios)
} else {
studios, total, err = r.repository.Studio.Query(ctx, studioFilter, filter)
}
if err != nil {
return err
}

View File

@@ -5,6 +5,7 @@ import (
"strconv"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag, err error) {
@@ -23,9 +24,24 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
return ret, nil
}
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType) (ret *FindTagsResultType, err error) {
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType, ids []string) (ret *FindTagsResultType, err error) {
idInts, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
tags, total, err := r.repository.Tag.Query(ctx, tagFilter, filter)
var tags []*models.Tag
var err error
var total int
if len(idInts) > 0 {
tags, err = r.repository.Tag.FindMany(ctx, idInts)
total = len(tags)
} else {
tags, total, err = r.repository.Tag.Query(ctx, tagFilter, filter)
}
if err != nil {
return err
}

View File

@@ -0,0 +1,212 @@
package api
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/pkg"
"github.com/stashapp/stash/pkg/sliceutil"
)
var ErrInvalidPackageType = errors.New("invalid package type")
func getPackageManager(typeArg PackageType) (*pkg.Manager, error) {
var pm *pkg.Manager
switch typeArg {
case PackageTypeScraper:
pm = manager.GetInstance().ScraperPackageManager
case PackageTypePlugin:
pm = manager.GetInstance().PluginPackageManager
default:
return nil, ErrInvalidPackageType
}
if pm == nil {
return nil, fmt.Errorf("%s package manager not initialized", typeArg)
}
return pm, nil
}
func manifestToPackage(p pkg.Manifest) *Package {
ret := &Package{
PackageID: p.ID,
Name: p.Name,
SourceURL: p.RepositoryURL,
}
if len(p.Version) > 0 {
ret.Version = &p.Version
}
if !p.Date.IsZero() {
ret.Date = &p.Date.Time
}
ret.Metadata = p.Metadata
if ret.Metadata == nil {
ret.Metadata = make(map[string]interface{})
}
return ret
}
func remotePackageToPackage(p pkg.RemotePackage, index pkg.RemotePackageIndex) *Package {
ret := &Package{
PackageID: p.ID,
Name: p.Name,
}
if len(p.Version) > 0 {
ret.Version = &p.Version
}
if !p.Date.IsZero() {
ret.Date = &p.Date.Time
}
ret.Metadata = p.Metadata
if ret.Metadata == nil {
ret.Metadata = make(map[string]interface{})
}
ret.SourceURL = p.Repository.Path()
for _, r := range p.Requires {
// required packages must come from the same source
spec := models.PackageSpecInput{
ID: r,
SourceURL: p.Repository.Path(),
}
req, found := index[spec]
if !found {
// shouldn't happen, but we'll ignore it
continue
}
ret.Requires = append(ret.Requires, remotePackageToPackage(req, index))
}
return ret
}
func sortedPackageSpecKeys[V any](m map[models.PackageSpecInput]V) []models.PackageSpecInput {
// sort keys
var keys []models.PackageSpecInput
for k := range m {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
a := keys[i]
b := keys[j]
aID := a.ID
bID := b.ID
if aID == bID {
return a.SourceURL < b.SourceURL
}
aIDL := strings.ToLower(aID)
bIDL := strings.ToLower(bID)
if aIDL == bIDL {
return aID < bID
}
return aIDL < bIDL
})
return keys
}
func (r *queryResolver) getInstalledPackagesWithUpgrades(ctx context.Context, pm *pkg.Manager) ([]*Package, error) {
// get all installed packages
installed, err := pm.ListInstalled(ctx)
if err != nil {
return nil, err
}
// get remotes for all installed packages
allRemoteList, err := pm.ListInstalledRemotes(ctx, installed)
if err != nil {
return nil, err
}
packageStatusIndex := pkg.MakePackageStatusIndex(installed, allRemoteList)
ret := make([]*Package, len(packageStatusIndex))
i := 0
for _, k := range sortedPackageSpecKeys(packageStatusIndex) {
v := packageStatusIndex[k]
p := manifestToPackage(*v.Local)
if v.Remote != nil {
pp := remotePackageToPackage(*v.Remote, allRemoteList)
p.SourcePackage = pp
}
ret[i] = p
i++
}
return ret, nil
}
func (r *queryResolver) InstalledPackages(ctx context.Context, typeArg PackageType) ([]*Package, error) {
pm, err := getPackageManager(typeArg)
if err != nil {
return nil, err
}
var ret []*Package
if sliceutil.Contains(graphql.CollectAllFields(ctx), "source_package") {
ret, err = r.getInstalledPackagesWithUpgrades(ctx, pm)
if err != nil {
return nil, err
}
} else {
installed, err := pm.ListInstalled(ctx)
if err != nil {
return nil, err
}
ret = make([]*Package, len(installed))
i := 0
for _, k := range sortedPackageSpecKeys(installed) {
ret[i] = manifestToPackage(installed[k])
i++
}
}
return ret, nil
}
func (r *queryResolver) AvailablePackages(ctx context.Context, typeArg PackageType, source string) ([]*Package, error) {
pm, err := getPackageManager(typeArg)
if err != nil {
return nil, err
}
available, err := pm.ListRemote(ctx, source)
if err != nil {
return nil, err
}
ret := make([]*Package, len(available))
i := 0
for _, k := range sortedPackageSpecKeys(available) {
p := available[k]
ret[i] = remotePackageToPackage(p, available)
i++
}
return ret, nil
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scraper"
"github.com/stashapp/stash/pkg/scraper/stashbox"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
@@ -21,70 +22,10 @@ func (r *queryResolver) ScrapeURL(ctx context.Context, url string, ty scraper.Sc
return r.scraperCache().ScrapeURL(ctx, url, ty)
}
// deprecated
func (r *queryResolver) ScrapeFreeonesPerformerList(ctx context.Context, query string) ([]string, error) {
content, err := r.scraperCache().ScrapeName(ctx, scraper.FreeonesScraperID, query, scraper.ScrapeContentTypePerformer)
if err != nil {
return nil, err
}
performers, err := marshalScrapedPerformers(content)
if err != nil {
return nil, err
}
var ret []string
for _, p := range performers {
if p.Name != nil {
ret = append(ret, *p.Name)
}
}
return ret, nil
}
func (r *queryResolver) ListScrapers(ctx context.Context, types []scraper.ScrapeContentType) ([]*scraper.Scraper, error) {
return r.scraperCache().ListScrapers(types), nil
}
func (r *queryResolver) ListPerformerScrapers(ctx context.Context) ([]*scraper.Scraper, error) {
return r.scraperCache().ListScrapers([]scraper.ScrapeContentType{scraper.ScrapeContentTypePerformer}), nil
}
func (r *queryResolver) ListSceneScrapers(ctx context.Context) ([]*scraper.Scraper, error) {
return r.scraperCache().ListScrapers([]scraper.ScrapeContentType{scraper.ScrapeContentTypeScene}), nil
}
func (r *queryResolver) ListGalleryScrapers(ctx context.Context) ([]*scraper.Scraper, error) {
return r.scraperCache().ListScrapers([]scraper.ScrapeContentType{scraper.ScrapeContentTypeGallery}), nil
}
func (r *queryResolver) ListMovieScrapers(ctx context.Context) ([]*scraper.Scraper, error) {
return r.scraperCache().ListScrapers([]scraper.ScrapeContentType{scraper.ScrapeContentTypeMovie}), nil
}
func (r *queryResolver) ScrapePerformerList(ctx context.Context, scraperID string, query string) ([]*models.ScrapedPerformer, error) {
if query == "" {
return nil, nil
}
content, err := r.scraperCache().ScrapeName(ctx, scraperID, query, scraper.ScrapeContentTypePerformer)
if err != nil {
return nil, err
}
return marshalScrapedPerformers(content)
}
func (r *queryResolver) ScrapePerformer(ctx context.Context, scraperID string, scrapedPerformer scraper.ScrapedPerformerInput) (*models.ScrapedPerformer, error) {
content, err := r.scraperCache().ScrapeFragment(ctx, scraperID, scraper.Input{Performer: &scrapedPerformer})
if err != nil {
return nil, err
}
return marshalScrapedPerformer(content)
}
func (r *queryResolver) ScrapePerformerURL(ctx context.Context, url string) (*models.ScrapedPerformer, error) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypePerformer)
if err != nil {
@@ -113,29 +54,6 @@ func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string,
return ret, nil
}
func (r *queryResolver) ScrapeScene(ctx context.Context, scraperID string, scene models.SceneUpdateInput) (*scraper.ScrapedScene, error) {
id, err := strconv.Atoi(scene.ID)
if err != nil {
return nil, fmt.Errorf("%w: scene.ID is not an integer: '%s'", ErrInput, scene.ID)
}
content, err := r.scraperCache().ScrapeID(ctx, scraperID, id, scraper.ScrapeContentTypeScene)
if err != nil {
return nil, err
}
ret, err := marshalScrapedScene(content)
if err != nil {
return nil, err
}
if ret != nil {
filterSceneTags([]*scraper.ScrapedScene{ret})
}
return ret, nil
}
// filterSceneTags removes tags matching excluded tag patterns from the provided scraped scenes
func filterSceneTags(scenes []*scraper.ScrapedScene) {
excludePatterns := manager.GetInstance().Config.GetScraperExcludeTagPatterns()
@@ -163,7 +81,7 @@ func filterSceneTags(scenes []*scraper.ScrapedScene) {
for _, reg := range excludeRegexps {
if reg.MatchString(strings.ToLower(t.Name)) {
ignore = true
ignoredTags = stringslice.StrAppendUnique(ignoredTags, t.Name)
ignoredTags = sliceutil.AppendUnique(ignoredTags, t.Name)
break
}
}
@@ -199,20 +117,6 @@ func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*scrape
return ret, nil
}
func (r *queryResolver) ScrapeGallery(ctx context.Context, scraperID string, gallery models.GalleryUpdateInput) (*scraper.ScrapedGallery, error) {
id, err := strconv.Atoi(gallery.ID)
if err != nil {
return nil, fmt.Errorf("%w: gallery id is not an integer: '%s'", ErrInput, gallery.ID)
}
content, err := r.scraperCache().ScrapeID(ctx, scraperID, id, scraper.ScrapeContentTypeGallery)
if err != nil {
return nil, err
}
return marshalScrapedGallery(content)
}
func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*scraper.ScrapedGallery, error) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeGallery)
if err != nil {
@@ -238,9 +142,11 @@ func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) {
return nil, fmt.Errorf("%w: invalid stash_box_index %d", ErrInput, index)
}
return stashbox.NewClient(*boxes[index], r.txnManager, r.stashboxRepository()), nil
return stashbox.NewClient(*boxes[index], r.stashboxRepository()), nil
}
// FIXME - in the following resolvers, we're processing the deprecated field and not processing the new endpoint input
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*scraper.ScrapedScene, error) {
var ret []*scraper.ScrapedScene
@@ -378,6 +284,7 @@ func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scrape
}
return nil, ErrNotImplemented
// FIXME - we're relying on a deprecated field and not processing the endpoint input
} else if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if err != nil {

15
internal/api/routes.go Normal file
View File

@@ -0,0 +1,15 @@
package api
import (
"net/http"
"github.com/stashapp/stash/pkg/txn"
)
type routes struct {
txnManager txn.Manager
}
func (rs routes) withReadTxn(r *http.Request, fn txn.TxnFunc) error {
return txn.WithReadTxn(r.Context(), rs.txnManager, fn)
}

View File

@@ -4,12 +4,16 @@ import (
"net/http"
"strings"
"github.com/go-chi/chi"
"github.com/stashapp/stash/internal/manager/config"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/pkg/utils"
)
type customRoutes struct {
servedFolders config.URLMap
servedFolders utils.URLMap
}
func getCustomRoutes(servedFolders utils.URLMap) chi.Router {
return customRoutes{servedFolders: servedFolders}.Routes()
}
func (rs customRoutes) Routes() chi.Router {

View File

@@ -4,7 +4,8 @@ import (
"context"
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/internal/manager"
)

View File

@@ -3,13 +3,13 @@ package api
import (
"context"
"errors"
"io"
"io/fs"
"net/http"
"os/exec"
"strconv"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/file"
@@ -17,7 +17,6 @@ import (
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
"github.com/stashapp/stash/pkg/utils"
)
@@ -27,7 +26,7 @@ type ImageFinder interface {
}
type imageRoutes struct {
txnManager txn.Manager
routes
imageFinder ImageFinder
fileGetter models.FileGetter
}
@@ -46,11 +45,10 @@ func (rs imageRoutes) Routes() chi.Router {
return r
}
// region Handlers
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
mgr := manager.GetInstance()
img := r.Context().Value(imageKey).(*models.Image)
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
filepath := mgr.Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
// if the thumbnail doesn't exist, encode on the fly
exists, _ := fsutil.FileExists(filepath)
@@ -65,13 +63,18 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
return
}
// use the image thumbnail generate wait group to limit the number of concurrent thumbnail generation tasks
wg := &mgr.ImageThumbnailGenerateWaitGroup
wg.Add()
defer wg.Done()
clipPreviewOptions := image.ClipPreviewOptions{
InputArgs: manager.GetInstance().Config.GetTranscodeInputArgs(),
OutputArgs: manager.GetInstance().Config.GetTranscodeOutputArgs(),
Preset: manager.GetInstance().Config.GetPreviewPreset().String(),
}
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMPEG, manager.GetInstance().FFProbe, clipPreviewOptions)
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMpeg, manager.GetInstance().FFProbe, clipPreviewOptions)
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
if err != nil {
// don't log for unsupported image format
@@ -119,8 +122,6 @@ func (rs imageRoutes) Image(w http.ResponseWriter, r *http.Request) {
}
func (rs imageRoutes) serveImage(w http.ResponseWriter, r *http.Request, i *models.Image, useDefault bool) {
const defaultImageImage = "image/image.svg"
if i.Files.Primary() != nil {
err := i.Files.Primary().Base().Serve(&file.OsFS{}, w, r)
if err == nil {
@@ -141,22 +142,18 @@ func (rs imageRoutes) serveImage(w http.ResponseWriter, r *http.Request, i *mode
return
}
// fall back to static image
f, _ := static.Image.Open(defaultImageImage)
defer f.Close()
image, _ := io.ReadAll(f)
// fallback to default image
image := static.ReadAll(static.DefaultImageImage)
utils.ServeImage(w, r, image)
}
// endregion
func (rs imageRoutes) ImageCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
imageIdentifierQueryParam := chi.URLParam(r, "imageId")
imageID, _ := strconv.Atoi(imageIdentifierQueryParam)
var image *models.Image
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = rs.withReadTxn(r, func(ctx context.Context) error {
qb := rs.imageFinder
if imageID == 0 {
images, _ := qb.FindByChecksum(ctx, imageIdentifierQueryParam)

View File

@@ -6,10 +6,11 @@ import (
"net/http"
"strconv"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
"github.com/stashapp/stash/pkg/utils"
)
@@ -20,7 +21,7 @@ type MovieFinder interface {
}
type movieRoutes struct {
txnManager txn.Manager
routes
movieFinder MovieFinder
}
@@ -41,7 +42,7 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
image, err = rs.movieFinder.GetFrontImage(ctx, movie.ID)
return err
@@ -54,8 +55,9 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
}
}
// fallback to default image
if len(image) == 0 {
image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
image = static.ReadAll(static.DefaultMovieImage)
}
utils.ServeImage(w, r, image)
@@ -66,7 +68,7 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
image, err = rs.movieFinder.GetBackImage(ctx, movie.ID)
return err
@@ -79,8 +81,9 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
}
}
// fallback to default image
if len(image) == 0 {
image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
image = static.ReadAll(static.DefaultMovieImage)
}
utils.ServeImage(w, r, image)
@@ -95,7 +98,7 @@ func (rs movieRoutes) MovieCtx(next http.Handler) http.Handler {
}
var movie *models.Movie
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = rs.withReadTxn(r, func(ctx context.Context) error {
movie, _ = rs.movieFinder.Find(ctx, movieID)
return nil
})

View File

@@ -6,11 +6,10 @@ import (
"net/http"
"strconv"
"github.com/go-chi/chi"
"github.com/stashapp/stash/internal/manager/config"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
"github.com/stashapp/stash/pkg/utils"
)
@@ -20,7 +19,7 @@ type PerformerFinder interface {
}
type performerRoutes struct {
txnManager txn.Manager
routes
performerFinder PerformerFinder
}
@@ -41,7 +40,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
var image []byte
if defaultParam != "true" {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
image, err = rs.performerFinder.GetImage(ctx, performer.ID)
return err
@@ -55,7 +54,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
}
if len(image) == 0 {
image, _ = getRandomPerformerImageUsingName(performer.Name, performer.Gender, config.GetInstance().GetCustomPerformerImageLocation())
image = getDefaultPerformerImage(performer.Name, performer.Gender)
}
utils.ServeImage(w, r, image)
@@ -70,7 +69,7 @@ func (rs performerRoutes) PerformerCtx(next http.Handler) http.Handler {
}
var performer *models.Performer
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = rs.withReadTxn(r, func(ctx context.Context) error {
var err error
performer, err = rs.performerFinder.Find(ctx, performerID)
return err

View File

@@ -0,0 +1,102 @@
package api
import (
"context"
"net/http"
"path/filepath"
"strings"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/pkg/plugin"
)
type pluginRoutes struct {
pluginCache *plugin.Cache
}
func (rs pluginRoutes) Routes() chi.Router {
r := chi.NewRouter()
r.Route("/{pluginId}", func(r chi.Router) {
r.Use(rs.PluginCtx)
r.Get("/assets/*", rs.Assets)
r.Get("/javascript", rs.Javascript)
r.Get("/css", rs.CSS)
})
return r
}
func (rs pluginRoutes) Assets(w http.ResponseWriter, r *http.Request) {
p := r.Context().Value(pluginKey).(*plugin.Plugin)
if !p.Enabled {
http.Error(w, "plugin disabled", http.StatusBadRequest)
return
}
prefix := "/plugin/" + chi.URLParam(r, "pluginId") + "/assets"
r.URL.Path = strings.Replace(r.URL.Path, prefix, "", 1)
// http.FileServer redirects to / if the path ends with index.html
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/index.html")
pluginDir := filepath.Dir(p.ConfigPath)
// map the path to the applicable filesystem location
var dir string
r.URL.Path, dir = p.UI.Assets.GetFilesystemLocation(r.URL.Path)
if dir == "" {
http.NotFound(w, r)
return
}
dir = filepath.Join(pluginDir, filepath.FromSlash(dir))
// ensure directory is still within the plugin directory
if !strings.HasPrefix(dir, pluginDir) {
http.NotFound(w, r)
return
}
http.FileServer(http.Dir(dir)).ServeHTTP(w, r)
}
func (rs pluginRoutes) Javascript(w http.ResponseWriter, r *http.Request) {
p := r.Context().Value(pluginKey).(*plugin.Plugin)
if !p.Enabled {
http.Error(w, "plugin disabled", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/javascript")
serveFiles(w, r, p.UI.Javascript)
}
func (rs pluginRoutes) CSS(w http.ResponseWriter, r *http.Request) {
p := r.Context().Value(pluginKey).(*plugin.Plugin)
if !p.Enabled {
http.Error(w, "plugin disabled", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/css")
serveFiles(w, r, p.UI.CSS)
}
func (rs pluginRoutes) PluginCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := rs.pluginCache.GetPlugin(chi.URLParam(r, "pluginId"))
if p == nil {
http.Error(w, http.StatusText(404), 404)
return
}
ctx := context.WithValue(r.Context(), pluginKey, p)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -8,7 +8,8 @@ import (
"strconv"
"strings"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/ffmpeg"
@@ -16,7 +17,6 @@ import (
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
"github.com/stashapp/stash/pkg/utils"
)
@@ -43,7 +43,7 @@ type CaptionFinder interface {
}
type sceneRoutes struct {
txnManager txn.Manager
routes
sceneFinder SceneFinder
fileGetter models.FileGetter
captionFinder CaptionFinder
@@ -89,8 +89,6 @@ func (rs sceneRoutes) Routes() chi.Router {
return r
}
// region Handlers
func (rs sceneRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
ss := manager.SceneServer{
@@ -270,13 +268,13 @@ func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) {
utils.ServeStaticFile(w, r, filepath)
}
func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.SceneMarker) (*string, error) {
func (rs sceneRoutes) getChapterVttTitle(r *http.Request, marker *models.SceneMarker) (*string, error) {
if marker.Title != "" {
return &marker.Title, nil
}
var title string
if err := txn.WithReadTxn(ctx, rs.txnManager, func(ctx context.Context) error {
if err := rs.withReadTxn(r, func(ctx context.Context) error {
qb := rs.tagFinder
primaryTag, err := qb.Find(ctx, marker.PrimaryTagID)
if err != nil {
@@ -305,7 +303,7 @@ func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.Sce
func (rs sceneRoutes) VttChapter(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
var sceneMarkers []*models.SceneMarker
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
sceneMarkers, err = rs.sceneMarkerFinder.FindBySceneID(ctx, scene.ID)
return err
@@ -325,7 +323,7 @@ func (rs sceneRoutes) VttChapter(w http.ResponseWriter, r *http.Request) {
time := utils.GetVTTTime(marker.Seconds)
vttLines = append(vttLines, time+" --> "+time)
vttTitle, err := rs.getChapterVttTitle(r.Context(), marker)
vttTitle, err := rs.getChapterVttTitle(r, marker)
if errors.Is(err, context.Canceled) {
return
}
@@ -404,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.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
primaryFile := s.Files.Primary()
if primaryFile == nil {
@@ -466,7 +464,7 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@@ -494,7 +492,7 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@@ -530,7 +528,7 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
sceneMarker, err = rs.sceneMarkerFinder.Find(ctx, sceneMarkerID)
return err
@@ -561,8 +559,6 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque
}
}
// endregion
func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sceneID, err := strconv.Atoi(chi.URLParam(r, "sceneId"))
@@ -572,7 +568,7 @@ func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler {
}
var scene *models.Scene
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = rs.withReadTxn(r, func(ctx context.Context) error {
qb := rs.sceneFinder
scene, _ = qb.Find(ctx, sceneID)

View File

@@ -3,15 +3,14 @@ package api
import (
"context"
"errors"
"io"
"net/http"
"strconv"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
"github.com/stashapp/stash/pkg/utils"
)
@@ -21,7 +20,7 @@ type StudioFinder interface {
}
type studioRoutes struct {
txnManager txn.Manager
routes
studioFinder StudioFinder
}
@@ -42,7 +41,7 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
var image []byte
if defaultParam != "true" {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
image, err = rs.studioFinder.GetImage(ctx, studio.ID)
return err
@@ -55,15 +54,9 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
}
}
// fallback to default image
if len(image) == 0 {
const defaultStudioImage = "studio/studio.svg"
// fall back to static image
f, _ := static.Studio.Open(defaultStudioImage)
defer f.Close()
stat, _ := f.Stat()
http.ServeContent(w, r, "studio.svg", stat.ModTime(), f.(io.ReadSeeker))
return
image = static.ReadAll(static.DefaultStudioImage)
}
utils.ServeImage(w, r, image)
@@ -78,7 +71,7 @@ func (rs studioRoutes) StudioCtx(next http.Handler) http.Handler {
}
var studio *models.Studio
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = rs.withReadTxn(r, func(ctx context.Context) error {
var err error
studio, err = rs.studioFinder.Find(ctx, studioID)
return err

View File

@@ -3,15 +3,14 @@ package api
import (
"context"
"errors"
"io"
"net/http"
"strconv"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
"github.com/stashapp/stash/pkg/utils"
)
@@ -21,8 +20,8 @@ type TagFinder interface {
}
type tagRoutes struct {
txnManager txn.Manager
tagFinder TagFinder
routes
tagFinder TagFinder
}
func (rs tagRoutes) Routes() chi.Router {
@@ -42,7 +41,7 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) {
var image []byte
if defaultParam != "true" {
readTxnErr := txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
image, err = rs.tagFinder.GetImage(ctx, tag.ID)
return err
@@ -55,15 +54,9 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) {
}
}
// fallback to default image
if len(image) == 0 {
const defaultTagImage = "tag/tag.svg"
// fall back to static image
f, _ := static.Tag.Open(defaultTagImage)
defer f.Close()
stat, _ := f.Stat()
http.ServeContent(w, r, "tag.svg", stat.ModTime(), f.(io.ReadSeeker))
return
image = static.ReadAll(static.DefaultTagImage)
}
utils.ServeImage(w, r, image)
@@ -78,7 +71,7 @@ func (rs tagRoutes) TagCtx(next http.Handler) http.Handler {
}
var tag *models.Tag
_ = txn.WithReadTxn(r.Context(), rs.txnManager, func(ctx context.Context) error {
_ = rs.withReadTxn(r, 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