Compare commits

..

284 Commits

Author SHA1 Message Date
DogmaDragon
0df4077ace Fix typo 2025-04-01 13:21:24 +03:00
DogmaDragon
b14b2796f9 Update scraper objects 2025-03-31 14:49:45 +03:00
bob123491234
4bfc93b7ae Add marker end seconds import/export (#5777)
* skip importing markers if scene is skipped
2025-03-28 16:50:26 +11:00
bob123491234
c0d5d1e5a7 Add tag count to studio sort whitelist (#5776) 2025-03-28 12:45:40 +11:00
WithoutPants
bac0b0a379 Refactor scene list to not use ItemList component (#5767)
* Add fields to useListSelect
* Add more utility hooks
* Remove context from FilteredListToolbar
* Refactor SceneList to not use ItemList
* Move common logic into useFilteredListHook
2025-03-28 11:59:05 +11:00
WithoutPants
d9b4e62420 Login page internationalisation (#5765)
* Load locale strings in login page
* Generate and use login locale strings
* Add makefile target
* Update workflow
* Update build dockerfiles
* Add missing default string
2025-03-27 11:56:43 +11:00
WithoutPants
c8d74f0bcf Add rate limit to stashbox connection (#5764)
* Add max requests per minute stashbox option
* Implement rate limiting
* Add requests per minute to stashbox config
* Add UI setting
2025-03-27 11:54:00 +11:00
DogmaDragon
18381664aa Update Configuration.md (#5770) 2025-03-27 11:50:14 +11:00
blaspheme-ship-it
e9a67eb51f Add IP address to login errors (#5760) 2025-03-25 13:15:10 +11:00
WithoutPants
2ec264ed62 Fix merge error 2025-03-25 11:19:14 +11:00
dependabot[bot]
e5446a2336 Bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 (#5754)
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 10:32:43 +11:00
WithoutPants
db7d45792e Refactor stashbox package (#5699)
* Move stashbox package under pkg
* Remove StashBox from method names
* Add fingerprint conversion methods to Fingerprint

Refactor Fingerprints methods

* Make FindSceneByFingerprints accept fingerprints not scene ids
* Refactor SubmitSceneDraft to not require readers
* Have SubmitFingerprints accept scenes

Remove SceneReader dependency

* Move ScrapedScene to models package
* Move ScrapedImage into models package
* Move ScrapedGallery into models package
* Move Scene relationship matching out of stashbox package

This is now expected to be done in the client code

* Remove TagFinder dependency from stashbox.Client
* Make stashbox scene find full hierarchy of studios
* Move studio resolution into separate method
* Move studio matching out of stashbox package

This is now client code responsibility

* Move performer matching out of FindPerformerByID and FindPerformerByName
* Refactor performer querying logic and remove unused stashbox models

Renames FindStashBoxPerformersByPerformerNames to QueryPerformers and accepts names instead of performer ids

* Refactor SubmitPerformerDraft to not load relationships

This will be the responsibility of the calling code

* Remove repository references
2025-03-25 10:30:51 +11:00
WithoutPants
5d3d02e1e7 Optimise card width calculation (#5713)
* Add hook for grid card width calculation
* Move card width calculation into grid instead of card

Now calculates once instead of per card

* Debounce resize observer
2025-03-25 10:28:57 +11:00
WithoutPants
2541e9d1eb Refactor login page to not include in history (#5747) 2025-03-25 10:26:31 +11:00
WithoutPants
cc6917f29d Update changelog for bugfix release 2025-03-20 09:13:19 +11:00
WithoutPants
9636ff7c16 Parse scene t value as number not int (#5744) 2025-03-20 08:29:44 +11:00
WithoutPants
81f642b8b8 Fix incorrect URL field in studio exclusions (#5743) 2025-03-20 08:29:32 +11:00
WithoutPants
6f848f7f1c Fix setFromSavedCriterion for TimestampCriterion (#5742) 2025-03-20 08:29:17 +11:00
WithoutPants
720bbcb5c0 Update changelog 2025-03-19 09:10:29 +11:00
WithoutPants
8ce31a2831 Update weblate translations (#5734)
* Translated using Weblate (Swedish)

Currently translated at 99.8% (1195 of 1197 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sv/

* Translated using Weblate (Dutch)

Currently translated at 70.0% (838 of 1197 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/nl/

* Translated using Weblate (Dutch)

Currently translated at 70.0% (838 of 1197 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/nl/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (1195 of 1197 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (French)

Currently translated at 99.9% (1197 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Ukrainian)

Currently translated at 87.8% (1052 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/uk/

* Translated using Weblate (French)

Currently translated at 100.0% (1198 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Japanese)

Currently translated at 83.6% (1002 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/ja/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1198 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 19.3% (232 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/nb_NO/

* Translated using Weblate (French)

Currently translated at 100.0% (1198 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Czech)

Currently translated at 100.0% (1198 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/cs/

* Translated using Weblate (Russian)

Currently translated at 96.4% (1155 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/ru/

* Translated using Weblate (Catalan)

Currently translated at 37.3% (447 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/ca/

* Translated using Weblate (Spanish)

Currently translated at 98.2% (1177 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/es/

* Translated using Weblate (Finnish)

Currently translated at 77.3% (927 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fi/

* Translated using Weblate (Romanian)

Currently translated at 33.8% (406 of 1198 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/ro/

---------

Co-authored-by: AlpacaSerious <alpacaserious@noreply.codeberg.org>
Co-authored-by: youri <youri@noreply.codeberg.org>
Co-authored-by: wql219 <wanqinglin219@hotmail.com>
Co-authored-by: doodoo <doodoo@noreply.codeberg.org>
Co-authored-by: jmels <jmels@noreply.codeberg.org>
Co-authored-by: spyder039 <spyder039@noreply.codeberg.org>
Co-authored-by: ahsibu <ahsibu@noreply.codeberg.org>
Co-authored-by: Larsluph <codeberg@larsluph.dev>
Co-authored-by: NymeriaCZ <nymeriacz@noreply.codeberg.org>
Co-authored-by: meeeh <meeeh@noreply.codeberg.org>
Co-authored-by: Adolfo Jayme Barrientos <fito@noreply.codeberg.org>
Co-authored-by: IiroS <iiros@noreply.codeberg.org>
Co-authored-by: MallV0 <mallv0@noreply.codeberg.org>
2025-03-19 08:52:49 +11:00
WithoutPants
7a4ff20d66 Remove duplicates from ScrapedTagsRow (#5733) 2025-03-19 08:04:39 +11:00
WithoutPants
daed09e487 Fix various migration issues (#5723)
* Indicate while backing up database
* Close migrate connection to db before optimising
* Don't vacuum post-migration

In most cases is probably not needed and can be an optonal user-initiated step

* Ensure connection close on NewMigrator error
* Perform post-migration using migrator connection

Flush WAL file at end of migration
2025-03-19 08:04:21 +11:00
WithoutPants
529e4f6514 Improve UI loadable components (#5732)
* Add TagLink and PerformerCard to loadableComponents
* Add coarse grain loadable components
2025-03-18 13:21:00 +11:00
javstash
6d451d52ea Add sort by scene code option (#5708) 2025-03-17 11:23:51 +11:00
CJ
4d61c88661 Patchable ExternalLinkButtons component (#5727)
* Patchable ExternalLinkButtons component
* added fontAwesomeBrands
* use ExternalLinkButtons on groups page
2025-03-17 11:20:39 +11:00
WithoutPants
bc923929bb Stash scraper scene query (#5722)
* Enable scene querying in stash scraper
* Update docs
2025-03-17 10:20:08 +11:00
feederbox826
193b175618 docker documentation update (#5721)
- use docker compose instead of deprecated docker-compose
- add note for package as docker-cli-compose
- add link for reverse proxy
- remove obselete version string

Co-authored-by: feederbox826 <feederbox826@users.noreply.github.com>
2025-03-14 16:21:38 +11:00
WithoutPants
913a58057a Fix custom field between filter tag 2025-03-12 23:41:03 +00:00
Rémi Marseault
a621514c71 Replace history when merging tag (#5712) 2025-03-13 10:05:40 +11:00
WithoutPants
c2bc31387c Fix marker validation message 2025-03-07 14:52:54 +11:00
WithoutPants
9b7e20351a Plugin api improvements (#5703)
* Add ReactSelect to PluginApi.libraries
* Make Performer tabs patchable
* Make PerformerCard patchable
* Use registration pattern for HoverPopover, TagLink and LoadingIndicator

Initialising the components map to include these was causing an initialisation error.

* Add showZero property to PopoverCountButton
* Make TagCard patchable
* Make ScenePage and ScenePlayer patchable
* Pass properties to container components
* Add example for scene tabs
* Make FrontPage patchable
* Add FrontPage example
2025-03-05 14:04:12 +11:00
dogwithakeyboard
df5566771a Performer select calculated ages (#5110)
* Change wording of performer age at production

The Performer card had "x years old in this scene", regardless of what sort of media it was attached to. I have made both strings "x [years old] at production instead.
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-03-05 13:30:59 +11:00
WithoutPants
cbcc1994e8 Weblate translation update (#5698)
* Translated using Weblate (French)

Currently translated at 100.0% (1193 of 1193 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (1193 of 1193 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sv/

* Translated using Weblate (Turkish)

Currently translated at 92.6% (1105 of 1193 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/tr/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1193 of 1193 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (German)

Currently translated at 100.0% (1193 of 1193 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (1193 of 1193 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/et/

* Translated using Weblate (English)

Currently translated at 100.0% (1194 of 1194 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/en/

* Translated using Weblate (English (United States))

Currently translated at 2.0% (24 of 1194 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/en_US/

* Translated using Weblate (Persian)

Currently translated at 0.3% (4 of 1194 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fa/

* Translated using Weblate (Finnish)

Currently translated at 77.3% (924 of 1194 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fi/

* Translated using Weblate (French)

Currently translated at 99.8% (1192 of 1194 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (1192 of 1194 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (French)

Currently translated at 100.0% (1194 of 1194 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1194 of 1194 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Turkish)

Currently translated at 92.3% (1103 of 1194 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/tr/

* Revert "Translated using Weblate (English)"

This reverts commit 037755e80d.

* Revert "Translated using Weblate (English (United States))"

This reverts commit c71d87c866.

* Revert "Translated using Weblate (Chinese (Simplified Han script))"

This reverts commit 95f9ba0490.

---------

Co-authored-by: doodoo <doodoo@users.noreply.translate.codeberg.org>
Co-authored-by: AlpacaSerious <alpacaserious@users.noreply.translate.codeberg.org>
Co-authored-by: slickdaddy <slickdaddy@users.noreply.translate.codeberg.org>
Co-authored-by: wql219 <wanqinglin219@hotmail.com>
Co-authored-by: upstairs <upstairs@users.noreply.translate.codeberg.org>
Co-authored-by: Zesty6249 <zesty6249@users.noreply.translate.codeberg.org>
Co-authored-by: scottjwalter <scottjwalter@users.noreply.translate.codeberg.org>
Co-authored-by: Ricky-Tigg <ricky-tigg@users.noreply.translate.codeberg.org>
2025-03-04 14:03:15 +11:00
WithoutPants
bfdc4bac59 Add changelog for 0.28 2025-03-04 12:30:09 +11:00
CJ
a3f8c36536 Add zoom slider to other grid views (#4674)
* bring zoom slider to other list views
* updated 0 index to scale more proportionally
2025-03-04 11:56:59 +11:00
WithoutPants
0f32311f6e Wrap overflowing setting values
Long strings were pushing the edit buttons out of view
2025-03-04 09:50:39 +11:00
WithoutPants
fdb2dd9a8b Use existing formats for saved filters (#5697)
* Use existing formats for saved filters
* Fix date criterion marshalling
2025-03-04 09:26:46 +11:00
WithoutPants
ea5073fef4 Fix panic when no performer filter passed to FindPerformers 2025-03-04 09:12:40 +11:00
WithoutPants
ce2d779dbc Add FileSize component and refactor file size rendering in various components (#5695) 2025-03-03 18:38:19 +11:00
DogmaDragon
a391fa4345 Fix code comment in config.go --skip-ci (#5691) 2025-03-01 16:40:25 +11:00
WithoutPants
23e36b12fe Clear markers on unmount (#5678) 2025-02-28 18:22:13 +11:00
WithoutPants
59014f14ca Revert "Add docker labels to have update tools be able to pull changelog/rele…" (#5688)
This reverts commit 661e9eba51.
2025-02-28 17:56:32 +11:00
dumdum7
bf3a0e7944 Handy integration improvements (#5576)
* Use playing event instead of play
* Remove unnecessary ensurePlaying() from timeupdate listener

Eliminates redundant API calls by only relying on playing and pause events. Handles edge cases for playback before script upload completion.

* Remove unnecessary video seeking event listener

We don't need it anymore, listening for playing and pause events is enough.

* Send second play event after a play event to adjust for video player issues
* Fix script being paused and played after 10 seconds because of activity tracker dependency change
2025-02-28 14:11:50 +11:00
echo6ix
5f595f8ca7 Update KeyboardShortcuts.md (#5615)
* Update KeyboardShortcuts.md

- Added table of shortcuts for Image page
- Reorganized a few tables to include sub-headings
- Renamed `Edit Scene tab [...]` heading to `Scene Edit tab [...]` for logical consistency with other headings
- Moved Scene page rating shortcuts from vestigial location in *Edit Scene* section to global Scene page section

* Update edit header to be consitant with the others

---------

Co-authored-by: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
2025-02-28 13:42:22 +11:00
fume8866
4d447c3340 update merged performer upon batch update (#5664)
* update merged performer upon batch update
* Handle aliases and name for merged performer
* Refactor merge performer code

Log when merging performers
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-02-27 14:23:48 +11:00
Gavin Mogan
661e9eba51 Add docker labels to have update tools be able to pull changelog/release notes (#4923)
* Add docker labels to have update tools be able to pull changelog/release notes

For example https://docs.renovatebot.com/modules/datasource/docker/

but other tools will pull those same labels

* Add stash version to docker push

---------

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-02-27 14:23:24 +11:00
bunkmate5127
b49157f968 Fix erroneous filesize units (#4266)
Units are all calculated in the base 2 variants (as they should be), but were all named, and carry the units for, the base 10 variants.
2025-02-27 14:05:32 +11:00
javstash
7f58309143 Preserve JAV title in Tagger (#5645)
* Preserve JAV title in Tagger
* Styling and documentation
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-02-27 14:02:17 +11:00
RampantDespair
4f45ea8e7d Added f4v to default video extensions (#5624) 2025-02-27 10:45:43 +11:00
WithoutPants
ccf79d077f Use tag exclusions when identifying scenes (#5686)
* Move tag exclusion code back into scraper package

Reverts #2391

* Rearrange stash box client code
* Filter excluded tags in stashbox queries

Re-application of fix for #2379
2025-02-27 09:07:02 +11:00
WithoutPants
f23450c380 Fix custom field numbers not filtering correctly (#5685) 2025-02-26 14:04:51 +11:00
skier233
f65976cf4d fix point markers ui bug (#5684) 2025-02-26 09:18:53 +11:00
WithoutPants
b8af147a8d Initialise UpdatedAt for stash ids (#5680)
* Initialise imported zero time to epoch time

Fixes null time error after importing stash id without updatedAt set

* Update unit tests
2025-02-26 08:03:35 +11:00
WithoutPants
1e05766571 Fix scraping multiple URLs (#5677)
* Hack fix for scraping URLs field
* Rewrite apply function using known value types
2025-02-26 08:03:08 +11:00
WithoutPants
587fd9e6b8 Fix image title not appearing in lightbox (#5675) 2025-02-24 17:55:51 +11:00
WeedLordVegeta420
e97f647a43 Add Image Scraping (#5562)
Co-authored-by: keenbed <155155956+keenbed@users.noreply.github.com>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-02-24 16:38:14 +11:00
WithoutPants
b6ace42973 Custom fields filter UI (#5632)
* Refactor criteria and criterion options
* Add custom fields filtering in UI
2025-02-24 14:32:53 +11:00
Ceri Loosley
46d424fbaf UI: Various pwa manifests fixes (#5669)
* UI: Manifest changes and new square SVG to be used by PWA's
* UI: Fix manifest to include smaller sizes
* Make a maskable icon with a background so it can be seen on most platforms
* UI: Anti-Flashbang

Make the background colour the same as the background as stash
2025-02-24 14:30:53 +11:00
skier233
d915787840 Fix markers ui bug (#5671)
* Move loadMarkers to separate callback
* Remove async from findColors
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-02-24 14:29:59 +11:00
DogmaDragon
57e044e689 fix: broken links on README.md 2025-02-19 22:57:35 +02:00
Maista
3f90e57861 Show scene cover image in player preview (#5666)
This was accidentally removed in #5633
2025-02-19 08:25:36 +11:00
WithoutPants
0296b63be5 Fix lint error 2025-02-18 18:18:48 +11:00
DogmaDragon
e041ad190f Fix variables for help links in Stash Setup Wizard (#5661)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-02-18 17:10:34 +11:00
skier233
3ea49c6c2e Add UI for Markers with end seconds on scene player. (#5633)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-02-18 17:10:15 +11:00
WithoutPants
c8032f04fa Don't set image.title to file basename in graphql (#5658)
* Don't set image title to filename in graphql
* Remove deprecated files field from image fragments
2025-02-18 16:32:33 +11:00
WithoutPants
50a900e83c Show correct image in lightbox (#5659) 2025-02-18 16:32:13 +11:00
InfiniteStash
638398808b Add death date fetching from stash-box (#5653) 2025-02-11 15:09:50 +11:00
stg-annon
d2daf6c69f Add Sort Name to Tags (#5531)
* override "name" sort with COALESCE
* tag sort_name frontend

adds `data-sort-name` attribute to tag links prioritizes sort_name value but will default to tag name if not present in the same way that COALESCE will prioritize the same values in the same way
* add sort_name filter, update locale per request

* Include sort name in anonymiser
* Add import/export support
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-02-11 08:17:21 +11:00
81hvac1k02
dd40c07a6d removed un-necesarry dependancy (#5641) 2025-02-06 13:35:08 +11:00
WithoutPants
d95e35783a Weblate translation update (#5636)
* Translated using Weblate (Swedish)

Currently translated at 100.0% (1175 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sv/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (1175 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/et/

* Added translation using Weblate (Slovak)

* Translated using Weblate (Slovak)

Currently translated at 1.9% (23 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sk/

* Added translation using Weblate (Vietnamese)

* Translated using Weblate (Vietnamese)

Currently translated at 9.4% (111 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/vi/

* Translated using Weblate (Czech)

Currently translated at 100.0% (1175 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/cs/

* Translated using Weblate (German)

Currently translated at 87.4% (1028 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (French)

Currently translated at 100.0% (1179 of 1179 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (1179 of 1179 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1181 of 1181 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (1181 of 1181 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (1181 of 1181 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/et/

* Translated using Weblate (French)

Currently translated at 100.0% (1181 of 1181 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (1181 of 1181 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (Czech)

Currently translated at 99.3% (1175 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/cs/

* Translated using Weblate (Czech)

Currently translated at 100.0% (1183 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/cs/

* Translated using Weblate (French)

Currently translated at 100.0% (1183 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Polish)

Currently translated at 83.8% (992 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/pl/

* Translated using Weblate (Vietnamese)

Currently translated at 14.1% (167 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/vi/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1183 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Turkish)

Currently translated at 72.6% (859 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/tr/

* Translated using Weblate (Turkish)

Currently translated at 82.3% (974 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/tr/

* Translated using Weblate (German)

Currently translated at 87.1% (1031 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (Dutch)

Currently translated at 69.7% (825 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/nl/

* Translated using Weblate (Turkish)

Currently translated at 85.4% (1011 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/tr/

* Translated using Weblate (Dutch)

Currently translated at 70.4% (834 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/nl/

* Translated using Weblate (Turkish)

Currently translated at 87.2% (1032 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/tr/

* Translated using Weblate (Turkish)

Currently translated at 87.2% (1032 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/tr/

* Translated using Weblate (Norwegian Nynorsk)

Currently translated at 14.7% (174 of 1183 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/nn/

* Translated using Weblate (French)

Currently translated at 100.0% (1184 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Korean)

Currently translated at 99.5% (1179 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/ko/

* Translated using Weblate (Turkish)

Currently translated at 91.0% (1078 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/tr/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1184 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (1184 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (Turkish)

Currently translated at 93.3% (1105 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/tr/

* Translated using Weblate (French)

Currently translated at 100.0% (1184 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Ukrainian)

Currently translated at 18.7% (222 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 29.3% (348 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/uk/

* Translated using Weblate (Ukrainian)

Currently translated at 60.3% (714 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/uk/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (1184 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sv/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1184 of 1184 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (German)

Currently translated at 87.5% (1037 of 1185 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (English (United States))

Currently translated at 1.3% (16 of 1185 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/en_US/

* Translated using Weblate (Spanish)

Currently translated at 97.6% (1157 of 1185 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/es/

* Translated using Weblate (French)

Currently translated at 100.0% (1192 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1192 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Czech)

Currently translated at 99.9% (1191 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/cs/

* Translated using Weblate (Ukrainian)

Currently translated at 88.2% (1052 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/uk/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (1192 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sv/

* Translated using Weblate (German)

Currently translated at 100.0% (1192 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (1192 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (Slovak)

Currently translated at 2.4% (29 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sk/

* Translated using Weblate (Japanese)

Currently translated at 83.8% (999 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/ja/

* Translated using Weblate (Latvian)

Currently translated at 9.4% (113 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/lv/

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 19.0% (227 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/nb_NO/

* Translated using Weblate (Turkish)

Currently translated at 92.7% (1105 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/tr/

* Translated using Weblate (Polish)

Currently translated at 83.8% (1000 of 1192 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/pl/

* Add instructions to merge codeberg translations

* Rename locale files to correct format

* Add new locales to dropdown list

* Fix error when selecting nn-NO locale

---------

Co-authored-by: AlpacaSerious <AlpacaSerious@users.noreply.translate.codeberg.org>
Co-authored-by: Zesty6249 <Zesty6249@users.noreply.translate.codeberg.org>
Co-authored-by: rodopd <rodopd@users.noreply.translate.codeberg.org>
Co-authored-by: namphongbody <namphongbody@users.noreply.translate.codeberg.org>
Co-authored-by: NymeriaCZ <NymeriaCZ@users.noreply.translate.codeberg.org>
Co-authored-by: upstairs <upstairs@users.noreply.translate.codeberg.org>
Co-authored-by: doodoo <doodoo@users.noreply.translate.codeberg.org>
Co-authored-by: TWNO1 <TWNO1@users.noreply.translate.codeberg.org>
Co-authored-by: wql219 <wanqinglin219@hotmail.com>
Co-authored-by: Larsluph <Larsluph@users.noreply.translate.codeberg.org>
Co-authored-by: danny60718 <danny60718@users.noreply.translate.codeberg.org>
Co-authored-by: k1ngt0ng <k1ngt0ng@users.noreply.translate.codeberg.org>
Co-authored-by: slickdaddy <slickdaddy@users.noreply.translate.codeberg.org>
Co-authored-by: Vistaus <Vistaus@users.noreply.translate.codeberg.org>
Co-authored-by: throbbing <throbbing@users.noreply.translate.codeberg.org>
Co-authored-by: yec <yec@users.noreply.translate.codeberg.org>
Co-authored-by: orders-pawl <orders-pawl@users.noreply.translate.codeberg.org>
Co-authored-by: Mila_42 <Mila_42@users.noreply.translate.codeberg.org>
Co-authored-by: murgleburgle <murgleburgle@users.noreply.translate.codeberg.org>
Co-authored-by: DJSweder <DJSweder@users.noreply.translate.codeberg.org>
Co-authored-by: debate <debate@users.noreply.translate.codeberg.org>
Co-authored-by: abev66 <abev66@users.noreply.translate.codeberg.org>
Co-authored-by: pipo <pipo@users.noreply.translate.codeberg.org>
Co-authored-by: ikayaki <ikayaki@users.noreply.translate.codeberg.org>
Co-authored-by: Marky05 <marky05@users.noreply.translate.codeberg.org>
Co-authored-by: lexiconi <lexiconi@users.noreply.translate.codeberg.org>
Co-authored-by: LauraS <lauras@users.noreply.translate.codeberg.org>
2025-01-30 16:38:44 +11:00
dependabot[bot]
3078cb39c1 Bump golang.org/x/net from 0.30.0 to 0.33.0 (#5634)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.30.0 to 0.33.0.
- [Commits](https://github.com/golang/net/compare/v0.30.0...v0.33.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>
2025-01-30 14:14:33 +11:00
dependabot[bot]
5a8725b233 Bump vite from 4.5.5 to 4.5.6 in /ui/v2.5 (#5621)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.5 to 4.5.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.6/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>
2025-01-30 13:58:32 +11:00
dumdum7
b0a10399d7 Automatically resync Handy (#5581)
* Resync Handy every hour
* Don't try to upload script after resync if Handy is disconnected
2025-01-30 13:54:46 +11:00
dumdum7
9f7d00d83f Don't set interactiveReady when initializing player (#5578) 2025-01-30 13:51:46 +11:00
damontecres
b30bd8d2fe Find scene markers by ID (#5567) 2025-01-30 13:41:09 +11:00
stg-annon
8bacaa17f4 Use Marker endSeconds value when generating previews (#5542)
* generate marker previews using endSeconds value
* Limit marker preview duration to 20 seconds max
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-01-30 13:40:08 +11:00
0x60B2
4d43763a39 feat: Add ETA for tasks (#5535)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2025-01-30 13:28:40 +11:00
bayured
44d764d832 Update PerformerModal.tsx to fix aliases exclusions (#5566) 2025-01-30 13:24:45 +11:00
dependabot[bot]
726296bb54 Bump nanoid from 3.3.6 to 3.3.8 in /ui/v2.5 (#5552)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.6 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.6...3.3.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-30 13:19:56 +11:00
DogmaDragon
4ed522c5f8 Correct syntax (#5586) 2025-01-29 14:55:59 +11:00
echo6ix
b7592374aa Update Tasks.md (#5603)
* Update Tasks.md

Denoted which task items are only accessible in advanced mode.

* Add note to transcodes

---------

Co-authored-by: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
2025-01-29 14:55:21 +11:00
DogmaDragon
077cd774f3 docs: clarify regex case sensitivity (#5607) 2025-01-29 14:54:59 +11:00
DogmaDragon
b5cb52bb5e docs: add note about lack of SOCKS5 support (#5630) 2025-01-29 14:53:44 +11:00
Ikko Eltociear Ashimine
0621d87133 docs: update ScraperDevelopment.md (#5529) 2024-12-04 15:57:57 +11:00
WithoutPants
cacfe5a268 Add PerformerSelect as loadable component (#5528)
* Add PerformerSelect to loadable components
* Add PerformerSelect to example plugin
2024-12-04 14:15:32 +11:00
WithoutPants
8c8be22fe4 Performer custom fields (#5487)
* Backend changes
* Show custom field values
* Add custom fields table input
* Add custom field filtering
* Add unit tests
* Include custom fields in import/export
* Anonymise performer custom fields
* Move json.Number handler functions to api
* Handle json.Number conversion in api
2024-12-03 13:49:55 +11:00
WithoutPants
a0e09bbe5c Fix UI plugin race conditions (#5523)
* useScript to return load state of scripts
* Wait for scripts to load before rendering

Also moves plugin code into plugins.tsx
2024-12-03 08:02:46 +11:00
WithoutPants
4be793d4b3 Fix scraped tags issues (#5522)
* Fix display of matched scraped tags
* Fix create new scraped tag not updating field correctly
2024-12-03 08:02:29 +11:00
WithoutPants
60bb6bf50b Hide legacy groups criterion option (#5521) 2024-12-03 08:02:13 +11:00
dogwithakeyboard
7f8349469a Scene Marker grid view (#5443)
* add bulk delete mutation
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-11-29 17:02:20 +11:00
dogwithakeyboard
6ad0951878 Scene Marker duration filter and sort (#5472)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-11-29 16:28:10 +11:00
dogwithakeyboard
e097f2b3f4 Tagger preview scrubber and thumbnail (#5507) 2024-11-28 09:31:37 +11:00
dependabot[bot]
3c81d3b154 Bump cross-spawn from 7.0.3 to 7.0.6 in /ui/v2.5 (#5486)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-28 09:28:16 +11:00
DogmaDragon
ef2231f97b Update tripwire link (#5493) 2024-11-22 08:28:09 +11:00
WithoutPants
f81202660c Validate tagger blacklist entries (#5497)
* Don't let invalid tagger regex crash UI
* Validate blacklist entries and show errors
2024-11-22 08:27:41 +11:00
WithoutPants
6c5bf5f052 Convert json numbers to numbers (#5496) 2024-11-22 08:27:23 +11:00
WithoutPants
5f690d96bd Fix stash scraper errors and add apikey field (#5474)
* Use hasura/go-graphql-client instead of shurcooL version
* Fix graphql query errors
* Support setting api key for stash server
2024-11-13 10:14:55 +11:00
WithoutPants
64fed3553a Lint fixes (#5476)
* Fix lint errors
* Bump lint action version
2024-11-13 09:47:29 +11:00
WithoutPants
a18c538c1f Maintain saved filters in full export/import (#5465)
* Remove ellipsis from full export button
2024-11-12 16:59:28 +11:00
dependabot[bot]
41d1b45fb9 Bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 (#5451)
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  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-11-08 14:54:22 +11:00
WithoutPants
602f95dd29 Fix video files with identical phashes being merged during scan (#5461)
* Change Fingerprints.Remove to return new instead of mutate current
* Match only by oshash and md5 when merging scenes during scan
2024-11-07 14:29:26 +11:00
WithoutPants
2a454e5a1e Fix scraped tag name being used in matched scraped tags field (#5462) 2024-11-07 14:29:13 +11:00
WithoutPants
a100f8ffc8 Refactor setup wizard to fix text input (#5459) 2024-11-07 13:16:28 +11:00
MinasukiHikimuna
527c282b92 Setting marker end time with clock button uses full precision (#5437) 2024-11-03 15:02:52 +11:00
WithoutPants
e8125d08db Sub second marker timestamp precision (#5431)
* Allow DurationInput to accept/format timestamps with milliseconds
* Get current frame at sub-second precision
2024-11-02 14:59:54 +11:00
MinasukiHikimuna
0d40056f8c Markers can have end time (#5311)
* Markers can have end time

Other metadata sources such as ThePornDB and timestamp.trade support end times for markers but Stash did not yet support saving those. This is a first step which only allows end time to be set either via API or via UI. Other aspects of Stash such as video player timeline are not yet updated to take end time into account.

- User can set end time when creating or editing markers in the UI or in the API.
- End time cannot be before start time. This is validated in the backend and for better UX also in the frontend.
- End time is shown in scene details view or markers wall view if present.
- GraphQL API does not require end_seconds.
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-11-02 11:55:48 +11:00
Ian McKenzie
180a0fa8dd Add updated_at field to stash_id's (#5259)
* Add updated_at field to stash_id's
* Only set updated at on stash ids when actually updating in identify

---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-10-31 15:56:16 +11:00
InfiniteStash
b1d5dc2a0e Fix scraping stash-box performers with null birthdates (#5428) 2024-10-31 11:55:58 +11:00
WithoutPants
89f539ee24 Upgrade gqlgenc and regenerate stash-box client (#5391)
* Upgrade gqlgenc and regenerate stash-box client
* Fix go version
* Don't generate resolvers
* Bump go version in compiler image. Bump freebsd version
2024-10-29 17:35:17 +11:00
WithoutPants
f949fab231 Move modifiers into selectable options (#5203) 2024-10-29 14:17:46 +11:00
WithoutPants
edb66bd4e4 Remove unnecessary scroll to top on mount for top level query pages (#5288) 2024-10-29 13:52:17 +11:00
randemgame
1b7e729750 Update Scenes' 'Updated At' Date on Heatmap Gen & Expand Error Log (#5401)
* Update Scenes' 'Updated At' Date on Heatmap Gen & Expand Error Log

The UpdatedAt field of a scene is now correctly updated after Generate Task is run and a heatmap linked to a scene.. I thought it made more sense here in the generate heatmap compared to scan.go, owing to funscript data not being tracked/stored in a typical sense with the scan message "updating metadata".
I used a simplified error messaging as I did not think it was critcal but I do not know if did not use the correct code structure
If updating the UpdatedAt field should be done there when the file is marked as interactive I can try and do that?
This would fix this long-standing issue https://github.com/stashapp/stash/issues/3738

The error message change is useful as I could not tell which scripts were causing errors before but now it is clear in the logs

* Use single transaction

---------

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-10-29 12:19:49 +11:00
CJ
7fb8f9172e Group details: Flippable images in expanded view. (#5367)
* flippable images in expanded view

* adjust table title width

* cleanup

* eliminate bounce and other improvements

* expand support to non full-width option
2024-10-29 11:40:46 +11:00
CJ
069a4b1f80 show/hide details via CSS rather than Javascript (#5396) 2024-10-29 11:35:58 +11:00
its-josh4
c6bcdd89be Use slices package from the stdlib when possible (#5360)
* Use slices from the stdlib when possible

* Add some unit tests

* More small tweaks + add benchmark func
2024-10-29 11:26:23 +11:00
dependabot[bot]
093de3bce2 Bump rollup from 3.29.4 to 3.29.5 in /ui/v2.5 (#5305)
Bumps [rollup](https://github.com/rollup/rollup) from 3.29.4 to 3.29.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.29.4...v3.29.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 11:11:30 +11:00
dependabot[bot]
8c5ebf3797 Bump dset from 3.1.2 to 3.1.4 in /ui/v2.5 (#5258)
Bumps [dset](https://github.com/lukeed/dset) from 3.1.2 to 3.1.4.
- [Release notes](https://github.com/lukeed/dset/releases)
- [Commits](https://github.com/lukeed/dset/compare/v3.1.2...v3.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 11:09:41 +11:00
dependabot[bot]
33e46bad64 Bump path-to-regexp from 1.8.0 to 1.9.0 in /ui/v2.5 (#5257)
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 11:06:58 +11:00
dependabot[bot]
eca41dc7b4 Bump vite from 4.5.3 to 4.5.5 in /ui/v2.5 (#5270)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.3 to 4.5.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.5/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-10-29 11:03:43 +11:00
feederbox826
33ca4f8887 remove bencoder.pyx, update vips (#5416)
* remove bencoder.pyx
* revert vips downgrade
---------
Co-authored-by: feederbox826 <feederbox826@users.noreply.github.com>
2024-10-29 10:14:52 +11:00
WithoutPants
76648fee66 Update changelog for patch release 2024-10-16 08:08:37 +11:00
WithoutPants
6d07ecf751 More scene player bug fixes (#5379)
* Don't play video when seeking non-started video
* Set initial time on load instead of play
* Continue playing from current position when switching sources on error
* Remove unnecessary ref
2024-10-15 16:03:56 +11:00
WithoutPants
5283eb8ce3 Fix duplicate items appearing in selected list (again) (#5377)
* Fix duplicate detection in useListSelect
* Prevent double invocation of select handler
2024-10-15 14:29:29 +11:00
Arshad
32c48443b5 adding exists check before dropping constraints (#5363)
Co-authored-by: Arshad Khan <arshad@Arshads-MacBook-Air-2.local>
2024-10-15 13:10:47 +11:00
WithoutPants
ad00bee393 Update changelog for patch 2024-10-10 11:53:22 +11:00
WithoutPants
a54996d8a2 Weblate translation update (#5359)
* Translated using Weblate (Korean)

Currently translated at 100.0% (1174 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/ko/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1174 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1174 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Hungarian)

Currently translated at 41.9% (493 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/hu/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1175 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (French)

Currently translated at 100.0% (1175 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Hungarian)

Currently translated at 43.7% (514 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/hu/

* Translated using Weblate (Czech)

Currently translated at 100.0% (1175 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/cs/

* Translated using Weblate (German)

Currently translated at 87.2% (1025 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 18.7% (220 of 1175 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/nb_NO/

---------

Co-authored-by: yec <yec@users.noreply.translate.codeberg.org>
Co-authored-by: wql219 <wanqinglin219@hotmail.com>
Co-authored-by: tozoktala <tozoktala@users.noreply.translate.codeberg.org>
Co-authored-by: zdh <zdh@users.noreply.translate.codeberg.org>
Co-authored-by: doodoo <doodoo@users.noreply.translate.codeberg.org>
Co-authored-by: NymeriaCZ <NymeriaCZ@users.noreply.translate.codeberg.org>
Co-authored-by: augeee <augeee@users.noreply.translate.codeberg.org>
Co-authored-by: noTranslator <noTranslator@users.noreply.translate.codeberg.org>
2024-10-10 11:52:11 +11:00
WithoutPants
b6db4c31ca Prevent duplicate entries in selection list (#5358) 2024-10-10 10:54:39 +11:00
WithoutPants
f82e24762b Add blobs location to env binds (#5350) 2024-10-07 11:20:29 +11:00
WithoutPants
35b74be585 Restore persistence in selection when paging (#5349) 2024-10-07 11:20:20 +11:00
WithoutPants
7199d2b5ac Handle legacy scene movies criterion in saved filters (#5348) 2024-10-07 10:19:36 +11:00
WithoutPants
4697271294 Scene player fixes and improvements (#5340)
* Don't log context canceled error during live transcode
* Pause live transcode if still scrubbing
* Debounce loading live transcode source to avoid multiple ffmpeg instances
* Don't start from start or resume time if seeking before playing
* Play video when seeked before playing
2024-10-07 09:00:49 +11:00
forcalas
3e4515e62a Add Open Container Image annotations as labels to Docker image (#5323) 2024-10-03 12:51:07 +10:00
WithoutPants
58c58beb4a Fix match studio/performer links in performer view (#5337) 2024-10-03 12:50:46 +10:00
damontecres
f05518860f Add include_sub_groups message ID (#5318) 2024-10-03 12:31:43 +10:00
WithoutPants
9b567fa6f4 Exclude null values from image studio id index (#5335) 2024-10-03 11:53:29 +10:00
WithoutPants
c92de09ece Fix rating display in filter tags (#5334) 2024-10-03 11:26:18 +10:00
huochexizhan
9765b6d50e fix: fix slice init length (#5327) 2024-10-02 16:23:10 +10:00
WithoutPants
c6c3754f02 Fix panic when deleting image with no files (#5328) 2024-10-02 12:19:13 +10:00
WithoutPants
76a5b2a06a Fix UI error when image has no files (#5325) 2024-10-02 09:58:48 +10:00
WithoutPants
93a2ee1277 Fix page > total redirecting to first page instead of last (#5321) 2024-09-30 17:13:57 +10:00
WithoutPants
be6431ac13 Fix parent/child tag sort order (#5320) 2024-09-30 17:13:45 +10:00
WithoutPants
4dd8dd948e Refresh URL if random seed set (#5319) 2024-09-30 16:44:59 +10:00
Stephan
e253ba71f8 Update README.md (#5309)
Indicate dropped support for old Windows versions and indicate minimal Windows version required from 0.27.0
2024-09-30 15:39:57 +10:00
CJ
30fc2d1209 fix link menu issues (#5310) 2024-09-30 15:12:22 +10:00
WithoutPants
cef5b46f93 Fix merge dialog select boxes display issue (#5299) 2024-09-25 14:04:42 +10:00
WithoutPants
c45ae068fc Weblate translation update (#5289)
* Translated using Weblate (Korean)

Currently translated at 100.0% (1174 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/ko/

* Translated using Weblate (German)

Currently translated at 87.2% (1024 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (1174 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/et/

---------

Co-authored-by: yec <yec@users.noreply.translate.codeberg.org>
Co-authored-by: TheJojonas <TheJojonas@users.noreply.translate.codeberg.org>
Co-authored-by: Zesty6249 <Zesty6249@users.noreply.translate.codeberg.org>
2024-09-23 11:56:31 +10:00
WithoutPants
a20fbe33c0 Fix tag select breaking layout 2024-09-23 10:28:43 +10:00
WithoutPants
82f4a8f671 Fix number field render 2024-09-23 10:14:29 +10:00
WithoutPants
33050f700e Prevent mouse wheel window scrolling on other number fields (#5283) 2024-09-22 15:24:54 +10:00
WithoutPants
4ad0241c53 Update changelog 2024-09-22 14:04:26 +10:00
WithoutPants
7e8c764dc7 Fix migrations not using tx (#5282) 2024-09-22 14:03:54 +10:00
dogwithakeyboard
fd9e4b3ec2 add table alias to group scene sort (#5279) 2024-09-22 13:20:33 +10:00
DogmaDragon
3abdcbee6f Replace movie with group (#5280) 2024-09-22 13:18:12 +10:00
WithoutPants
476688c84d Database connection pool refactor (#5274)
* Move optimise out of RunAllMigrations
* Separate read and write database connections
* Enforce readonly connection constraint
* Fix migrations not using tx
* #5155 - allow setting cache size from environment
* Document new environment variable
2024-09-20 12:56:26 +10:00
WithoutPants
7152be6086 Weblate translation update (#5271)
* Translated using Weblate (French)

Currently translated at 100.0% (1157 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1157 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (1157 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sv/

* Translated using Weblate (German)

Currently translated at 87.8% (1017 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1157 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 89.3% (1034 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 89.3% (1034 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1157 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 95.5% (1106 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (1157 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (French)

Currently translated at 100.0% (1157 of 1157 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1161 of 1161 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (1161 of 1161 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (French)

Currently translated at 100.0% (1172 of 1172 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (German)

Currently translated at 87.2% (1023 of 1172 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (1172 of 1172 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sv/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1172 of 1172 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (1172 of 1172 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (French)

Currently translated at 100.0% (1174 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1174 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (1174 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hant/

* Translated using Weblate (German)

Currently translated at 87.2% (1024 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (Spanish)

Currently translated at 98.5% (1157 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/es/

* Translated using Weblate (Japanese)

Currently translated at 84.3% (990 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/ja/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (1174 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sv/

* Translated using Weblate (Ukrainian)

Currently translated at 16.6% (195 of 1174 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/uk/

---------

Co-authored-by: Larsluph <Larsluph@users.noreply.translate.codeberg.org>
Co-authored-by: wql219 <wql219@users.noreply.translate.codeberg.org>
Co-authored-by: AlpacaSerious <AlpacaSerious@users.noreply.translate.codeberg.org>
Co-authored-by: mrtuxa <mrtuxa@users.noreply.translate.codeberg.org>
Co-authored-by: Lytel <Lytel@users.noreply.translate.codeberg.org>
Co-authored-by: doodoo <doodoo@users.noreply.translate.codeberg.org>
Co-authored-by: Lucqqq <Lucqqq@users.noreply.translate.codeberg.org>
Co-authored-by: wql219 <wanqinglin219@hotmail.com>
Co-authored-by: lurch <lurch@users.noreply.translate.codeberg.org>
Co-authored-by: miamoreau <miamoreau@users.noreply.translate.codeberg.org>
Co-authored-by: ikayaki <ikayaki@users.noreply.translate.codeberg.org>
Co-authored-by: jc_back <jc_back@users.noreply.translate.codeberg.org>
2024-09-18 14:38:56 +10:00
WithoutPants
e4ef14e830 Fix preview scrubber touch issues (#5267) 2024-09-16 16:30:16 +10:00
Ian McKenzie
f543046349 Update upload-artifact action to pass build (#5260) 2024-09-16 10:12:09 +10:00
WithoutPants
c9f76a01c5 Add UI option for rescan scan option (#5254) 2024-09-11 17:29:27 +10:00
WithoutPants
5c4bf4ecdf Add portals for selects in dialogs (#5253) 2024-09-11 16:12:18 +10:00
WithoutPants
17be7e97d3 Emit error in SafeMove if remove from source fails (#5251) 2024-09-11 14:29:16 +10:00
WithoutPants
71e39e5cb8 Default database backup to same directory as database (#5250) 2024-09-11 14:02:00 +10:00
WithoutPants
a17199ba21 Handle symlink zip files (#5249) 2024-09-11 13:58:02 +10:00
WithoutPants
d1c207e40b Rename movies to groups in menu items in 65 post-migration (#5247)
* Only backup config file if needed in 58 migration
* Change movies to groups in menu items in 65 post-migration
2024-09-11 13:39:46 +10:00
GlitchGal
129dd0ffcc ImageDetailPanel Patch Component (#5245) 2024-09-11 11:56:17 +10:00
WithoutPants
a3838734c5 Set max-height for all modals (#5242) 2024-09-11 11:55:06 +10:00
WithoutPants
b897de3e5e Fix hover scrubber error in Firefox (#5243) 2024-09-11 11:50:40 +10:00
WithoutPants
5407596e0d Anonymise missing fields (#5244)
* Anonymise missing fields:
- galleries.photographer
- performers.disambiguation
- gallery_urls

* Anonymise captions and saved filters
2024-09-11 11:50:27 +10:00
WithoutPants
f7a164ffe5 Fix performer disambiguation styling in select (#5246) 2024-09-11 11:46:41 +10:00
Gykes
653cd16eb2 Updating Reload Scrapers formatting (#5235)
Per convo with people on Discord. I have updated the Reload Scrapers UI. It now adds a button if the filter box appears and then the button extends and takes up the whole space if the filter box does not exist.
---------
Co-authored-by: CJ <tedabed@gmail.com>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-09-10 14:54:15 +10:00
Gykes
a2153ced52 Bottom Page-Count button causes scenes page to reset to top (#5241)
I think was was happening is the browser was trying to do too much at once (Rendering the popup and focusing the input simultaneously). I believe it was triggering a reflow and setting the site back to default aka: back to the top.

I set the timeout to 0 which  moves the execution to the next loop event. It gives the browser time to do one and then the other, not both at the same time.

I moved `onKeyPress` to `onKeyDown` due to the former being depreciated.
2024-09-10 14:52:12 +10:00
Gykes
a44993bbf4 Fix source-selector being blocked on mobile (#5229)
Small CSS change to allow the `source-selector` to be brought to the front of the controls to allow people to select which source they would like.
2024-09-10 14:43:09 +10:00
DogmaDragon
ba83da1983 Add note about saved filter sorting (#5234) 2024-09-08 01:20:44 +03:00
WithoutPants
0a98296642 Fix scroll to top behaviour (#5228) 2024-09-06 13:53:23 +10:00
WithoutPants
ca970b9706 Use gallery updated at for cover mod time (#5225) 2024-09-05 16:45:15 +10:00
WithoutPants
2b288fd67c Add changelog for 0.27 (#5224) 2024-09-05 16:35:14 +10:00
WithoutPants
7f1ad30db1 Show option for sub-content only if there are child objects (#5223) 2024-09-05 16:34:56 +10:00
yoshnopa
5721ea2b70 Gallery scrubber wall view (#5191)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-09-05 13:06:43 +10:00
WithoutPants
8c2a25b833 Fix gallery scrubber image order (#5222) 2024-09-05 12:59:20 +10:00
DirtyRacer1337
601a16b5cb replace stashBox validation (#5187) 2024-09-05 12:59:05 +10:00
dogwithakeyboard
879c20efc7 Add linkTypes to performer popover (#5195) 2024-09-05 12:55:19 +10:00
yoshnopa
283f76240f Make Scrubbers touchscreen capable (#5183) 2024-09-05 11:43:31 +10:00
WithoutPants
ad17e7defe Fix handling of files to delete during delete Gallery operation (#5213)
* Only remove file in zip from image if deleting from zip file
* Only remove file in folder from image if deleting from folder
2024-09-05 11:27:31 +10:00
WithoutPants
7a2e59fcef Fix scene filter panel colour slider range (#5221)
* Refactor SceneVideoFilterPanel sliders.

Fix colour values to go between 0-200%.

* Add cursor for filter slider values to hint interaction
2024-09-05 11:26:25 +10:00
WithoutPants
7c09f24f34 Don't try to migrate non-existent vtt files (#5216) 2024-09-05 11:25:56 +10:00
WithoutPants
fb82866512 Don't show move drop target on non-move drag operations (#5219) 2024-09-05 11:25:30 +10:00
Gykes
15da2c1f4c Fix select field alias odd spacing (#5218)
* Fix Tag and Alias odd spacing

As Echo6ix brough up the HTML Engine doesn't generate whitespace at the beginning of a string. Modifying it to use `&nbsp;` so that the spacing will be correct.

fixes https://github.com/stashapp/stash/issues/4997

* update for performerSelect and studioSelect
2024-09-05 11:25:05 +10:00
WithoutPants
1dac598755 Remove console.log. Remove vestigial property (#5217) 2024-09-05 09:35:56 +10:00
Ian McKenzie
ad442fbee5 lint: switch exportloopref to copyloopvar per warning message (#5212) 2024-09-04 16:05:44 +10:00
WithoutPants
4e9925fd3f Show page numbers on low page count (#5206)
Shows individual page numbers instead of the page count selector when pages < 4.
2024-09-04 09:41:53 +10:00
WithoutPants
7b064ac99e Only give height to gallery-container on larger devices (#5205)
Having height/overflow on the stacked/vertical orientation causes weird scrolling behaviour.
2024-09-04 09:39:59 +10:00
WithoutPants
a8a3b4cfd9 Don't focus query field on select (#5204) 2024-09-04 09:39:41 +10:00
WithoutPants
306ba63ab6 Prevent window scrolling on mouse wheel scroll when numeric input field is focused and hovered. (#5199) 2024-09-03 16:33:49 +10:00
WithoutPants
c21ded028a Scan video orientation (#5189)
* Adjust video dimensions for side data rotation
* Warn user when ffprobe version < 5. Only get rotation data on version >= 5
2024-09-03 16:33:15 +10:00
WithoutPants
899ee713ab Adjust image dimensions for exif orientation (#5188) 2024-09-03 16:32:29 +10:00
WithoutPants
a3c34a51aa Gallery cover url (#5182)
* Add default gallery image
* Add gallery cover URL path
* Use new cover URL in UI
* Hide gallery preview scrubber when gallery has no images
* Don't try to show lightbox for gallery without images
* Ignore unrelated lint issue
2024-09-03 16:31:55 +10:00
DogmaDragon
010a355e0b Add info about regex modifiers (#5200) 2024-09-02 22:44:17 +03:00
WithoutPants
bcf0fda7ac Containing Group/Sub-Group relationships (#5105)
* Add UI support for setting containing groups
* Show containing groups in group details panel
* Move tag hierarchical filter code into separate type
* Add depth to scene_count and add sub_group_count
* Add sub-groups tab to groups page
* Add containing groups to edit groups dialog
* Show containing group description in sub-group view
* Show group scene number in group scenes view
* Add ability to drag move grid cards
* Add sub group order option
* Add reorder sub-groups interface
* Separate page size selector component
* Add interfaces to add and remove sub-groups to a group
* Separate MultiSet components
* Allow setting description while setting containing groups
2024-08-30 11:43:44 +10:00
Ian McKenzie
96fdd94a01 Create a section in the history panel to reset scene activity (#5168)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-08-29 13:34:22 +10:00
sezzim
68738bd227 Support for assigning any image from a gallery as the cover (#5053)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-08-29 11:24:52 +10:00
dependabot[bot]
8133aa8c91 Bump micromatch from 4.0.5 to 4.0.8 in /ui/v2.5 (#5180)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-29 10:21:04 +10:00
Andi
ae1841efb0 chore: use errors.New to replace fmt.Errorf with no parameters will much better (#4778) 2024-08-28 14:45:57 +10:00
WithoutPants
27aef4ac2e Update gqlgen and gqlparser dependencies (#5179) 2024-08-28 14:31:56 +10:00
Gykes
b3d6a8eedd Removing Play Button With No File (#5141)
* Remove Play Button With No File
* Hide controls when there is no file
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-08-28 10:50:04 +10:00
Gykes
a023a86ca6 Fix Studio Pluralization (#5161)
Small bug fix so that if a studio only has 1 child studio then the correct pluralization is used.
2024-08-28 10:20:16 +10:00
WithoutPants
294e2090d0 Scene player presentation improvements (#5145)
* Show controls before video plays
* Allow interaction with controls while displaying error
* Source selector improvements

Don't auto-play next source if manually selected.
Don't remove errored sources

* Show errored sources in different style
2024-08-28 10:10:47 +10:00
Gykes
c69d72b243 Add Overlay-duration to span (#5177)
Adding overlay-duration so it has it's own dedicated class. Helps with theming/customization

closes https://github.com/stashapp/stash/issues/4240
2024-08-28 09:40:58 +10:00
Gykes
cdea9374d8 Standardizing the delete dropdown. (#5176)
Changed to use "delete" rather than "delete_entity"
2024-08-28 09:23:58 +10:00
WithoutPants
b1b223c90a Persist tagger settings and change defaults (#5165)
* Persist tagger settings in UIConfig
* Show males and set tags by default
* Add release note
2024-08-28 09:19:50 +10:00
WithoutPants
c74456c07e Bump linux ffmpeg URL for latest version (#5172) 2024-08-28 09:07:49 +10:00
WithoutPants
ca55f96fd8 Replace group image with more consistent svg (#5170) 2024-08-28 09:02:52 +10:00
WithoutPants
b7799df2a6 Add package docs and project vision/goals (#5169)
* Add goals/design vision to contributing doc
* Add barebones package documentation
2024-08-28 09:01:39 +10:00
WithoutPants
10341fba58 Update builtin freeones scraper (#5171) 2024-08-28 09:00:14 +10:00
WithoutPants
996dfb1c2f Gallery scrubber (#5133) 2024-08-28 08:59:41 +10:00
NodudeWasTaken
ce47efc415 Add video codec profiles (#5154) 2024-08-27 18:03:48 +10:00
dogwithakeyboard
3089e1ad69 Markers scene filter (#5097)
* Add scene filter to markers
* Fix labels for scenes
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-08-27 17:23:34 +10:00
Ian McKenzie
62ff6f3c7f Use existing consts for table names in anonymise.go where available (#5167) 2024-08-27 13:01:12 +10:00
Ian McKenzie
e49beb139c Truncate scenes_o_dates and scenes_view_dates as part of anonymize (#5166) 2024-08-27 09:03:22 +10:00
DogmaDragon
d8ee57cd50 [Docs] add note about caption functionality [skip ci] 2024-08-23 02:31:33 +03:00
Gykes
427c18be7d QOL Move Refresh Scrapers to Top (#5142)
QOL change to move the "Refresh Scrapers" button within the "Scrape with..." dropdown to the top.
2024-08-21 09:22:59 +10:00
Gykes
7788a6fd07 PatchComponentRedo (#5136)
* PatchComponent update specifically for SettingsInterfacePanel
* Fix unrelated lint issues
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-08-20 14:25:06 +10:00
WithoutPants
49060e6686 UI nested instead (#5125)
* Support multiple calls to PluginApi.patch.instead for a component.

Allow calling the original/chained function from the hook function.

* Add example of new usage of instead
* Update documentation
2024-08-20 12:36:45 +10:00
WithoutPants
a94bf29b34 Add missing performer sort options to whitelist (#5129)
Adds career length, measurements and weight.
2024-08-13 16:13:15 +10:00
hwill83
ecb53cee55 Fix broken link in development documentation. (#5128)
The existing link 404s.
2024-08-13 09:08:04 +10:00
WithoutPants
fb77e18182 Fix view history imported from o-history json (#5127)
* Fix view history imported from o-history json
* Add scene import unit tests
2024-08-13 09:07:36 +10:00
WithoutPants
c47aafff66 Filter issue fixes (#5126)
* Fix filter reading from URL when not active
* Use alternative clone mechanism. Fixes weird filter hook behaviour
* Separate search term input component
2024-08-12 14:10:10 +10:00
WithoutPants
aa1894964f Codeberg weblate update (#5123)
* Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/

* Translated using Weblate (French)

Currently translated at 100.0% (1155 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1155 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/es/

* Translated using Weblate (Estonian)

Currently translated at 95.7% (1106 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/et/

* Translated using Weblate (French)

Currently translated at 100.0% (1155 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fr/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (1155 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/sv/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1155 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Translated using Weblate (German)

Currently translated at 85.7% (990 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (German)

Currently translated at 86.8% (1003 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 74.0% (855 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/pt_BR/

* Translated using Weblate (Finnish)

Currently translated at 75.7% (875 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/fi/

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 0.4% (5 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/nb_NO/

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Latvian)

* Translated using Weblate (Hindi)

Currently translated at 5.7% (66 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/hi/

* Translated using Weblate (Latvian)

Currently translated at 5.6% (65 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/lv/

* Translated using Weblate (German)

Currently translated at 87.9% (1016 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/de/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1155 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/zh_Hans/

* Added translation using Weblate (Norwegian Nynorsk)

* Translated using Weblate (Norwegian Nynorsk)

Currently translated at 10.3% (120 of 1155 strings)

Translation: stash/stash
Translate-URL: https://translate.codeberg.org/projects/stash/stash/nn/

---------

Co-authored-by: Codeberg Translate <translate@codeberg.org>
Co-authored-by: Larsluph <Larsluph@users.noreply.translate.codeberg.org>
Co-authored-by: gallegonovato <gallegonovato@users.noreply.translate.codeberg.org>
Co-authored-by: Zesty6249 <Zesty6249@users.noreply.translate.codeberg.org>
Co-authored-by: doodoo <doodoo@users.noreply.translate.codeberg.org>
Co-authored-by: AlpacaSerious <AlpacaSerious@users.noreply.translate.codeberg.org>
Co-authored-by: wql219 <wql219@users.noreply.translate.codeberg.org>
Co-authored-by: BSSPM <BSSPM@users.noreply.translate.codeberg.org>
Co-authored-by: icaro <icaro@users.noreply.translate.codeberg.org>
Co-authored-by: IiroS <IiroS@users.noreply.translate.codeberg.org>
Co-authored-by: DogmaDragon <DogmaDragon@users.noreply.translate.codeberg.org>
Co-authored-by: saumya <saumya@users.noreply.translate.codeberg.org>
Co-authored-by: Marky05 <Marky05@users.noreply.translate.codeberg.org>
Co-authored-by: human-corset <human-corset@users.noreply.translate.codeberg.org>
Co-authored-by: tzuuuss <tzuuuss@users.noreply.translate.codeberg.org>
Co-authored-by: throbbing <throbbing@users.noreply.translate.codeberg.org>
2024-08-12 09:29:34 +10:00
blackx69
c8d4dacffd Interactive Tools Enhancements Support (#5115)
* added `useInteractive` hook and exposed to `PluginApi`
* made `SceneFileInfoPanel` patchable
2024-08-06 10:34:27 +10:00
WithoutPants
c79f299d1a Add clone methods to all criterion classes (#5109) 2024-08-02 18:32:11 +10:00
WithoutPants
6a5dc4e774 Refactor ItemList code and re-enable viewing sub-tag/studio content (#5080)
* Refactor list filter to use contexts
* Refactor FilteredListToolbar
* Move components into separate files
* Convert ItemList hook into components
* Fix criteria clone functions
* Add toggle for sub-studio content
* Add toggle for sub-tag content
* Make LoadingIndicator height smaller and fade in.
2024-07-31 16:35:37 +10:00
WithoutPants
540d72bc44 Fix bulk scene setting groups (#5106) 2024-07-31 10:53:40 +10:00
WithoutPants
d96850c008 Rename movie tables to groups in database schema (#5082)
* Rename movie tables to groups
* Correct index name
* Rename synopsis to description in schema
2024-07-30 14:14:16 +10:00
thundxrr
48c6373afa Added detection for n-prefixed ffmpeg version string (#5102) 2024-07-29 21:54:04 +10:00
CJ
5512d37da3 fix missing transgender color icon (#5090) 2024-07-29 14:38:37 +10:00
Lenny3D
f79677ba96 Copy apikey query parameter to DASH & HLS manifest (#5061)
* Copy apikey query parameter to DASH & HLS manifest

When an API key is provided to the DASH and HLS manifest endpoints, this
it will now be copied to the URLs inside the manifest. This allows for
clients that are only able to pass an URL to an (external) video player
to function in case authentication is set up on stash.
2024-07-16 13:17:18 +10:00
dogwithakeyboard
bfd8e81ffd add birthdate to performer select and restyle (#5076) 2024-07-16 13:16:57 +10:00
WithoutPants
720b233be6 Rename movie group backend (#5044)
* Rename movie go files
* Rename movie package to group
2024-07-04 11:36:05 +10:00
WithoutPants
3ddfafa831 Fix background image for group and studio 2024-07-04 11:35:35 +10:00
WithoutPants
f598fa71da Use the rescan option when rescanning files from menu (#5043) 2024-07-04 11:24:03 +10:00
WithoutPants
6cebf146cb Fix validate-ui-quick to only run required checks 2024-07-04 11:11:26 +10:00
WithoutPants
d0caf87eeb Add quick fmt/validate targets 2024-07-04 10:53:29 +10:00
WithoutPants
a0b082a36d Various detail page refactoring (#5037)
* Refactor repeated code into BackgroundImage
* Move BackgroundImage into Details folder
* Refactor performer tabs
* Refactor studio tabs
* Refactor tag tabs
* Refactor repeated code into DetailTitle
* Refactor repeated collapse button code into component
* Reuse FavoriteIcon in details pages
* Refactor performer urls into component
* Refactor alias list into component
* Refactor repeated image code into HeaderImage and LightboxLink components
* Replace render functions with inline conditional rendering
* Support new twitter hostname
2024-07-04 10:52:46 +10:00
WithoutPants
ec23b26c60 Adds mutex protection around dms.Eventing (#5042)
It's possible for concurrent map read/write panic in the Eventing.Subscribe function.
2024-07-04 10:52:04 +10:00
WithoutPants
15a7b8a859 Movie group renames (#5039)
* Rename Movie and MoviePartial to Group/GroupPartial
* Rename Movie interfaces
* Update movie url builders to use group
* Rename movieRoutes to groupRoutes
* Update dataloader
* Update names in sqlite package
* Rename in resolvers
* Add GroupByURL to scraper config
* Scraper backward compatibility hacks
2024-07-04 09:10:26 +10:00
WithoutPants
b69d9cc840 Metadata Providers -> Scraper list improvements (#5040)
* Refactor scraping settings panel
* Add max-height to scraper table
* Separate scraper section
* Add filter to scrapers section
* Add counters to scraper headings
* Show all urls with a scrollbar
* Sort URLs
2024-07-04 09:09:31 +10:00
WithoutPants
12917f51d0 Scraper menu filter (#5041)
* Move scene scraper menu into reusable component
* Reuse ScraperMenu for scene query menu
* Reuse scraper menu in GalleryEditPanel
* Add filter to scraper menu
* Add divider between stashboxes and scrapers
2024-07-04 09:01:35 +10:00
WithoutPants
a3e72b61ee Rename movie components to group (#5038) 2024-07-03 14:17:02 +10:00
WithoutPants
2739696813 Add group graphql interfaces (#5017)
* Deprecate movie and add group interfaces
* UI changes
2024-07-03 13:59:40 +10:00
DogmaDragon
f477b996b5 Update README.md [skip ci]
Fix typo
2024-07-01 17:35:46 +03:00
DogmaDragon
70250c93f1 Update translation instance (#5031)
Replace (incomplete) flag names with SVG banner.
2024-07-01 16:15:21 +10:00
WithoutPants
4cca3b298d Add Opus as supported audio for mp4 (#5030) 2024-07-01 11:19:38 +10:00
dependabot[bot]
436ae0a027 Bump ws from 8.16.0 to 8.17.1 in /ui/v2.5 (#4980)
Bumps [ws](https://github.com/websockets/ws) from 8.16.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.16.0...8.17.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 11:18:54 +10:00
barraged1
dc3ce2b414 updating scrapedPerformerToCreateInput to pass Disambiguation (#5029) 2024-07-01 11:18:20 +10:00
dependabot[bot]
4244bd0b18 Bump golang.org/x/image from 0.16.0 to 0.18.0 (#5021)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.16.0 to 0.18.0.
- [Commits](https://github.com/golang/image/compare/v0.16.0...v0.18.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 11:09:58 +10:00
WithoutPants
cbd273a19c Merge branch 'v0.26.2-stage' into develop 2024-06-27 10:24:25 +10:00
dogwithakeyboard
b7f938531b Check for null disambiguation on validate (#5019) 2024-06-27 10:12:39 +10:00
WithoutPants
af6841be49 Rename Movie to Group in UI (#4963)
* Replace movies with groups in the UI
* Massage menu items
* Change view names
* Rename Movie components to Group
* Refactor movie to group variable names
* Rename movie class names to group
2024-06-26 11:39:31 +10:00
CJ
d986a9eb4f Address resize loop (#5004) 2024-06-24 16:03:29 +10:00
WithoutPants
3156191b83 Fix scene marker query (#5014) 2024-06-24 16:02:46 +10:00
WithoutPants
593207866f Adjust 64 post-migrate where logic
I think not including the scene_id meant that a date could be corrected earlier, meaning the rows affected would be 0. Adding scene_id means that each row should be migrated one by one.
2024-06-24 16:02:18 +10:00
CJ
1f5377da1c Added path column to tables in list view (#5005) 2024-06-24 13:39:32 +10:00
NodudeWasTaken
a4e25f32ea Add apple encoder and fix extra_hw_frames bug (#4986)
* Fixes format in full hw encoding to nv12 for cuda, vaapi and qsv now
* Remove extra_hw_frames
* Add apple transcoder support
* Up the duration to discover decoding errors
* yuv420p is not supported on intel
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-06-24 13:33:27 +10:00
WithoutPants
6775a28ec7 Add schema migration to fix view_date format (#4992)
Also adds index on scene_id and adds a not null constraint to scene_id
2024-06-24 13:15:54 +10:00
WithoutPants
a8fca47a8c Fix save default filter not clearing criteria (#4999) 2024-06-21 16:16:16 +10:00
WithoutPants
2b1a57c6d0 Fix key for tagger scenes (#5000) 2024-06-21 16:15:59 +10:00
Weblate (bot)
a7e5ccd080 Translations update from Hosted Weblate (#4930)
* Translated using Weblate (Thai)

Currently translated at 77.1% (887 of 1149 strings)

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

* Translated using Weblate (Korean)

Currently translated at 100.0% (1149 of 1149 strings)

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

* Translated using Weblate (Thai)

Currently translated at 85.6% (984 of 1149 strings)

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

* Translated using Weblate (Thai)

Currently translated at 99.0% (1138 of 1149 strings)

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

* Translated using Weblate (Russian)

Currently translated at 99.9% (1148 of 1149 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1149 of 1149 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1152 of 1152 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1153 of 1153 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1155 of 1155 strings)

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

---------

Co-authored-by: PZKL48 <nicorobinhood321@gmail.com>
Co-authored-by: 이예찬 <yechan24680@gmail.com>
Co-authored-by: Alexusfree (alexusfree) <alexusfree@gmail.com>
Co-authored-by: Nymeria <Tractorb@seznam.cz>
Co-authored-by: wql219 <160428035+wql219@users.noreply.github.com>
Co-authored-by: Hansi <hansi-go@163.com>
2024-06-19 16:07:09 -04:00
WithoutPants
a1fc14f8c4 Fix join function for studio scenes_filter handler (#4994) 2024-06-19 20:00:30 +10:00
WithoutPants
9c13b39f99 Fix identify clearing parent studio when merging (#4993)
* Refactor ScrapedStudio.ToPartial signature
* Add unit test
* Don't clear parent studio during ToPartial
2024-06-19 19:52:33 +10:00
bob123491234
b3d35dfae4 Add tags to studios (#4858)
* Fix makeTagFilter mode

* Remove studio_tags filter criterion

This is handled by studios_filter. The support for this still needs to be added in the UI, so I have removed the criterion options in the short-term.
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-06-18 15:55:20 +10:00
WithoutPants
f26766033e Performer urls (#4958)
* Populate URLs from legacy fields
* Return nil properly in xpath/json scrapers
* Improve migration logging
2024-06-18 13:41:05 +10:00
WithoutPants
fda4776d30 Movie/Group tags (#4969)
* Combine common tag control code into hook
* Combine common scraped tag row code into hook
2024-06-18 11:24:15 +10:00
WithoutPants
f9a624b803 Default view filters (#4962)
* Merge/adapt from yoshnopa:defaultDetails
* Deprecate and remove default filter calls
* Fix weird behaviour when clicking set as default
* Update deprecated get/set default filter resolvers
* Add config migration
---------
Co-authored-by: yoshnopa <usingusenet@protonmail.com>
2024-06-18 10:51:52 +10:00
well
4be60310c3 In performer scrapers, forward non-http single performer images (#4947)
* Forward non-http single performer images
* Don't set if Images already set
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-06-11 13:41:43 +10:00
WithoutPants
2d483f2d11 Bulk edit tags (#4925)
* Refactor tag relationships and add bulk edit
* Add bulk edit tags dialog
2024-06-11 13:41:20 +10:00
Maista
e18c050fb1 Add log and utils modules to the Javascript VM used in scrapers (#4937) 2024-06-11 13:21:39 +10:00
dependabot[bot]
da4d49d940 Bump braces from 3.0.2 to 3.0.3 in /ui/v2.5 (#4955)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-11 13:19:35 +10:00
WithoutPants
845d718c67 Plugin api improvements (#4935)
* Support hook into App component
* Add hookable PluginSettings component
* Add useSettings to plugin hooks
* Make setting inputs hookable
* Add hooks for performer details panel
* Update docs
2024-06-11 13:18:45 +10:00
WithoutPants
ed057c971f Correct Stash box endpoint inputs (#4924)
* Use stashbox endpoint instead of index
* Update UI to not use deprecated fields
2024-06-11 13:14:12 +10:00
WithoutPants
94a978d063 Scraper inputs (#4922)
* Pass more details in scene/gallery scrape
2024-06-11 13:12:45 +10:00
Flashy78
dcb86d9186 Allow SSL cert paths to be specified in config (#4910) 2024-06-11 13:11:41 +10:00
WithoutPants
62bdff351d Movie URLs (#4900)
* Fix exclude behaviour for stringListCriterionHandlerBuilder
2024-06-11 13:08:49 +10:00
WithoutPants
bf25759a57 Validate custom locale and javascript strings (#4893)
* Validate locale json string
* Validate custom javascript string
2024-06-11 11:36:24 +10:00
WithoutPants
621e890a48 Make pagination more compact (#4882)
* Make pagination more compact

Support entering page number or clicking from drop down

* Fix border radius in dropdown in btn group
* Separate page count control
2024-06-11 11:35:28 +10:00
WithoutPants
e843c890fb Add related object filter criteria to various filter types in graphql schema (#4861)
* Move filter criterion handlers into separate file
* Add related filters for image filter
* Add related filters for scene filter
* Add related filters to gallery filter
* Add related filters to movie filter
* Add related filters to performer filter
* Add related filters to studio filter
* Add related filters to tag filter
* Add scene filter to scene marker filter
2024-06-11 11:34:38 +10:00
its-josh4
ff23d4e20b Update to Go 1.22 (#4822)
* Update to Go 1.22

Updates to Go 1.22 because 1.19 is un-supported and has some CVEs.

Also updates a small number of low-risk deps

* Explicitly install Go in CI
* Bump compiler version
* Add build tags to it target
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2024-06-11 10:55:02 +10:00
808 changed files with 51786 additions and 23738 deletions

View File

@@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true
env:
COMPILER_IMAGE: stashapp/compiler:8
COMPILER_IMAGE: stashapp/compiler:10
jobs:
build:
@@ -23,6 +23,11 @@ jobs:
- name: Checkout
run: git fetch --prune --unshallow --tags
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Pull compiler image
run: docker pull $COMPILER_IMAGE
@@ -108,7 +113,7 @@ jobs:
- name: Upload Windows binary
# only upload binaries for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: stash-win.exe
path: dist/stash-win.exe
@@ -116,7 +121,7 @@ jobs:
- name: Upload macOS binary
# only upload binaries for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: stash-macos
path: dist/stash-macos
@@ -124,7 +129,7 @@ jobs:
- name: Upload Linux binary
# only upload binaries for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: stash-linux
path: dist/stash-linux
@@ -132,7 +137,7 @@ jobs:
- name: Upload UI
# only upload for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: stash-ui.zip
path: dist/stash-ui.zip

View File

@@ -9,7 +9,7 @@ on:
pull_request:
env:
COMPILER_IMAGE: stashapp/compiler:8
COMPILER_IMAGE: stashapp/compiler:10
jobs:
golangci:
@@ -21,6 +21,11 @@ jobs:
- name: Checkout
run: git fetch --prune --unshallow --tags
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Pull compiler image
run: docker pull $COMPILER_IMAGE
@@ -33,7 +38,7 @@ jobs:
run: docker exec -t build /bin/bash -c "make generate-backend"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: latest

3
.gitignore vendored
View File

@@ -21,6 +21,9 @@ vendor
# GraphQL generated output
internal/api/generated_*.go
# Generated locale files
ui/login/locales/*
####
# Visual Studio
####

View File

@@ -15,11 +15,11 @@ linters:
- unused
# Linters added by the stash project.
# - contextcheck
- copyloopvar
- dogsled
- errchkjson
- errorlint
# - exhaustive
- exportloopref
- gocritic
# - goerr113
- gofmt
@@ -48,8 +48,6 @@ linters-settings:
ignore-generated-header: true
severity: error
confidence: 0.8
error-code: 1
warning-code: 1
rules:
- name: blank-imports
disabled: true

View File

@@ -281,6 +281,10 @@ generate-ui:
generate-backend: touch-ui
go generate ./cmd/stash
.PHONY: generate-login-locale
generate-login-locale:
go generate ./ui
.PHONY: generate-dataloaders
generate-dataloaders:
go generate ./internal/api/loaders
@@ -307,7 +311,8 @@ test:
# runs all tests - including integration tests
.PHONY: it
it:
go test -tags=integration ./...
$(eval GO_BUILD_TAGS += integration)
go test -tags "$(GO_BUILD_TAGS)" ./...
# generates test mocks
.PHONY: generate-test-mocks
@@ -350,7 +355,10 @@ ifdef STASH_SOURCEMAPS
endif
.PHONY: ui
ui: ui-env
ui: ui-only generate-login-locale
.PHONY: ui-only
ui-only: ui-env
cd ui/v2.5 && yarn build
.PHONY: zip-ui
@@ -371,6 +379,20 @@ fmt-ui:
validate-ui:
cd ui/v2.5 && yarn run validate
# these targets run the same steps as fmt-ui and validate-ui, but only on files that have changed
fmt-ui-quick:
cd ui/v2.5 && yarn run prettier --write $$(git diff --name-only --relative --diff-filter d . ../../graphql)
# does not run tsc checks, as they are slow
validate-ui-quick:
cd ui/v2.5 && \
tsfiles=$$(git diff --name-only --relative --diff-filter d src | grep -e "\.tsx\?\$$"); \
scssfiles=$$(git diff --name-only --relative --diff-filter d src | grep "\.scss"); \
prettyfiles=$$(git diff --name-only --relative --diff-filter d . ../../graphql); \
if [ -n "$$tsfiles" ]; then yarn run eslint $$tsfiles; fi && \
if [ -n "$$scssfiles" ]; then yarn run stylelint $$scssfiles; fi && \
if [ -n "$$prettyfiles" ]; then yarn run prettier --check $$prettyfiles; fi
# runs all of the backend PR-acceptance steps
.PHONY: validate-backend
validate-backend: lint it

View File

@@ -24,6 +24,11 @@ For further information you can consult the [documentation](https://docs.stashap
# Installing Stash
#### Windows Users:
As of version 0.27.0, Stash doesn't support anymore _Windows 7, 8, Server 2008 and Server 2012._
Windows 10 or Server 2016 are at least required.
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> macOS | <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
:---:|:---:|:---:|:---:
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release](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>
@@ -57,10 +62,11 @@ Stash can pull metadata (performers, tags, descriptions, studios, and more) dire
<sub>[StashDB](http://stashdb.org) is the canonical instance of our open source metadata API, [stash-box](https://github.com/stashapp/stash-box).</sub>
# Translation
[![Translate](https://hosted.weblate.org/widget/stashapp/stash/svg-badge.svg)](https://hosted.weblate.org/engage/stashapp/)
🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇪🇪 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇷🇺 🇪🇸 🇸🇪 🇹🇼 🇹🇷
[![Translate](https://translate.codeberg.org/widget/stash/stash/svg-badge.svg)](https://translate.codeberg.org/engage/stash/)
Stash is available in 25 languages (so far!) and it could be in your language too. We use Weblate to coordinate community translations. If you want to help us translate Stash into your language, you can make an account at [Stash's Weblate](https://hosted.weblate.org/projects/stashapp/stash/) to get started contributing new languages or improving existing ones. Thanks!
Stash is available in 32 languages (so far!) and it could be in your language too. We use Weblate to coordinate community translations. If you want to help us translate Stash into your language, you can make an account at [Codeberg's Weblate](https://translate.codeberg.org/projects/stash/stash/) to get started contributing new languages or improving existing ones. Thanks!
[![Translation status](https://translate.codeberg.org/widget/stash/stash/multi-auto.svg)](https://translate.codeberg.org/engage/stash/)
# Support (FAQ)
@@ -75,9 +81,9 @@ For more help you can:
# Customization
## Themes and CSS Customization
There is a [directory of community-created themes](https://docs.stashapp.cc/user-interface-ui/themes) on Stash-Docs, along with instructions on how to install them.
There is a [directory of community-created themes](https://docs.stashapp.cc/themes/list) on Stash-Docs.
You can also change the Stash interface to fit your desired style with various snippets from [Custom CSS snippets](https://docs.stashapp.cc/user-interface-ui/custom-css-snippets).
You can also change the Stash interface to fit your desired style with various snippets from [Custom CSS snippets](https://docs.stashapp.cc/themes/custom-css-snippets).
# For Developers

View File

@@ -18,7 +18,7 @@ func customUsage() {
flag.PrintDefaults()
}
func printPhash(ff *ffmpeg.FFMpeg, ffp ffmpeg.FFProbe, inputfile string, quiet *bool) error {
func printPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, quiet *bool) error {
ffvideoFile, err := ffp.NewVideoFile(inputfile)
if err != nil {
return err
@@ -80,7 +80,7 @@ func main() {
ffmpegPath, ffprobePath := getPaths()
encoder := ffmpeg.NewEncoder(ffmpegPath)
// don't need to InitHWSupport, phashing doesn't use hw acceleration
ffprobe := ffmpeg.FFProbe(ffprobePath)
ffprobe := ffmpeg.NewFFProbe(ffprobePath)
for _, item := range args {
if err := printPhash(encoder, ffprobe, item, quiet); err != nil {

View File

@@ -1,7 +1,7 @@
# This dockerfile should be built with `make docker-build` from the stash root.
# Build Frontend
FROM node:alpine as frontend
FROM node:20-alpine AS frontend
RUN apk add --no-cache make git
## cache node_modules separately
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
@@ -13,19 +13,22 @@ RUN make pre-ui
RUN make generate-ui
ARG GITHASH
ARG STASH_VERSION
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui-only
# Build Backend
FROM golang:1.19-alpine as backend
FROM golang:1.22.8-alpine AS backend
RUN apk add --no-cache make alpine-sdk
WORKDIR /stash
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
COPY ./graphql /stash/graphql/
COPY ./scripts /stash/scripts/
COPY ./pkg /stash/pkg/
COPY ./cmd /stash/cmd
COPY ./internal /stash/internal
COPY ./cmd /stash/cmd/
COPY ./internal /stash/internal/
# needed for generate-login-locale
COPY ./ui /stash/ui/
RUN make generate-backend generate-login-locale
COPY --from=frontend /stash /stash/
RUN make generate-backend
ARG GITHASH
ARG STASH_VERSION
RUN make flags-release flags-pie stash

View File

@@ -1,7 +1,7 @@
# This dockerfile should be built with `make docker-cuda-build` from the stash root.
# Build Frontend
FROM node:alpine as frontend
FROM node:20-alpine AS frontend
RUN apk add --no-cache make git
## cache node_modules separately
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
@@ -13,19 +13,22 @@ RUN make pre-ui
RUN make generate-ui
ARG GITHASH
ARG STASH_VERSION
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui-only
# Build Backend
FROM golang:1.19-bullseye as backend
FROM golang:1.22.8-bullseye AS backend
RUN apt update && apt install -y build-essential golang
WORKDIR /stash
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
COPY ./graphql /stash/graphql/
COPY ./scripts /stash/scripts/
COPY ./pkg /stash/pkg/
COPY ./cmd /stash/cmd
COPY ./internal /stash/internal
# needed for generate-login-locale
COPY ./ui /stash/ui/
RUN make generate-backend generate-login-locale
COPY --from=frontend /stash /stash/
RUN make generate-backend
ARG GITHASH
ARG STASH_VERSION
RUN make flags-release flags-pie stash

View File

@@ -4,7 +4,7 @@ This dockerfile is used to build a stash docker container using the current sour
# Building the docker container
From the top-level directory (should contain `main.go` file):
From the top-level directory (should contain `tools.go` file):
```
make docker-build

View File

@@ -12,16 +12,18 @@ 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 ruby tzdata \
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.18/community vips=8.14.3-r0 vips-tools=8.14.3-r0 \
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx stashapp-tools \
&& gem install faraday \
&& apk del .build-deps
RUN apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg ruby tzdata vips vips-tools \
&& pip install --user --break-system-packages mechanicalsoup cloudscraper stashapp-tools \
&& gem install faraday
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
# Basic build-time metadata as defined at https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
LABEL org.opencontainers.image.title="Stash" \
org.opencontainers.image.description="An organizer for your porn, written in Go." \
org.opencontainers.image.url="https://stashapp.cc" \
org.opencontainers.image.documentation="https://docs.stashapp.cc" \
org.opencontainers.image.source="https://github.com/stashapp/stash" \
org.opencontainers.image.licenses="AGPL-3.0"
EXPOSE 9999
CMD ["stash"]

View File

@@ -1,4 +1,4 @@
FROM golang:1.19
FROM golang:1.22.8
LABEL maintainer="https://discord.gg/2TsNFKt"
@@ -26,9 +26,9 @@ RUN apt-get update && \
# FreeBSD cross-compilation setup
# https://github.com/smartmontools/docker-build/blob/6b8c92560d17d325310ba02d9f5a4b250cb0764a/Dockerfile#L66
ENV FREEBSD_VERSION 12.4
ENV FREEBSD_VERSION 13.4
ENV FREEBSD_DOWNLOAD_URL http://ftp.plusline.de/FreeBSD/releases/amd64/${FREEBSD_VERSION}-RELEASE/base.txz
ENV FREEBSD_SHA 581c7edacfd2fca2bdf5791f667402d22fccd8a5e184635e0cac075564d57aa8
ENV FREEBSD_SHA 8e13b0a93daba349b8d28ad246d7beb327659b2ef4fe44d89f447392daec5a7c
RUN cd /tmp && \
curl -o base.txz $FREEBSD_DOWNLOAD_URL && \

View File

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

View File

@@ -1,12 +1,14 @@
# Docker Installation (for most 64-bit GNU/Linux systems)
StashApp is supported on most systems that support Docker and docker-compose. Your OS likely ships with or makes available the necessary packages.
StashApp is supported on most systems that support Docker. Your OS likely ships with or makes available the necessary packages.
## Dependencies
Only `docker` and `docker-compose` are required. For the most part your understanding of the technologies can be superficial. So long as you can follow commands and are open to reading a bit, you should be fine.
Only `docker` is required. For the most part your understanding of the technologies can be superficial. So long as you can follow commands and are open to reading a bit, you should be fine.
Installation instructions are available below, and if your distrobution's repository ships a current version of docker, you may use that.
Installation instructions are available below, and if your distributions's repository ships a current version of docker, you may use that.
https://docs.docker.com/engine/install/
On some distributions, `docker compose` is shipped seperately, usually as `docker-cli-compose`. docker-compose is not recommended.
### Get the docker-compose.yml file
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:
@@ -19,7 +21,7 @@ curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/deve
Once you have that file where you want it, modify the settings as you please, and then run:
```
docker-compose up -d
docker compose up -d
```
Installing StashApp this way will by default bind stash to port 9999. This is available in your web browser locally at http://localhost:9999 or on your network as http://YOUR-LOCAL-IP:9999
@@ -29,9 +31,9 @@ Good luck and have fun!
### Docker
Docker is effectively a cross-platform software package repository. It allows you to ship an entire environment in what's referred to as a container. Containers are intended to hold everything that is needed to run an application from one place to another, making it easy for everyone along the way to reproduce the environment.
The StashApp docker container ships with everything you need to automatically build and run stash, including ffmpeg.
The StashApp docker container ships with everything you need to automatically run stash, including ffmpeg.
### docker-compose
Docker Compose lets you specify how and where to run your containers, and to manage their environment. The docker-compose.yml file in this folder gets you a fully working instance of StashApp exactly as you would need it to have a reasonable instance for testing / developing on. If you are deploying a live instance for production, a reverse proxy (such as NGINX or Traefik) is recommended, but not required.
### docker compose
Docker Compose lets you specify how and where to run your containers, and to manage their environment. The docker-compose.yml file in this folder gets you a fully working instance of StashApp exactly as you would need it to have a reasonable instance for testing / developing on. If you are deploying a live instance for production, a [reverse proxy](https://docs.stashapp.cc/guides/reverse-proxy/) (such as NGINX or Traefik) is recommended, but not required.
The latest version is always recommended.

View File

@@ -1,6 +1,5 @@
# APPNICENAME=Stash
# APPDESCRIPTION=An organizer for your porn, written in Go
version: '3.4'
services:
stash:
image: stashapp/stash:latest
@@ -27,10 +26,12 @@ services:
- /etc/localtime:/etc/localtime:ro
## Adjust below paths (the left part) to your liking.
## E.g. you can change ./config:/root/.stash to ./stash:/root/.stash
## The left part is the path on your host, the right part is the path in the stash container.
## Keep configs, scrapers, and plugins here.
- ./config:/root/.stash
## Point this at your collection.
## The left side is where your collection is on your host, the right side is where it will be in stash.
- ./data:/data
## This is where your stash's metadata lives
- ./metadata:/metadata

View File

@@ -1,3 +1,24 @@
## Goals and design vision
The goal of stash is to be:
- an application for organising and viewing adult content - currently this is videos and images, in future this will be extended to include audio and text content
- organising includes scraping of metadata from websites and metadata repositories
- free and open-source
- portable and offline - can be run on a USB stick without needing to install dependencies (with the exception of ffmpeg)
- minimal, but highly extensible. The core feature set should be the minimum required to achieve the primary goal, while being extensible enough to extend via plugins
- easy to learn and use, with minimal technical knowledge required
The core stash system is not intended for:
- managing downloading of content
- managing content on external websites
- publically sharing content
Other requirements:
- support as many video and image formats as possible
- interfaces with external systems (for example stash-box) should be made as generic as possible.
Design considerations:
- features are easy to add and difficult to remove. Large superfluous features should be scrutinised and avoided where possible (eg DLNA, filename parser). Such features should be considered for third-party plugins instead.
## Technical Debt
Please be sure to consider how heavily your contribution impacts the maintainability of the project long term, sometimes less is more. We don't want to merge collossal pull requests with hundreds of dependencies by a driveby contributor.

View File

@@ -4,7 +4,7 @@
* [Go](https://golang.org/dl/)
* [GolangCI](https://golangci-lint.run/) - A meta-linter which runs several linters in parallel
* To install, follow the [local installation instructions](https://golangci-lint.run/usage/install/#local-installation)
* To install, follow the [local installation instructions](https://golangci-lint.run/welcome/install/#local-installation)
* [Yarn](https://yarnpkg.com/en/docs/install) - Yarn package manager
## Environment
@@ -69,6 +69,9 @@ NOTE: The `make` command in OpenBSD will be `gmake`. For example, `make pre-ui`
* `make it` - Runs all unit and integration tests
* `make fmt` - Formats the Go source code
* `make fmt-ui` - Formats the UI source code
* `make validate-ui` - Runs tests and checks for the UI only
* `make fmt-ui-quick` - (experimental) Formats only changed UI source code
* `make validate-ui-quick` - (experimental) Runs tests and checks of changed UI code
* `make server-start` - Runs a development stash server in the `.local` directory
* `make server-clean` - Removes the `.local` directory and all of its contents
* `make ui-start` - Runs the UI in development mode. Requires a running Stash server to connect to - the server 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.

63
go.mod
View File

@@ -1,11 +1,11 @@
module github.com/stashapp/stash
go 1.19
go 1.22.8
require (
github.com/99designs/gqlgen v0.17.2
github.com/99designs/gqlgen v0.17.55
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552
github.com/Yamashou/gqlgenc v0.0.6
github.com/Yamashou/gqlgenc v0.25.3
github.com/anacrolix/dms v1.2.2
github.com/antchfx/htmlquery v1.3.0
github.com/asticode/go-astisub v0.25.1
@@ -15,57 +15,61 @@ require (
github.com/disintegration/imaging v1.6.2
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
github.com/doug-martin/goqu/v9 v9.18.0
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/cors v1.2.1
github.com/go-chi/httplog v0.3.1
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/gofrs/uuid/v5 v5.0.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang-migrate/migrate/v4 v4.16.2
github.com/google/uuid v1.6.0
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/golang-lru/v2 v2.0.6
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/hasura/go-graphql-client v0.13.1
github.com/jinzhu/copier v0.4.0
github.com/jmoiron/sqlx v1.3.5
github.com/jmoiron/sqlx v1.4.0
github.com/json-iterator/go v1.1.12
github.com/kermieisinthehouse/gosx-notifier v0.1.2
github.com/kermieisinthehouse/systray v1.2.4
github.com/knadh/koanf v1.5.0
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-sqlite3 v1.14.17
github.com/mattn/go-sqlite3 v1.14.22
github.com/mitchellh/mapstructure v1.5.0
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/remeh/sizedwaitgroup v1.0.0
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cast v1.5.1
github.com/spf13/cast v1.6.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.9.0
github.com/tidwall/gjson v1.16.0
github.com/vearutop/statigz v1.4.0
github.com/vektah/dataloaden v0.3.0
github.com/vektah/gqlparser/v2 v2.4.2
github.com/vektah/gqlparser/v2 v2.5.18
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.21.0
golang.org/x/image v0.12.0
golang.org/x/net v0.23.0
golang.org/x/sys v0.18.0
golang.org/x/term v0.18.0
golang.org/x/text v0.14.0
golang.org/x/crypto v0.31.0
golang.org/x/image v0.18.0
golang.org/x/net v0.33.0
golang.org/x/sys v0.28.0
golang.org/x/term v0.27.0
golang.org/x/text v0.21.0
golang.org/x/time v0.10.0
gopkg.in/guregu/null.v4 v4.0.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/agnivade/levenshtein v1.2.0 // indirect
github.com/antchfx/xpath v1.2.3 // indirect
github.com/asticode/go-astikit v0.20.0 // indirect
github.com/asticode/go-astits v1.8.0 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
@@ -77,18 +81,15 @@ require (
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // 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.1.0 // indirect
github.com/josharian/intern v1.0.0 // 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/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -99,19 +100,21 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/zerolog v1.30.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/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
github.com/urfave/cli/v2 v2.27.5 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // 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
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/tools v0.26.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

171
go.sum
View File

@@ -49,23 +49,25 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/99designs/gqlgen v0.17.2 h1:yczvlwMsfcVu/JtejqfrLwXuSP0yZFhmcss3caEvHw8=
github.com/99designs/gqlgen v0.17.2/go.mod h1:K5fzLKwtph+FFgh9j7nFbRUdBKvTcGnsta51fsMTn3o=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/99designs/gqlgen v0.17.55 h1:3vzrNWYyzSZjGDFo68e5j9sSauLxfKvLp+6ioRokVtM=
github.com/99designs/gqlgen v0.17.55/go.mod h1:3Bq768f8hgVPGZxL8aY9MaYmbxa6llPM/qu1IGH1EJo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0=
github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U=
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552 h1:eukVk+mGmbSZppLw8WJGpEUgMC570eb32y7FOsPW4Kc=
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552/go.mod h1:LKbO1i6L1lSlwWx4NHWVECxubHNKFz2YQoEMGXAFVy8=
github.com/Yamashou/gqlgenc v0.0.6 h1:wfMTtuVSrX2N1z5/ssecxx+E7l1fa0FOq5mwFW47oY4=
github.com/Yamashou/gqlgenc v0.0.6/go.mod h1:WOXjogecRGpD1WKgxnnyHJo0/Dxn44p/LNRoE6mtFQo=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/Yamashou/gqlgenc v0.25.3 h1:mVV8/Ho8EDZUQKQZbQqqXGNq8jc8aQfPpHhZOnTkMNE=
github.com/Yamashou/gqlgenc v0.25.3/go.mod h1:G0g1N81xpIklVdnyboW1zwOHcj/n4hNfhTwfN29Rjig=
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -81,6 +83,9 @@ github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes=
@@ -114,6 +119,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bool64/dev v0.2.28 h1:6ayDfrB/jnNr2iQAZHI+uT3Qi6rErSbJYQs1y8rSrwM=
github.com/bool64/dev v0.2.28/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -147,22 +153,24 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
@@ -193,7 +201,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@@ -202,8 +211,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/httplog v0.3.1 h1:uC3IUWCZagtbCinb3ypFh36SEcgd6StWw2Bu0XSXRtg=
@@ -220,8 +229,9 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
@@ -234,12 +244,10 @@ github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K
github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0=
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@@ -294,7 +302,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -319,6 +328,8 @@ github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@@ -329,7 +340,6 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -368,10 +378,9 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.6 h1:3xi/Cafd1NaoEnS/yDssIiuVeDVywU0QdFGl3aQaQHM=
github.com/hashicorp/golang-lru/v2 v2.0.6/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
@@ -385,6 +394,8 @@ github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoI
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hasura/go-graphql-client v0.13.1 h1:kKbjhxhpwz58usVl+Xvgah/TDha5K2akNTRQdsEHN6U=
github.com/hasura/go-graphql-client v0.13.1/go.mod h1:k7FF7h53C+hSNFRG3++DdVZWIuHdCaTbI7siTJ//zGQ=
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
@@ -399,8 +410,8 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -421,7 +432,6 @@ github.com/kermieisinthehouse/gosx-notifier v0.1.2 h1:KV0KBeKK2B24kIHY7iK0jgS64Q
github.com/kermieisinthehouse/gosx-notifier v0.1.2/go.mod h1:xyWT07azFtUOcHl96qMVvKhvKzsMcS7rKTHQyv8WTho=
github.com/kermieisinthehouse/systray v1.2.4 h1:pdH5vnl+KKjRrVCRU4g/2W1/0HVzuuJ6WXHlPPHYY6s=
github.com/kermieisinthehouse/systray v1.2.4/go.mod h1:axh6C/jNuSyC0QGtidZJURc9h+h41HNoMySoLVrhVR4=
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
@@ -435,16 +445,16 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
@@ -453,8 +463,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/moq v0.2.3 h1:Q06vEqnBYjjfx5KKgHfYRKE/lvlRu+Nj+xodG4YdHnU=
github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -469,12 +477,12 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
@@ -491,7 +499,6 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@@ -565,6 +572,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -572,9 +580,10 @@ github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
@@ -582,11 +591,8 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5P
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
@@ -594,6 +600,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
@@ -601,8 +609,8 @@ github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfA
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
@@ -617,8 +625,9 @@ github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1Fof
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -628,8 +637,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
@@ -642,24 +652,21 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/vearutop/statigz v1.4.0 h1:RQL0KG3j/uyA/PFpHeZ/L6l2ta920/MxlOAIGEOuwmU=
github.com/vearutop/statigz v1.4.0/go.mod h1:LYTolBLiz9oJISwiVKnOQoIwhO1LWX1A7OECawGS8XE=
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/vektah/gqlparser/v2 v2.4.2 h1:29TGc6QmhEUq5fll+2FPoTmhUhR65WEKN4VK/jo0OlM=
github.com/vektah/gqlparser/v2 v2.4.2/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/vektah/gqlparser/v2 v2.5.18 h1:zSND3GtutylAQ1JpWnTHcqtaRZjl+y3NROeW8vuNo6Y=
github.com/vektah/gqlparser/v2 v2.5.18/go.mod h1:6HLzf7JKv9Fi3APymudztFQNmLXR5qJeEo6BOFcXVfc=
github.com/vektra/mockery/v2 v2.10.0 h1:MiiQWxwdq7/ET6dCXLaJzSGEN17k758H7JHS9kOdiks=
github.com/vektra/mockery/v2 v2.10.0/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e h1:GruPsb+44XvYAzuAgJW1d1WHqmcI73L2XSjsbx/eJZw=
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e/go.mod h1:wA8kQ8WFipMciY9WcWzqQgZordm/P7l8IZdvx1crwmc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -706,8 +713,8 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -721,8 +728,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -748,11 +755,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -802,9 +807,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -834,8 +838,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -918,24 +922,21 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -948,13 +949,13 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -997,7 +998,6 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -1014,12 +1014,9 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -7,9 +7,6 @@ exec:
filename: internal/api/generated_exec.go
model:
filename: internal/api/generated_models.go
resolver:
filename: internal/api/resolver.go
type: Resolver
struct_tag: gqlgen
@@ -20,7 +17,7 @@ autobind:
- github.com/stashapp/stash/pkg/scraper
- github.com/stashapp/stash/internal/identify
- github.com/stashapp/stash/internal/dlna
- github.com/stashapp/stash/pkg/scraper/stashbox
- github.com/stashapp/stash/pkg/stashbox
models:
# Scalars
@@ -38,12 +35,6 @@ models:
model: github.com/stashapp/stash/internal/api.BoolMap
PluginConfigMap:
model: github.com/stashapp/stash/internal/api.PluginConfigMap
# define to force resolvers
Image:
model: github.com/stashapp/stash/pkg/models.Image
fields:
title:
resolver: true
VideoFile:
fields:
# override float fields - #1572
@@ -51,6 +42,11 @@ models:
fieldName: DurationFinite
frame_rate:
fieldName: FrameRateFinite
# movie is group under the hood
Movie:
model: github.com/stashapp/stash/pkg/models.Group
MovieFilterType:
model: github.com/stashapp/stash/pkg/models.GroupFilterType
# autobind on config causes generation issues
BlobsStorageType:
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
@@ -127,9 +123,6 @@ models:
model: github.com/stashapp/stash/internal/identify.FieldStrategy
ScraperSource:
model: github.com/stashapp/stash/pkg/scraper.Source
# rebind inputs to types
StashIDInput:
model: github.com/stashapp/stash/pkg/models.StashID
IdentifySourceInput:
model: github.com/stashapp/stash/internal/identify.Source
IdentifyFieldOptionsInput:

View File

@@ -4,6 +4,7 @@ type Query {
findSavedFilter(id: ID!): SavedFilter
findSavedFilters(mode: FilterMode): [SavedFilter!]!
findDefaultFilter(mode: FilterMode!): SavedFilter
@deprecated(reason: "default filter now stored in UI config")
"Find a scene by ID or Checksum"
findScene(id: ID, checksum: String): Scene
@@ -44,6 +45,7 @@ type Query {
findSceneMarkers(
scene_marker_filter: SceneMarkerFilterType
filter: FindFilterType
ids: [ID!]
): FindSceneMarkersResultType!
findImage(id: ID, checksum: String): Image
@@ -76,13 +78,22 @@ type Query {
): FindStudiosResultType!
"Find a movie by ID"
findMovie(id: ID!): Movie
findMovie(id: ID!): Movie @deprecated(reason: "Use findGroup instead")
"A function which queries Movie objects"
findMovies(
movie_filter: MovieFilterType
filter: FindFilterType
ids: [ID!]
): FindMoviesResultType!
): FindMoviesResultType! @deprecated(reason: "Use findGroups instead")
"Find a group by ID"
findGroup(id: ID!): Group
"A function which queries Group objects"
findGroups(
group_filter: GroupFilterType
filter: FindFilterType
ids: [ID!]
): FindGroupsResultType!
findGallery(id: ID!): Gallery
findGalleries(
@@ -155,7 +166,19 @@ type Query {
scrapeSingleMovie(
source: ScraperSourceInput!
input: ScrapeSingleMovieInput!
): [ScrapedMovie!]!
): [ScrapedMovie!]! @deprecated(reason: "Use scrapeSingleGroup instead")
"Scrape for a single group"
scrapeSingleGroup(
source: ScraperSourceInput!
input: ScrapeSingleGroupInput!
): [ScrapedGroup!]!
"Scrape for a single image"
scrapeSingleImage(
source: ScraperSourceInput!
input: ScrapeSingleImageInput!
): [ScrapedImage!]!
"Scrapes content based on a URL"
scrapeURL(url: String!, ty: ScrapeContentType!): ScrapedContent
@@ -166,8 +189,13 @@ type Query {
scrapeSceneURL(url: String!): ScrapedScene
"Scrapes a complete gallery record based on a URL"
scrapeGalleryURL(url: String!): ScrapedGallery
"Scrapes a complete image record based on a URL"
scrapeImageURL(url: String!): ScrapedImage
"Scrapes a complete movie record based on a URL"
scrapeMovieURL(url: String!): ScrapedMovie
@deprecated(reason: "Use scrapeGroupURL instead")
"Scrapes a complete group record based on a URL"
scrapeGroupURL(url: String!): ScrapedGroup
# Plugins
"List loaded plugins"
@@ -213,7 +241,7 @@ type Query {
allPerformers: [Performer!]!
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
allMovies: [Movie!]! @deprecated(reason: "Use findMovies instead")
allMovies: [Movie!]! @deprecated(reason: "Use findGroups instead")
# Get everything with minimal metadata
@@ -257,6 +285,13 @@ type Mutation {
"Sets the resume time point (if provided) and adds the provided duration to the scene's play duration"
sceneSaveActivity(id: ID!, resume_time: Float, playDuration: Float): Boolean!
"Resets the resume time point and play duration"
sceneResetActivity(
id: ID!
reset_resume: Boolean
reset_duration: Boolean
): Boolean!
"Increments the play count for the scene. Returns the new play count value."
sceneIncrementPlayCount(id: ID!): Int!
@deprecated(reason: "Use sceneAddPlay instead")
@@ -274,6 +309,7 @@ type Mutation {
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean!
sceneMarkersDestroy(ids: [ID!]!): Boolean!
sceneAssignFile(input: AssignSceneFileInput!): Boolean!
@@ -298,6 +334,8 @@ type Mutation {
addGalleryImages(input: GalleryAddInput!): Boolean!
removeGalleryImages(input: GalleryRemoveInput!): Boolean!
setGalleryCover(input: GallerySetCoverInput!): Boolean!
resetGalleryCover(input: GalleryResetCoverInput!): Boolean!
galleryChapterCreate(input: GalleryChapterCreateInput!): GalleryChapter
galleryChapterUpdate(input: GalleryChapterUpdateInput!): GalleryChapter
@@ -315,16 +353,34 @@ type Mutation {
studiosDestroy(ids: [ID!]!): Boolean!
movieCreate(input: MovieCreateInput!): Movie
@deprecated(reason: "Use groupCreate instead")
movieUpdate(input: MovieUpdateInput!): Movie
@deprecated(reason: "Use groupUpdate instead")
movieDestroy(input: MovieDestroyInput!): Boolean!
@deprecated(reason: "Use groupDestroy instead")
moviesDestroy(ids: [ID!]!): Boolean!
@deprecated(reason: "Use groupsDestroy instead")
bulkMovieUpdate(input: BulkMovieUpdateInput!): [Movie!]
@deprecated(reason: "Use bulkGroupUpdate instead")
groupCreate(input: GroupCreateInput!): Group
groupUpdate(input: GroupUpdateInput!): Group
groupDestroy(input: GroupDestroyInput!): Boolean!
groupsDestroy(ids: [ID!]!): Boolean!
bulkGroupUpdate(input: BulkGroupUpdateInput!): [Group!]
addGroupSubGroups(input: GroupSubGroupAddInput!): Boolean!
removeGroupSubGroups(input: GroupSubGroupRemoveInput!): Boolean!
"Reorder sub groups within a group. Returns true if successful."
reorderSubGroups(input: ReorderSubGroupsInput!): Boolean!
tagCreate(input: TagCreateInput!): Tag
tagUpdate(input: TagUpdateInput!): Tag
tagDestroy(input: TagDestroyInput!): Boolean!
tagsDestroy(ids: [ID!]!): Boolean!
tagsMerge(input: TagsMergeInput!): Tag
bulkTagUpdate(input: BulkTagUpdateInput!): [Tag!]
"""
Moves the given files to the given destination. Returns true if successful.
@@ -344,6 +400,7 @@ type Mutation {
saveFilter(input: SaveFilterInput!): SavedFilter!
destroySavedFilter(input: DestroyFilterInput!): Boolean!
setDefaultFilter(input: SetDefaultFilterInput!): Boolean!
@deprecated(reason: "now uses UI config")
"Change general configuration options"
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!

View File

@@ -91,6 +91,12 @@ input StashIDCriterionInput {
modifier: CriterionModifier!
}
input CustomFieldCriterionInput {
field: String!
value: [Any!]
modifier: CriterionModifier!
}
input PerformerFilterType {
AND: PerformerFilterType
OR: PerformerFilterType
@@ -170,10 +176,20 @@ input PerformerFilterType {
birthdate: DateCriterionInput
"Filter by death date"
death_date: DateCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
custom_fields: [CustomFieldCriterionInput!]
}
input SceneMarkerFilterType {
@@ -183,6 +199,10 @@ input SceneMarkerFilterType {
scene_tags: HierarchicalMultiCriterionInput
"Filter to only include scene markers with these performers"
performers: MultiCriterionInput
"Filter to only include scene markers from these scenes"
scenes: MultiCriterionInput
"Filter by duration (in seconds)"
duration: FloatCriterionInput
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
@@ -193,6 +213,8 @@ input SceneMarkerFilterType {
scene_created_at: TimestampCriterionInput
"Filter by lscene ast update time"
scene_updated_at: TimestampCriterionInput
"Filter by related scenes that meet this criteria"
scene_filter: SceneFilterType
}
input SceneFilterType {
@@ -247,7 +269,9 @@ input SceneFilterType {
"Filter to only include scenes with this studio"
studios: HierarchicalMultiCriterionInput
"Filter to only include scenes with this movie"
movies: MultiCriterionInput
movies: MultiCriterionInput @deprecated(reason: "use groups instead")
"Filter to only include scenes with this group"
groups: HierarchicalMultiCriterionInput
"Filter to only include scenes with this gallery"
galleries: MultiCriterionInput
"Filter to only include scenes with these tags"
@@ -288,9 +312,29 @@ input SceneFilterType {
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by related performers that meet this criteria"
performers_filter: PerformerFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
"Filter by related movies that meet this criteria"
movies_filter: MovieFilterType
@deprecated(reason: "use groups_filter instead")
"Filter by related groups that meet this criteria"
groups_filter: GroupFilterType
"Filter by related markers that meet this criteria"
markers_filter: SceneMarkerFilterType
}
input MovieFilterType {
AND: MovieFilterType
OR: MovieFilterType
NOT: MovieFilterType
name: StringCriterionInput
director: StringCriterionInput
synopsis: StringCriterionInput
@@ -307,12 +351,68 @@ input MovieFilterType {
url: StringCriterionInput
"Filter to only include movies where performer appears in a scene"
performers: MultiCriterionInput
"Filter to only include movies with these tags"
tags: HierarchicalMultiCriterionInput
"Filter by tag count"
tag_count: IntCriterionInput
"Filter by date"
date: DateCriterionInput
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
}
input GroupFilterType {
AND: GroupFilterType
OR: GroupFilterType
NOT: GroupFilterType
name: StringCriterionInput
director: StringCriterionInput
synopsis: StringCriterionInput
"Filter by duration (in seconds)"
duration: IntCriterionInput
# rating expressed as 1-100
rating100: IntCriterionInput
"Filter to only include groups with this studio"
studios: HierarchicalMultiCriterionInput
"Filter to only include groups missing this property"
is_missing: String
"Filter by url"
url: StringCriterionInput
"Filter to only include groups where performer appears in a scene"
performers: MultiCriterionInput
"Filter to only include groups with these tags"
tags: HierarchicalMultiCriterionInput
"Filter by tag count"
tag_count: IntCriterionInput
"Filter by date"
date: DateCriterionInput
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
updated_at: TimestampCriterionInput
"Filter by containing groups"
containing_groups: HierarchicalMultiCriterionInput
"Filter by sub groups"
sub_groups: HierarchicalMultiCriterionInput
"Filter by number of containing groups the group has"
containing_group_count: IntCriterionInput
"Filter by number of sub-groups the group has"
sub_group_count: IntCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
}
input StudioFilterType {
@@ -326,6 +426,8 @@ input StudioFilterType {
parents: MultiCriterionInput
"Filter by StashID"
stash_id_endpoint: StashIDCriterionInput
"Filter to only include studios with these tags"
tags: HierarchicalMultiCriterionInput
"Filter to only include studios missing this property"
is_missing: String
# rating expressed as 1-100
@@ -338,6 +440,8 @@ input StudioFilterType {
image_count: IntCriterionInput
"Filter by gallery count"
gallery_count: IntCriterionInput
"Filter by tag count"
tag_count: IntCriterionInput
"Filter by url"
url: StringCriterionInput
"Filter by studio aliases"
@@ -346,6 +450,12 @@ input StudioFilterType {
child_count: IntCriterionInput
"Filter by autotag ignore value"
ignore_auto_tag: Boolean
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by creation time"
created_at: TimestampCriterionInput
"Filter by last update time"
@@ -411,6 +521,17 @@ input GalleryFilterType {
code: StringCriterionInput
"Filter by photographer"
photographer: StringCriterionInput
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related performers that meet this criteria"
performers_filter: PerformerFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
}
input TagFilterType {
@@ -421,6 +542,9 @@ input TagFilterType {
"Filter by tag name"
name: StringCriterionInput
"Filter by tag sort_name"
sort_name: StringCriterionInput
"Filter by tag aliases"
aliases: StringCriterionInput
@@ -445,6 +569,15 @@ input TagFilterType {
"Filter by number of performers with this tag"
performer_count: IntCriterionInput
"Filter by number of studios with this tag"
studio_count: IntCriterionInput
"Filter by number of movies with this tag"
movie_count: IntCriterionInput
"Filter by number of group with this tag"
group_count: IntCriterionInput
"Filter by number of markers with this tag"
marker_count: IntCriterionInput
@@ -463,6 +596,13 @@ input TagFilterType {
"Filter by autotag ignore value"
ignore_auto_tag: Boolean
"Filter by related scenes that meet this criteria"
scenes_filter: SceneFilterType
"Filter by related images that meet this criteria"
images_filter: ImageFilterType
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by creation time"
created_at: TimestampCriterionInput
@@ -528,6 +668,15 @@ input ImageFilterType {
code: StringCriterionInput
"Filter by photographer"
photographer: StringCriterionInput
"Filter by related galleries that meet this criteria"
galleries_filter: GalleryFilterType
"Filter by related performers that meet this criteria"
performers_filter: PerformerFilterType
"Filter by related studios that meet this criteria"
studios_filter: StudioFilterType
"Filter by related tags that meet this criteria"
tags_filter: TagFilterType
}
enum CriterionModifier {
@@ -623,6 +772,7 @@ enum FilterMode {
GALLERIES
SCENE_MARKERS
MOVIES
GROUPS
TAGS
IMAGES
}

View File

@@ -1,3 +1,8 @@
type GalleryPathsType {
cover: String!
preview: String! # Resolver
}
"Gallery type"
type Gallery {
id: ID!
@@ -25,6 +30,9 @@ type Gallery {
performers: [Performer!]!
cover: Image
paths: GalleryPathsType! # Resolver
image(index: Int!): Image!
}
input GalleryCreateInput {
@@ -108,3 +116,12 @@ input GalleryRemoveInput {
gallery_id: ID!
image_ids: [ID!]!
}
input GallerySetCoverInput {
gallery_id: ID!
cover_image_id: ID!
}
input GalleryResetCoverInput {
gallery_id: ID!
}

View File

@@ -0,0 +1,137 @@
"GroupDescription represents a relationship to a group with a description of the relationship"
type GroupDescription {
group: Group!
description: String
}
type Group {
id: ID!
name: String!
aliases: String
"Duration in seconds"
duration: Int
date: String
# rating expressed as 1-100
rating100: Int
studio: Studio
director: String
synopsis: String
urls: [String!]!
tags: [Tag!]!
created_at: Time!
updated_at: Time!
containing_groups: [GroupDescription!]!
sub_groups: [GroupDescription!]!
front_image_path: String # Resolver
back_image_path: String # Resolver
scene_count(depth: Int): Int! # Resolver
sub_group_count(depth: Int): Int! # Resolver
scenes: [Scene!]!
}
input GroupDescriptionInput {
group_id: ID!
description: String
}
input GroupCreateInput {
name: String!
aliases: String
"Duration in seconds"
duration: Int
date: String
# rating expressed as 1-100
rating100: Int
studio_id: ID
director: String
synopsis: String
urls: [String!]
tag_ids: [ID!]
containing_groups: [GroupDescriptionInput!]
sub_groups: [GroupDescriptionInput!]
"This should be a URL or a base64 encoded data URL"
front_image: String
"This should be a URL or a base64 encoded data URL"
back_image: String
}
input GroupUpdateInput {
id: ID!
name: String
aliases: String
duration: Int
date: String
# rating expressed as 1-100
rating100: Int
studio_id: ID
director: String
synopsis: String
urls: [String!]
tag_ids: [ID!]
containing_groups: [GroupDescriptionInput!]
sub_groups: [GroupDescriptionInput!]
"This should be a URL or a base64 encoded data URL"
front_image: String
"This should be a URL or a base64 encoded data URL"
back_image: String
}
input BulkUpdateGroupDescriptionsInput {
groups: [GroupDescriptionInput!]!
mode: BulkUpdateIdMode!
}
input BulkGroupUpdateInput {
clientMutationId: String
ids: [ID!]
# rating expressed as 1-100
rating100: Int
studio_id: ID
director: String
urls: BulkUpdateStrings
tag_ids: BulkUpdateIds
containing_groups: BulkUpdateGroupDescriptionsInput
sub_groups: BulkUpdateGroupDescriptionsInput
}
input GroupDestroyInput {
id: ID!
}
input ReorderSubGroupsInput {
"ID of the group to reorder sub groups for"
group_id: ID!
"""
IDs of the sub groups to reorder. These must be a subset of the current sub groups.
Sub groups will be inserted in this order at the insert_index
"""
sub_group_ids: [ID!]!
"The sub-group ID at which to insert the sub groups"
insert_at_id: ID!
"If true, the sub groups will be inserted after the insert_index, otherwise they will be inserted before"
insert_after: Boolean
}
type FindGroupsResultType {
count: Int!
groups: [Group!]!
}
input GroupSubGroupAddInput {
containing_group_id: ID!
sub_groups: [GroupDescriptionInput!]!
"The index at which to insert the sub groups. If not provided, the sub groups will be appended to the end"
insert_index: Int
}
input GroupSubGroupRemoveInput {
containing_group_id: ID!
sub_group_ids: [ID!]!
}

View File

@@ -284,7 +284,8 @@ input ExportObjectsInput {
studios: ExportObjectTypeInput
performers: ExportObjectTypeInput
tags: ExportObjectTypeInput
movies: ExportObjectTypeInput
groups: ExportObjectTypeInput
movies: ExportObjectTypeInput @deprecated(reason: "Use groups instead")
galleries: ExportObjectTypeInput
includeDependencies: Boolean
}
@@ -337,3 +338,10 @@ type SystemStatus {
input MigrateInput {
backupPath: String!
}
input CustomFieldsInput {
"If populated, the entire custom fields map will be replaced with this value"
full: Map
"If populated, only the keys in this map will be updated"
partial: Map
}

View File

@@ -10,13 +10,15 @@ type Movie {
studio: Studio
director: String
synopsis: String
url: String
url: String @deprecated(reason: "Use urls")
urls: [String!]!
tags: [Tag!]!
created_at: Time!
updated_at: Time!
front_image_path: String # Resolver
back_image_path: String # Resolver
scene_count: Int! # Resolver
scene_count(depth: Int): Int! # Resolver
scenes: [Scene!]!
}
@@ -31,7 +33,9 @@ input MovieCreateInput {
studio_id: ID
director: String
synopsis: String
url: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
front_image: String
"This should be a URL or a base64 encoded data URL"
@@ -49,7 +53,9 @@ input MovieUpdateInput {
studio_id: ID
director: String
synopsis: String
url: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
front_image: String
"This should be a URL or a base64 encoded data URL"
@@ -63,6 +69,8 @@ input BulkMovieUpdateInput {
rating100: Int
studio_id: ID
director: String
urls: BulkUpdateStrings
tag_ids: BulkUpdateIds
}
input MovieDestroyInput {

View File

@@ -16,10 +16,11 @@ type Performer {
id: ID!
name: String!
disambiguation: String
url: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
gender: GenderEnum
twitter: String
instagram: String
twitter: String @deprecated(reason: "Use urls")
instagram: String @deprecated(reason: "Use urls")
birthdate: String
ethnicity: String
country: String
@@ -41,7 +42,8 @@ type Performer {
scene_count: Int! # Resolver
image_count: Int! # Resolver
gallery_count: Int! # Resolver
movie_count: Int! # Resolver
group_count: Int! # Resolver
movie_count: Int! @deprecated(reason: "use group_count instead") # Resolver
performer_count: Int! # Resolver
o_counter: Int # Resolver
scenes: [Scene!]!
@@ -54,13 +56,17 @@ type Performer {
weight: Int
created_at: Time!
updated_at: Time!
movies: [Movie!]!
groups: [Group!]!
movies: [Movie!]! @deprecated(reason: "use groups instead")
custom_fields: Map!
}
input PerformerCreateInput {
name: String!
disambiguation: String
url: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
gender: GenderEnum
birthdate: String
ethnicity: String
@@ -75,8 +81,8 @@ input PerformerCreateInput {
tattoos: String
piercings: String
alias_list: [String!]
twitter: String
instagram: String
twitter: String @deprecated(reason: "Use urls")
instagram: String @deprecated(reason: "Use urls")
favorite: Boolean
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
@@ -89,13 +95,16 @@ input PerformerCreateInput {
hair_color: String
weight: Int
ignore_auto_tag: Boolean
custom_fields: Map
}
input PerformerUpdateInput {
id: ID!
name: String
disambiguation: String
url: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
gender: GenderEnum
birthdate: String
ethnicity: String
@@ -110,8 +119,8 @@ input PerformerUpdateInput {
tattoos: String
piercings: String
alias_list: [String!]
twitter: String
instagram: String
twitter: String @deprecated(reason: "Use urls")
instagram: String @deprecated(reason: "Use urls")
favorite: Boolean
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
@@ -124,6 +133,8 @@ input PerformerUpdateInput {
hair_color: String
weight: Int
ignore_auto_tag: Boolean
custom_fields: CustomFieldsInput
}
input BulkUpdateStrings {
@@ -135,7 +146,8 @@ input BulkPerformerUpdateInput {
clientMutationId: String
ids: [ID!]
disambiguation: String
url: String
url: String @deprecated(reason: "Use urls")
urls: BulkUpdateStrings
gender: GenderEnum
birthdate: String
ethnicity: String
@@ -150,8 +162,8 @@ input BulkPerformerUpdateInput {
tattoos: String
piercings: String
alias_list: BulkUpdateStrings
twitter: String
instagram: String
twitter: String @deprecated(reason: "Use urls")
instagram: String @deprecated(reason: "Use urls")
favorite: Boolean
tag_ids: BulkUpdateIds
# rating expressed as 1-100
@@ -161,6 +173,8 @@ input BulkPerformerUpdateInput {
hair_color: String
weight: Int
ignore_auto_tag: Boolean
custom_fields: CustomFieldsInput
}
input PerformerDestroyInput {

View File

@@ -2,7 +2,10 @@ type SceneMarker {
id: ID!
scene: Scene!
title: String!
"The required start time of the marker (in seconds). Supports decimals."
seconds: Float!
"The optional end time of the marker (in seconds). Supports decimals."
end_seconds: Float
primary_tag: Tag!
tags: [Tag!]!
created_at: Time!
@@ -18,7 +21,10 @@ type SceneMarker {
input SceneMarkerCreateInput {
title: String!
"The required start time of the marker (in seconds). Supports decimals."
seconds: Float!
"The optional end time of the marker (in seconds). Supports decimals."
end_seconds: Float
scene_id: ID!
primary_tag_id: ID!
tag_ids: [ID!]
@@ -27,7 +33,10 @@ input SceneMarkerCreateInput {
input SceneMarkerUpdateInput {
id: ID!
title: String
"The start time of the marker (in seconds). Supports decimals."
seconds: Float
"The end time of the marker (in seconds). Supports decimals."
end_seconds: Float
scene_id: ID
primary_tag_id: ID
tag_ids: [ID!]

View File

@@ -26,6 +26,11 @@ type SceneMovie {
scene_index: Int
}
type SceneGroup {
group: Group!
scene_index: Int
}
type VideoCaption {
language_code: String!
caption_type: String!
@@ -68,7 +73,8 @@ type Scene {
scene_markers: [SceneMarker!]!
galleries: [Gallery!]!
studio: Studio
movies: [SceneMovie!]!
groups: [SceneGroup!]!
movies: [SceneMovie!]! @deprecated(reason: "Use groups")
tags: [Tag!]!
performers: [Performer!]!
stash_ids: [StashID!]!
@@ -82,6 +88,11 @@ input SceneMovieInput {
scene_index: Int
}
input SceneGroupInput {
group_id: ID!
scene_index: Int
}
input SceneCreateInput {
title: String
code: String
@@ -96,7 +107,8 @@ input SceneCreateInput {
studio_id: ID
gallery_ids: [ID!]
performer_ids: [ID!]
movies: [SceneMovieInput!]
groups: [SceneGroupInput!]
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
cover_image: String
@@ -128,7 +140,8 @@ input SceneUpdateInput {
studio_id: ID
gallery_ids: [ID!]
performer_ids: [ID!]
movies: [SceneMovieInput!]
groups: [SceneGroupInput!]
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
cover_image: String
@@ -175,7 +188,8 @@ input BulkSceneUpdateInput {
gallery_ids: BulkUpdateIds
performer_ids: BulkUpdateIds
tag_ids: BulkUpdateIds
movie_ids: BulkUpdateIds
group_ids: BulkUpdateIds
movie_ids: BulkUpdateIds @deprecated(reason: "Use group_ids")
}
input SceneDestroyInput {

View File

@@ -0,0 +1,65 @@
"A movie from a scraping operation..."
type ScrapedMovie {
stored_id: ID
name: String
aliases: String
duration: String
date: String
rating: String
director: String
url: String @deprecated(reason: "use urls")
urls: [String!]
synopsis: String
studio: ScrapedStudio
tags: [ScrapedTag!]
"This should be a base64 encoded data URL"
front_image: String
"This should be a base64 encoded data URL"
back_image: String
}
input ScrapedMovieInput {
name: String
aliases: String
duration: String
date: String
rating: String
director: String
url: String @deprecated(reason: "use urls")
urls: [String!]
synopsis: String
# not including tags for the input
}
"A group from a scraping operation..."
type ScrapedGroup {
stored_id: ID
name: String
aliases: String
duration: String
date: String
rating: String
director: String
urls: [String!]
synopsis: String
studio: ScrapedStudio
tags: [ScrapedTag!]
"This should be a base64 encoded data URL"
front_image: String
"This should be a base64 encoded data URL"
back_image: String
}
input ScrapedGroupInput {
name: String
aliases: String
duration: String
date: String
rating: String
director: String
urls: [String!]
synopsis: String
# not including tags for the input
}

View File

@@ -1,29 +0,0 @@
"A movie from a scraping operation..."
type ScrapedMovie {
stored_id: ID
name: String
aliases: String
duration: String
date: String
rating: String
director: String
url: String
synopsis: String
studio: ScrapedStudio
"This should be a base64 encoded data URL"
front_image: String
"This should be a base64 encoded data URL"
back_image: String
}
input ScrapedMovieInput {
name: String
aliases: String
duration: String
date: String
rating: String
director: String
url: String
synopsis: String
}

View File

@@ -5,9 +5,10 @@ type ScrapedPerformer {
name: String
disambiguation: String
gender: String
url: String
twitter: String
instagram: String
url: String @deprecated(reason: "use urls")
urls: [String!]
twitter: String @deprecated(reason: "use urls")
instagram: String @deprecated(reason: "use urls")
birthdate: String
ethnicity: String
country: String
@@ -40,9 +41,10 @@ input ScrapedPerformerInput {
name: String
disambiguation: String
gender: String
url: String
twitter: String
instagram: String
url: String @deprecated(reason: "use urls")
urls: [String!]
twitter: String @deprecated(reason: "use urls")
instagram: String @deprecated(reason: "use urls")
birthdate: String
ethnicity: String
country: String

View File

@@ -10,7 +10,9 @@ enum ScrapeType {
"Type of the content a scraper generates"
enum ScrapeContentType {
GALLERY
IMAGE
MOVIE
GROUP
PERFORMER
SCENE
}
@@ -21,7 +23,9 @@ union ScrapedContent =
| ScrapedTag
| ScrapedScene
| ScrapedGallery
| ScrapedImage
| ScrapedMovie
| ScrapedGroup
| ScrapedPerformer
type ScraperSpec {
@@ -39,8 +43,12 @@ type Scraper {
scene: ScraperSpec
"Details for gallery scraper"
gallery: ScraperSpec
"Details for image scraper"
image: ScraperSpec
"Details for movie scraper"
movie: ScraperSpec
movie: ScraperSpec @deprecated(reason: "use group")
"Details for group scraper"
group: ScraperSpec
}
type ScrapedStudio {
@@ -76,7 +84,8 @@ type ScrapedScene {
studio: ScrapedStudio
tags: [ScrapedTag!]
performers: [ScrapedPerformer!]
movies: [ScrapedMovie!]
movies: [ScrapedMovie!] @deprecated(reason: "use groups")
groups: [ScrapedGroup!]
remote_site_id: String
duration: Int
@@ -123,12 +132,32 @@ input ScrapedGalleryInput {
# no studio, tags or performers
}
type ScrapedImage {
title: String
code: String
details: String
photographer: String
urls: [String!]
date: String
studio: ScrapedStudio
tags: [ScrapedTag!]
performers: [ScrapedPerformer!]
}
input ScrapedImageInput {
title: String
code: String
details: String
urls: [String!]
date: String
}
input ScraperSourceInput {
"Index of the configured stash-box instance to use. Should be unset if scraper_id is set"
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
"Stash-box endpoint"
stash_box_endpoint: String
"Scraper ID to scrape with. Should be unset if stash_box_index is set"
"Scraper ID to scrape with. Should be unset if stash_box_endpoint/stash_box_index is set"
scraper_id: ID
}
@@ -137,7 +166,7 @@ type ScraperSource {
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
"Stash-box endpoint"
stash_box_endpoint: String
"Scraper ID to scrape with. Should be unset if stash_box_index is set"
"Scraper ID to scrape with. Should be unset if stash_box_endpoint/stash_box_index is set"
scraper_id: ID
}
@@ -185,18 +214,38 @@ input ScrapeSingleGalleryInput {
gallery_input: ScrapedGalleryInput
}
input ScrapeSingleImageInput {
"Instructs to query by string"
query: String
"Instructs to query by image id"
image_id: ID
"Instructs to query by image fragment"
image_input: ScrapedImageInput
}
input ScrapeSingleMovieInput {
"Instructs to query by string"
query: String
"Instructs to query by movie id"
movie_id: ID
"Instructs to query by gallery fragment"
"Instructs to query by movie fragment"
movie_input: ScrapedMovieInput
}
input ScrapeSingleGroupInput {
"Instructs to query by string"
query: String
"Instructs to query by group id"
group_id: ID
"Instructs to query by group fragment"
group_input: ScrapedGroupInput
}
input StashBoxSceneQueryInput {
"Index of the configured stash-box instance to use"
stash_box_index: Int!
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
"Endpoint of the stash-box instance to use"
stash_box_endpoint: String
"Instructs query by scene fingerprints"
scene_ids: [ID!]
"Query by query string"
@@ -205,7 +254,9 @@ input StashBoxSceneQueryInput {
input StashBoxPerformerQueryInput {
"Index of the configured stash-box instance to use"
stash_box_index: Int!
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
"Endpoint of the stash-box instance to use"
stash_box_endpoint: String
"Instructs query by scene fingerprints"
performer_ids: [ID!]
"Query by query string"
@@ -226,7 +277,9 @@ type StashBoxFingerprint {
"If neither ids nor names are set, tag all items"
input StashBoxBatchTagInput {
"Stash endpoint to use for the tagging"
endpoint: Int!
endpoint: Int @deprecated(reason: "use stash_box_endpoint")
"Endpoint of the stash-box instance to use"
stash_box_endpoint: String
"Fields to exclude when executing the tagging"
exclude_fields: [String!]
"Refresh items already tagged by StashBox if true. Only tag items with no StashBox tagging if false"

View File

@@ -2,30 +2,37 @@ type StashBox {
endpoint: String!
api_key: String!
name: String!
max_requests_per_minute: Int!
}
input StashBoxInput {
endpoint: String!
api_key: String!
name: String!
# defaults to 240
max_requests_per_minute: Int
}
type StashID {
endpoint: String!
stash_id: String!
updated_at: Time!
}
input StashIDInput {
endpoint: String!
stash_id: String!
updated_at: Time
}
input StashBoxFingerprintSubmissionInput {
scene_ids: [String!]!
stash_box_index: Int!
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
stash_box_endpoint: String
}
input StashBoxDraftSubmissionInput {
id: String!
stash_box_index: Int!
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
stash_box_endpoint: String
}

View File

@@ -7,7 +7,8 @@ type StatsResultType {
gallery_count: Int!
performer_count: Int!
studio_count: Int!
movie_count: Int!
group_count: Int!
movie_count: Int! @deprecated(reason: "use group_count instead")
tag_count: Int!
total_o_count: Int!
total_play_duration: Float!

View File

@@ -5,6 +5,7 @@ type Studio {
parent_studio: Studio
child_studios: [Studio!]!
aliases: [String!]!
tags: [Tag!]!
ignore_auto_tag: Boolean!
image_path: String # Resolver
@@ -12,7 +13,8 @@ type Studio {
image_count(depth: Int): Int! # Resolver
gallery_count(depth: Int): Int! # Resolver
performer_count(depth: Int): Int! # Resolver
movie_count(depth: Int): Int! # Resolver
group_count(depth: Int): Int! # Resolver
movie_count(depth: Int): Int! @deprecated(reason: "use group_count instead") # Resolver
stash_ids: [StashID!]!
# rating expressed as 1-100
rating100: Int
@@ -20,7 +22,8 @@ type Studio {
details: String
created_at: Time!
updated_at: Time!
movies: [Movie!]!
groups: [Group!]!
movies: [Movie!]! @deprecated(reason: "use groups instead")
}
input StudioCreateInput {
@@ -35,6 +38,7 @@ input StudioCreateInput {
favorite: Boolean
details: String
aliases: [String!]
tag_ids: [ID!]
ignore_auto_tag: Boolean
}
@@ -51,6 +55,7 @@ input StudioUpdateInput {
favorite: Boolean
details: String
aliases: [String!]
tag_ids: [ID!]
ignore_auto_tag: Boolean
}

View File

@@ -1,6 +1,8 @@
type Tag {
id: ID!
name: String!
"Value that does not appear in the UI but overrides name for sorting"
sort_name: String
description: String
aliases: [String!]!
ignore_auto_tag: Boolean!
@@ -13,6 +15,9 @@ type Tag {
image_count(depth: Int): Int! # Resolver
gallery_count(depth: Int): Int! # Resolver
performer_count(depth: Int): Int! # Resolver
studio_count(depth: Int): Int! # Resolver
group_count(depth: Int): Int! # Resolver
movie_count(depth: Int): Int! @deprecated(reason: "use group_count instead") # Resolver
parents: [Tag!]!
children: [Tag!]!
@@ -22,6 +27,8 @@ type Tag {
input TagCreateInput {
name: String!
"Value that does not appear in the UI but overrides name for sorting"
sort_name: String
description: String
aliases: [String!]
ignore_auto_tag: Boolean
@@ -36,6 +43,8 @@ input TagCreateInput {
input TagUpdateInput {
id: ID!
name: String
"Value that does not appear in the UI but overrides name for sorting"
sort_name: String
description: String
aliases: [String!]
ignore_auto_tag: Boolean
@@ -60,3 +69,14 @@ input TagsMergeInput {
source: [ID!]!
destination: ID!
}
input BulkTagUpdateInput {
ids: [ID!]
description: String
aliases: BulkUpdateStrings
ignore_auto_tag: Boolean
favorite: Boolean
parent_ids: BulkUpdateIds
child_ids: BulkUpdateIds
}

View File

@@ -30,11 +30,6 @@ fragment TagFragment on Tag {
id
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
@@ -54,15 +49,16 @@ fragment PerformerFragment on Performer {
aliases
gender
merged_ids
deleted
merged_into_id
urls {
...URLFragment
}
images {
...ImageFragment
}
birthdate {
...FuzzyDateFragment
}
birth_date
death_date
ethnicity
country
eye_color

View File

@@ -16,12 +16,12 @@ import (
const (
tripwireActivatedErrMsg = "Stash is exposed to the public internet without authentication, and is not serving any more content to protect your privacy. " +
"More information and fixes are available at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
"More information and fixes are available at https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet"
externalAccessErrMsg = "You have attempted to access Stash over the internet, and authentication is not enabled. " +
"This is extremely dangerous! The whole world can see your your stash page and browse your files! " +
"Stash is not answering any other requests to protect your privacy. " +
"Please read the log entry or visit https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
"Please read the log entry or visit https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet"
)
func allowUnauthenticated(r *http.Request) bool {

View File

@@ -335,43 +335,86 @@ func (t changesetTranslator) updateStringsBulk(value *BulkUpdateStrings, field s
}
}
func (t changesetTranslator) updateStashIDs(value []models.StashID, field string) *models.UpdateStashIDs {
func (t changesetTranslator) updateStashIDs(value models.StashIDInputs, field string) *models.UpdateStashIDs {
if !t.hasField(field) {
return nil
}
return &models.UpdateStashIDs{
StashIDs: value,
StashIDs: value.ToStashIDs(),
Mode: models.RelationshipUpdateModeSet,
}
}
func (t changesetTranslator) relatedMovies(value []models.SceneMovieInput) (models.RelatedMovies, error) {
moviesScenes, err := models.MoviesScenesFromInput(value)
func (t changesetTranslator) relatedGroupsFromMovies(value []models.SceneMovieInput) (models.RelatedGroups, error) {
groupsScenes, err := models.GroupsScenesFromInput(value)
if err != nil {
return models.RelatedMovies{}, err
return models.RelatedGroups{}, err
}
return models.NewRelatedMovies(moviesScenes), nil
return models.NewRelatedGroups(groupsScenes), nil
}
func (t changesetTranslator) updateMovieIDs(value []models.SceneMovieInput, field string) (*models.UpdateMovieIDs, error) {
func groupsScenesFromGroupInput(input []models.SceneGroupInput) ([]models.GroupsScenes, error) {
ret := make([]models.GroupsScenes, len(input))
for i, v := range input {
mID, err := strconv.Atoi(v.GroupID)
if err != nil {
return nil, fmt.Errorf("invalid group ID: %s", v.GroupID)
}
ret[i] = models.GroupsScenes{
GroupID: mID,
SceneIndex: v.SceneIndex,
}
}
return ret, nil
}
func (t changesetTranslator) relatedGroups(value []models.SceneGroupInput) (models.RelatedGroups, error) {
groupsScenes, err := groupsScenesFromGroupInput(value)
if err != nil {
return models.RelatedGroups{}, err
}
return models.NewRelatedGroups(groupsScenes), nil
}
func (t changesetTranslator) updateGroupIDsFromMovies(value []models.SceneMovieInput, field string) (*models.UpdateGroupIDs, error) {
if !t.hasField(field) {
return nil, nil
}
moviesScenes, err := models.MoviesScenesFromInput(value)
groupsScenes, err := models.GroupsScenesFromInput(value)
if err != nil {
return nil, err
}
return &models.UpdateMovieIDs{
Movies: moviesScenes,
return &models.UpdateGroupIDs{
Groups: groupsScenes,
Mode: models.RelationshipUpdateModeSet,
}, nil
}
func (t changesetTranslator) updateMovieIDsBulk(value *BulkUpdateIds, field string) (*models.UpdateMovieIDs, error) {
func (t changesetTranslator) updateGroupIDs(value []models.SceneGroupInput, field string) (*models.UpdateGroupIDs, error) {
if !t.hasField(field) {
return nil, nil
}
groupsScenes, err := groupsScenesFromGroupInput(value)
if err != nil {
return nil, err
}
return &models.UpdateGroupIDs{
Groups: groupsScenes,
Mode: models.RelationshipUpdateModeSet,
}, nil
}
func (t changesetTranslator) updateGroupIDsBulk(value *BulkUpdateIds, field string) (*models.UpdateGroupIDs, error) {
if !t.hasField(field) || value == nil {
return nil, nil
}
@@ -381,13 +424,74 @@ func (t changesetTranslator) updateMovieIDsBulk(value *BulkUpdateIds, field stri
return nil, fmt.Errorf("converting ids [%v]: %w", value.Ids, err)
}
movies := make([]models.MoviesScenes, len(ids))
groups := make([]models.GroupsScenes, len(ids))
for i, id := range ids {
movies[i] = models.MoviesScenes{MovieID: id}
groups[i] = models.GroupsScenes{GroupID: id}
}
return &models.UpdateMovieIDs{
Movies: movies,
return &models.UpdateGroupIDs{
Groups: groups,
Mode: value.Mode,
}, nil
}
func groupsDescriptionsFromGroupInput(input []*GroupDescriptionInput) ([]models.GroupIDDescription, error) {
ret := make([]models.GroupIDDescription, len(input))
for i, v := range input {
gID, err := strconv.Atoi(v.GroupID)
if err != nil {
return nil, fmt.Errorf("invalid group ID: %s", v.GroupID)
}
ret[i] = models.GroupIDDescription{
GroupID: gID,
}
if v.Description != nil {
ret[i].Description = *v.Description
}
}
return ret, nil
}
func (t changesetTranslator) groupIDDescriptions(value []*GroupDescriptionInput) (models.RelatedGroupDescriptions, error) {
groupsScenes, err := groupsDescriptionsFromGroupInput(value)
if err != nil {
return models.RelatedGroupDescriptions{}, err
}
return models.NewRelatedGroupDescriptions(groupsScenes), nil
}
func (t changesetTranslator) updateGroupIDDescriptions(value []*GroupDescriptionInput, field string) (*models.UpdateGroupDescriptions, error) {
if !t.hasField(field) {
return nil, nil
}
groupsScenes, err := groupsDescriptionsFromGroupInput(value)
if err != nil {
return nil, err
}
return &models.UpdateGroupDescriptions{
Groups: groupsScenes,
Mode: models.RelationshipUpdateModeSet,
}, nil
}
func (t changesetTranslator) updateGroupIDDescriptionsBulk(value *BulkUpdateGroupDescriptionsInput, field string) (*models.UpdateGroupDescriptions, error) {
if !t.hasField(field) || value == nil {
return nil, nil
}
groups, err := groupsDescriptionsFromGroupInput(value.Groups)
if err != nil {
return nil, err
}
return &models.UpdateGroupDescriptions{
Groups: groups,
Mode: value.Mode,
}, nil
}

View File

@@ -5,11 +5,11 @@ package api
type key int
const (
// galleryKey key = 0
performerKey key = iota + 1
galleryKey key = 0
performerKey
sceneKey
studioKey
movieKey
groupKey
tagKey
downloadKey
imageKey

2
internal/api/doc.go Normal file
View File

@@ -0,0 +1,2 @@
// Package api provides the HTTP and Graphql API for the application.
package api

65
internal/api/json.go Normal file
View File

@@ -0,0 +1,65 @@
package api
import (
"encoding/json"
"strings"
"github.com/stashapp/stash/pkg/models"
)
// jsonNumberToNumber converts a JSON number to either a float64 or int64.
func jsonNumberToNumber(n json.Number) interface{} {
if strings.Contains(string(n), ".") {
f, _ := n.Float64()
return f
}
ret, _ := n.Int64()
return ret
}
// anyJSONNumberToNumber converts a JSON number using jsonNumberToNumber, otherwise it returns the existing value
func anyJSONNumberToNumber(v any) any {
if n, ok := v.(json.Number); ok {
return jsonNumberToNumber(n)
}
return v
}
// ConvertMapJSONNumbers converts all JSON numbers in a map to either float64 or int64.
func convertMapJSONNumbers(m map[string]interface{}) (ret map[string]interface{}) {
if m == nil {
return nil
}
ret = make(map[string]interface{})
for k, v := range m {
if n, ok := v.(json.Number); ok {
ret[k] = jsonNumberToNumber(n)
} else if mm, ok := v.(map[string]interface{}); ok {
ret[k] = convertMapJSONNumbers(mm)
} else {
ret[k] = v
}
}
return ret
}
func convertCustomFieldCriterionValues(c models.CustomFieldCriterionInput) models.CustomFieldCriterionInput {
nv := make([]any, len(c.Value))
for i, v := range c.Value {
nv[i] = anyJSONNumberToNumber(v)
}
c.Value = nv
return c
}
func convertCustomFieldCriterionInputJSONNumbers(c []models.CustomFieldCriterionInput) []models.CustomFieldCriterionInput {
ret := make([]models.CustomFieldCriterionInput, len(c))
for i, v := range c {
ret[i] = convertCustomFieldCriterionValues(v)
}
return ret
}

60
internal/api/json_test.go Normal file
View File

@@ -0,0 +1,60 @@
package api
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConvertMapJSONNumbers(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
expected map[string]interface{}
}{
{
name: "Convert JSON numbers to numbers",
input: map[string]interface{}{
"int": json.Number("12"),
"float": json.Number("12.34"),
"string": "foo",
},
expected: map[string]interface{}{
"int": int64(12),
"float": 12.34,
"string": "foo",
},
},
{
name: "Convert JSON numbers to numbers in nested maps",
input: map[string]interface{}{
"foo": map[string]interface{}{
"int": json.Number("56"),
"float": json.Number("56.78"),
"nested-string": "bar",
},
"int": json.Number("12"),
"float": json.Number("12.34"),
"string": "foo",
},
expected: map[string]interface{}{
"foo": map[string]interface{}{
"int": int64(56),
"float": 56.78,
"nested-string": "bar",
},
"int": int64(12),
"float": 12.34,
"string": "foo",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := convertMapJSONNumbers(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}

View File

@@ -0,0 +1,221 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// CustomFieldsLoaderConfig captures the config to create a new CustomFieldsLoader
type CustomFieldsLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]models.CustomFieldMap, []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
}
// NewCustomFieldsLoader creates a new CustomFieldsLoader given a fetch, wait, and maxBatch
func NewCustomFieldsLoader(config CustomFieldsLoaderConfig) *CustomFieldsLoader {
return &CustomFieldsLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// CustomFieldsLoader batches and caches requests
type CustomFieldsLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]models.CustomFieldMap, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]models.CustomFieldMap
// 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 *customFieldsLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type customFieldsLoaderBatch struct {
keys []int
data []models.CustomFieldMap
error []error
closing bool
done chan struct{}
}
// Load a CustomFieldMap by key, batching and caching will be applied automatically
func (l *CustomFieldsLoader) Load(key int) (models.CustomFieldMap, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a CustomFieldMap.
// 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 *CustomFieldsLoader) LoadThunk(key int) func() (models.CustomFieldMap, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (models.CustomFieldMap, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &customFieldsLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (models.CustomFieldMap, error) {
<-batch.done
var data models.CustomFieldMap
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 *CustomFieldsLoader) LoadAll(keys []int) ([]models.CustomFieldMap, []error) {
results := make([]func() (models.CustomFieldMap, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
customFieldMaps := make([]models.CustomFieldMap, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
customFieldMaps[i], errors[i] = thunk()
}
return customFieldMaps, errors
}
// LoadAllThunk returns a function that when called will block waiting for a CustomFieldMaps.
// 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 *CustomFieldsLoader) LoadAllThunk(keys []int) func() ([]models.CustomFieldMap, []error) {
results := make([]func() (models.CustomFieldMap, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]models.CustomFieldMap, []error) {
customFieldMaps := make([]models.CustomFieldMap, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
customFieldMaps[i], errors[i] = thunk()
}
return customFieldMaps, 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 *CustomFieldsLoader) Prime(key int, value models.CustomFieldMap) 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 *CustomFieldsLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *CustomFieldsLoader) unsafeSet(key int, value models.CustomFieldMap) {
if l.cache == nil {
l.cache = map[int]models.CustomFieldMap{}
}
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 *customFieldsLoaderBatch) keyIndex(l *CustomFieldsLoader, 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 *customFieldsLoaderBatch) startTimer(l *CustomFieldsLoader) {
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 *customFieldsLoaderBatch) end(l *CustomFieldsLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -1,14 +1,19 @@
// Package loaders contains the dataloaders used by the resolver in [api].
// They are generated with `make generate-dataloaders`.
// The dataloaders are used to batch requests to the database.
//go:generate go run github.com/vektah/dataloaden SceneLoader int *github.com/stashapp/stash/pkg/models.Scene
//go:generate go run github.com/vektah/dataloaden GalleryLoader int *github.com/stashapp/stash/pkg/models.Gallery
//go:generate go run github.com/vektah/dataloaden ImageLoader int *github.com/stashapp/stash/pkg/models.Image
//go:generate go run github.com/vektah/dataloaden PerformerLoader int *github.com/stashapp/stash/pkg/models.Performer
//go:generate go run github.com/vektah/dataloaden StudioLoader int *github.com/stashapp/stash/pkg/models.Studio
//go:generate go run github.com/vektah/dataloaden TagLoader int *github.com/stashapp/stash/pkg/models.Tag
//go:generate go run github.com/vektah/dataloaden MovieLoader int *github.com/stashapp/stash/pkg/models.Movie
//go:generate go run github.com/vektah/dataloaden GroupLoader int *github.com/stashapp/stash/pkg/models.Group
//go:generate go run github.com/vektah/dataloaden FileLoader github.com/stashapp/stash/pkg/models.FileID github.com/stashapp/stash/pkg/models.File
//go:generate go run github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
//go:generate go run github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
//go:generate go run github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
//go:generate go run github.com/vektah/dataloaden CustomFieldsLoader int github.com/stashapp/stash/pkg/models.CustomFieldMap
//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
@@ -47,13 +52,16 @@ type Loaders struct {
ImageFiles *ImageFileIDsLoader
GalleryFiles *GalleryFileIDsLoader
GalleryByID *GalleryLoader
ImageByID *ImageLoader
PerformerByID *PerformerLoader
StudioByID *StudioLoader
TagByID *TagLoader
MovieByID *MovieLoader
FileByID *FileLoader
GalleryByID *GalleryLoader
ImageByID *ImageLoader
PerformerByID *PerformerLoader
PerformerCustomFields *CustomFieldsLoader
StudioByID *StudioLoader
TagByID *TagLoader
GroupByID *GroupLoader
FileByID *FileLoader
}
type Middleware struct {
@@ -84,6 +92,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
maxBatch: maxBatch,
fetch: m.fetchPerformers(ctx),
},
PerformerCustomFields: &CustomFieldsLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchPerformerCustomFields(ctx),
},
StudioByID: &StudioLoader{
wait: wait,
maxBatch: maxBatch,
@@ -94,10 +107,10 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
maxBatch: maxBatch,
fetch: m.fetchTags(ctx),
},
MovieByID: &MovieLoader{
GroupByID: &GroupLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchMovies(ctx),
fetch: m.fetchGroups(ctx),
},
FileByID: &FileLoader{
wait: wait,
@@ -210,6 +223,18 @@ func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*mo
}
}
func (m Middleware) fetchPerformerCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Performer.GetCustomFieldsBulk(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*models.Studio, []error) {
return func(keys []int) (ret []*models.Studio, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
@@ -232,11 +257,11 @@ 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) {
func (m Middleware) fetchGroups(ctx context.Context) func(keys []int) ([]*models.Group, []error) {
return func(keys []int) (ret []*models.Group, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Movie.FindMany(ctx, keys)
ret, err = m.Repository.Group.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)

View File

@@ -9,10 +9,10 @@ import (
"github.com/stashapp/stash/pkg/models"
)
// MovieLoaderConfig captures the config to create a new MovieLoader
type MovieLoaderConfig struct {
// GroupLoaderConfig captures the config to create a new GroupLoader
type GroupLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Movie, []error)
Fetch func(keys []int) ([]*models.Group, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
@@ -21,19 +21,19 @@ type MovieLoaderConfig struct {
MaxBatch int
}
// NewMovieLoader creates a new MovieLoader given a fetch, wait, and maxBatch
func NewMovieLoader(config MovieLoaderConfig) *MovieLoader {
return &MovieLoader{
// NewGroupLoader creates a new GroupLoader given a fetch, wait, and maxBatch
func NewGroupLoader(config GroupLoaderConfig) *GroupLoader {
return &GroupLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// MovieLoader batches and caches requests
type MovieLoader struct {
// GroupLoader batches and caches requests
type GroupLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Movie, []error)
fetch func(keys []int) ([]*models.Group, []error)
// how long to done before sending a batch
wait time.Duration
@@ -44,51 +44,51 @@ type MovieLoader struct {
// INTERNAL
// lazily created cache
cache map[int]*models.Movie
cache map[int]*models.Group
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *movieLoaderBatch
batch *groupLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type movieLoaderBatch struct {
type groupLoaderBatch struct {
keys []int
data []*models.Movie
data []*models.Group
error []error
closing bool
done chan struct{}
}
// Load a Movie by key, batching and caching will be applied automatically
func (l *MovieLoader) Load(key int) (*models.Movie, error) {
// Load a Group by key, batching and caching will be applied automatically
func (l *GroupLoader) Load(key int) (*models.Group, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Movie.
// LoadThunk returns a function that when called will block waiting for a Group.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *MovieLoader) LoadThunk(key int) func() (*models.Movie, error) {
func (l *GroupLoader) LoadThunk(key int) func() (*models.Group, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Movie, error) {
return func() (*models.Group, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &movieLoaderBatch{done: make(chan struct{})}
l.batch = &groupLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Movie, error) {
return func() (*models.Group, error) {
<-batch.done
var data *models.Movie
var data *models.Group
if pos < len(batch.data) {
data = batch.data[pos]
}
@@ -113,43 +113,43 @@ func (l *MovieLoader) LoadThunk(key int) func() (*models.Movie, error) {
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *MovieLoader) LoadAll(keys []int) ([]*models.Movie, []error) {
results := make([]func() (*models.Movie, error), len(keys))
func (l *GroupLoader) LoadAll(keys []int) ([]*models.Group, []error) {
results := make([]func() (*models.Group, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
movies := make([]*models.Movie, len(keys))
groups := make([]*models.Group, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
movies[i], errors[i] = thunk()
groups[i], errors[i] = thunk()
}
return movies, errors
return groups, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Movies.
// LoadAllThunk returns a function that when called will block waiting for a Groups.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *MovieLoader) LoadAllThunk(keys []int) func() ([]*models.Movie, []error) {
results := make([]func() (*models.Movie, error), len(keys))
func (l *GroupLoader) LoadAllThunk(keys []int) func() ([]*models.Group, []error) {
results := make([]func() (*models.Group, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Movie, []error) {
movies := make([]*models.Movie, len(keys))
return func() ([]*models.Group, []error) {
groups := make([]*models.Group, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
movies[i], errors[i] = thunk()
groups[i], errors[i] = thunk()
}
return movies, errors
return groups, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *MovieLoader) Prime(key int, value *models.Movie) bool {
func (l *GroupLoader) Prime(key int, value *models.Group) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
@@ -163,22 +163,22 @@ func (l *MovieLoader) Prime(key int, value *models.Movie) bool {
}
// Clear the value at key from the cache, if it exists
func (l *MovieLoader) Clear(key int) {
func (l *GroupLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *MovieLoader) unsafeSet(key int, value *models.Movie) {
func (l *GroupLoader) unsafeSet(key int, value *models.Group) {
if l.cache == nil {
l.cache = map[int]*models.Movie{}
l.cache = map[int]*models.Group{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *movieLoaderBatch) keyIndex(l *MovieLoader, key int) int {
func (b *groupLoaderBatch) keyIndex(l *GroupLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
@@ -202,7 +202,7 @@ func (b *movieLoaderBatch) keyIndex(l *MovieLoader, key int) int {
return pos
}
func (b *movieLoaderBatch) startTimer(l *MovieLoader) {
func (b *groupLoaderBatch) startTimer(l *GroupLoader) {
time.Sleep(l.wait)
l.mu.Lock()
@@ -218,7 +218,7 @@ func (b *movieLoaderBatch) startTimer(l *MovieLoader) {
b.end(l)
}
func (b *movieLoaderBatch) end(l *MovieLoader) {
func (b *groupLoaderBatch) end(l *GroupLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/scraper"
"github.com/stashapp/stash/pkg/scraper/stashbox"
)
var (
@@ -37,6 +36,7 @@ type Resolver struct {
sceneService manager.SceneService
imageService manager.ImageService
galleryService manager.GalleryService
groupService manager.GroupService
hookExecutor hookExecutor
}
@@ -72,9 +72,14 @@ func (r *Resolver) SceneMarker() SceneMarkerResolver {
func (r *Resolver) Studio() StudioResolver {
return &studioResolver{r}
}
func (r *Resolver) Movie() MovieResolver {
return &movieResolver{r}
func (r *Resolver) Group() GroupResolver {
return &groupResolver{r}
}
func (r *Resolver) Movie() MovieResolver {
return &movieResolver{&groupResolver{r}}
}
func (r *Resolver) Subscription() SubscriptionResolver {
return &subscriptionResolver{r}
}
@@ -111,7 +116,11 @@ type sceneResolver struct{ *Resolver }
type sceneMarkerResolver struct{ *Resolver }
type imageResolver struct{ *Resolver }
type studioResolver struct{ *Resolver }
type movieResolver struct{ *Resolver }
// movie is group under the hood
type groupResolver struct{ *Resolver }
type movieResolver struct{ *groupResolver }
type tagResolver struct{ *Resolver }
type galleryFileResolver struct{ *Resolver }
type videoFileResolver struct{ *Resolver }
@@ -128,10 +137,6 @@ func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context)
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) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SceneMarker.Wall(ctx, q)
@@ -173,7 +178,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
galleryQB := repo.Gallery
studioQB := repo.Studio
performerQB := repo.Performer
movieQB := repo.Movie
movieQB := repo.Group
tagQB := repo.Tag
// embrace the error
@@ -218,7 +223,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
return err
}
moviesCount, err := movieQB.Count(ctx)
groupsCount, err := movieQB.Count(ctx)
if err != nil {
return err
}
@@ -262,7 +267,8 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
GalleryCount: galleryCount,
PerformerCount: performersCount,
StudioCount: studiosCount,
MovieCount: moviesCount,
GroupCount: groupsCount,
MovieCount: groupsCount,
TagCount: tagsCount,
TotalOCount: totalOCount,
TotalPlayDuration: totalPlayDuration,

View File

@@ -2,8 +2,10 @@ package api
import (
"context"
"fmt"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/image"
@@ -189,3 +191,28 @@ func (r *galleryResolver) Urls(ctx context.Context, obj *models.Gallery) ([]stri
return obj.URLs.List(), nil
}
func (r *galleryResolver) Paths(ctx context.Context, obj *models.Gallery) (*GalleryPathsType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewGalleryURLBuilder(baseURL, obj)
return &GalleryPathsType{
Cover: builder.GetCoverURL(),
Preview: builder.GetPreviewURL(),
}, nil
}
func (r *galleryResolver) Image(ctx context.Context, obj *models.Gallery, index int) (ret *models.Image, err error) {
if index < 0 {
return nil, fmt.Errorf("index must >= 0")
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Image.FindByGalleryIDIndex(ctx, obj.ID, uint(index))
return err
}); err != nil {
return nil, err
}
return
}

View File

@@ -18,11 +18,6 @@ func (r *imageResolver) getFiles(ctx context.Context, obj *models.Image) ([]mode
return files, firstError(errs)
}
func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string, error) {
ret := obj.GetTitle()
return &ret, nil
}
func (r *imageResolver) VisualFiles(ctx context.Context, obj *models.Image) ([]VisualFile, error) {
files, err := r.getFiles(ctx, obj)
if err != nil {

View File

@@ -5,10 +5,12 @@ import (
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/group"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
)
func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, error) {
func (r *groupResolver) Date(ctx context.Context, obj *models.Group) (*string, error) {
if obj.Date != nil {
result := obj.Date.String()
return &result, nil
@@ -16,11 +18,40 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e
return nil, nil
}
func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) {
func (r *groupResolver) Rating100(ctx context.Context, obj *models.Group) (*int, error) {
return obj.Rating, nil
}
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *models.Studio, err error) {
func (r *groupResolver) URL(ctx context.Context, obj *models.Group) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Group)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
if len(urls) == 0 {
return nil, nil
}
return &urls[0], nil
}
func (r *groupResolver) Urls(ctx context.Context, obj *models.Group) ([]string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Group)
}); err != nil {
return nil, err
}
}
return obj.URLs.List(), nil
}
func (r *groupResolver) Studio(ctx context.Context, obj *models.Group) (ret *models.Studio, err error) {
if obj.StudioID == nil {
return nil, nil
}
@@ -28,26 +59,102 @@ func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *mod
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
}
func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
func (r groupResolver) Tags(ctx context.Context, obj *models.Group) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Group)
}); err != nil {
return nil, err
}
}
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
return ret, firstError(errs)
}
func (r groupResolver) relatedGroups(ctx context.Context, rgd models.RelatedGroupDescriptions) (ret []*GroupDescription, err error) {
// rgd must be loaded
gds := rgd.List()
ids := make([]int, len(gds))
for i, gd := range gds {
ids[i] = gd.GroupID
}
groups, errs := loaders.From(ctx).GroupByID.LoadAll(ids)
err = firstError(errs)
if err != nil {
return
}
ret = make([]*GroupDescription, len(groups))
for i, group := range groups {
ret[i] = &GroupDescription{Group: group}
d := gds[i].Description
if d != "" {
ret[i].Description = &d
}
}
return ret, firstError(errs)
}
func (r groupResolver) ContainingGroups(ctx context.Context, obj *models.Group) (ret []*GroupDescription, err error) {
if !obj.ContainingGroups.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadContainingGroupIDs(ctx, r.repository.Group)
}); err != nil {
return nil, err
}
}
return r.relatedGroups(ctx, obj.ContainingGroups)
}
func (r groupResolver) SubGroups(ctx context.Context, obj *models.Group) (ret []*GroupDescription, err error) {
if !obj.SubGroups.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadSubGroupIDs(ctx, r.repository.Group)
}); err != nil {
return nil, err
}
}
return r.relatedGroups(ctx, obj.SubGroups)
}
func (r *groupResolver) SubGroupCount(ctx context.Context, obj *models.Group, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = group.CountByContainingGroupID(ctx, r.repository.Group, obj.ID, depth)
return err
}); err != nil {
return 0, err
}
return ret, nil
}
func (r *groupResolver) FrontImagePath(ctx context.Context, obj *models.Group) (*string, error) {
var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
hasImage, err = r.repository.Movie.HasFrontImage(ctx, obj.ID)
hasImage, err = r.repository.Group.HasFrontImage(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
imagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieFrontImageURL(hasImage)
imagePath := urlbuilders.NewGroupURLBuilder(baseURL, obj).GetGroupFrontImageURL(hasImage)
return &imagePath, nil
}
func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
func (r *groupResolver) BackImagePath(ctx context.Context, obj *models.Group) (*string, error) {
var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
hasImage, err = r.repository.Movie.HasBackImage(ctx, obj.ID)
hasImage, err = r.repository.Group.HasBackImage(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -59,13 +166,13 @@ func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*
}
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
imagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieBackImageURL()
imagePath := urlbuilders.NewGroupURLBuilder(baseURL, obj).GetGroupBackImageURL()
return &imagePath, nil
}
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret int, err error) {
func (r *groupResolver) SceneCount(ctx context.Context, obj *models.Group, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Scene.CountByMovieID(ctx, obj.ID)
ret, err = scene.CountByGroupID(ctx, r.repository.Scene, obj.ID, depth)
return err
}); err != nil {
return 0, err
@@ -74,10 +181,10 @@ func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret
return ret, nil
}
func (r *movieResolver) Scenes(ctx context.Context, obj *models.Movie) (ret []*models.Scene, err error) {
func (r *groupResolver) Scenes(ctx context.Context, obj *models.Group) (ret []*models.Scene, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Scene.FindByMovieID(ctx, obj.ID)
ret, err = r.repository.Scene.FindByGroupID(ctx, obj.ID)
return err
}); err != nil {
return nil, err

View File

@@ -24,6 +24,79 @@ func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer
return obj.Aliases.List(), nil
}
func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
if len(urls) == 0 {
return nil, nil
}
return &urls[0], nil
}
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
// find the first twitter url
for _, url := range urls {
if performer.IsTwitterURL(url) {
u := url
return &u, nil
}
}
return nil, nil
}
func (r *performerResolver) Instagram(ctx context.Context, obj *models.Performer) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
// find the first instagram url
for _, url := range urls {
if performer.IsInstagramURL(url) {
u := url
return &u, nil
}
}
return nil, nil
}
func (r *performerResolver) Urls(ctx context.Context, obj *models.Performer) ([]string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
return obj.URLs.List(), nil
}
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Height != nil {
ret := strconv.Itoa(*obj.Height)
@@ -106,9 +179,9 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
return ret, nil
}
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
func (r *performerResolver) GroupCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
ret, err = r.repository.Group.CountByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return 0, err
@@ -117,6 +190,11 @@ func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performe
return ret, nil
}
// deprecated
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
return r.GroupCount(ctx, obj)
}
func (r *performerResolver) PerformerCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = performer.CountByAppearsWith(ctx, r.repository.Performer, obj.ID)
@@ -179,9 +257,9 @@ func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer
return nil, nil
}
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
func (r *performerResolver) Groups(ctx context.Context, obj *models.Performer) (ret []*models.Group, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
ret, err = r.repository.Group.FindByPerformerID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -189,3 +267,21 @@ func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (
return ret, nil
}
func (r *performerResolver) CustomFields(ctx context.Context, obj *models.Performer) (map[string]interface{}, error) {
m, err := loaders.From(ctx).PerformerCustomFields.Load(obj.ID)
if err != nil {
return nil, err
}
if m == nil {
return make(map[string]interface{}), nil
}
return m, nil
}
// deprecated
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Group, err error) {
return r.Groups(ctx, obj)
}

View File

@@ -184,20 +184,20 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod
}
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) {
if !obj.Movies.Loaded() {
if !obj.Groups.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
return obj.LoadMovies(ctx, qb)
return obj.LoadGroups(ctx, qb)
}); err != nil {
return nil, err
}
}
loader := loaders.From(ctx).MovieByID
loader := loaders.From(ctx).GroupByID
for _, sm := range obj.Movies.List() {
movie, err := loader.Load(sm.MovieID)
for _, sm := range obj.Groups.List() {
movie, err := loader.Load(sm.GroupID)
if err != nil {
return nil, err
}
@@ -214,6 +214,37 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S
return ret, nil
}
func (r *sceneResolver) Groups(ctx context.Context, obj *models.Scene) (ret []*SceneGroup, err error) {
if !obj.Groups.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
return obj.LoadGroups(ctx, qb)
}); err != nil {
return nil, err
}
}
loader := loaders.From(ctx).GroupByID
for _, sm := range obj.Groups.List() {
group, err := loader.Load(sm.GroupID)
if err != nil {
return nil, err
}
sceneIdx := sm.SceneIndex
sceneGroup := &SceneGroup{
Group: group,
SceneIndex: sceneIdx,
}
ret = append(ret, sceneGroup)
}
return ret, nil
}
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {

View File

@@ -6,9 +6,9 @@ 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/group"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/movie"
"github.com/stashapp/stash/pkg/performer"
"github.com/stashapp/stash/pkg/scene"
)
@@ -40,6 +40,20 @@ func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) ([]str
return obj.Aliases.List(), nil
}
func (r *studioResolver) Tags(ctx context.Context, obj *models.Studio) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Studio)
}); err != nil {
return nil, err
}
}
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
return ret, firstError(errs)
}
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = scene.CountByStudioID(ctx, r.repository.Scene, obj.ID, depth)
@@ -84,9 +98,9 @@ func (r *studioResolver) PerformerCount(ctx context.Context, obj *models.Studio,
return ret, nil
}
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
func (r *studioResolver) GroupCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = movie.CountByStudioID(ctx, r.repository.Movie, obj.ID, depth)
ret, err = group.CountByStudioID(ctx, r.repository.Group, obj.ID, depth)
return err
}); err != nil {
return 0, err
@@ -95,6 +109,11 @@ func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, dep
return ret, nil
}
// deprecated
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
return r.GroupCount(ctx, obj, depth)
}
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
if obj.ParentID == nil {
return nil, nil
@@ -130,9 +149,9 @@ func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*in
return obj.Rating, nil
}
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
func (r *studioResolver) Groups(ctx context.Context, obj *models.Studio) (ret []*models.Group, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
ret, err = r.repository.Group.FindByStudioID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
@@ -140,3 +159,8 @@ func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []
return ret, nil
}
// deprecated
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Group, err error) {
return r.Groups(ctx, obj)
}

View File

@@ -3,45 +3,55 @@ package api
import (
"context"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/group"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/performer"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/studio"
)
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByChildTagID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
if !obj.ParentIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadParentIDs(ctx, r.repository.Tag)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.ParentIDs.List())
return ret, firstError(errs)
}
func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByParentTagID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
if !obj.ChildIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadChildIDs(ctx, r.repository.Tag)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.ChildIDs.List())
return ret, firstError(errs)
}
func (r *tagResolver) Aliases(ctx context.Context, obj *models.Tag) (ret []string, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.GetAliases(ctx, obj.ID)
return err
}); err != nil {
return nil, err
if !obj.Aliases.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadAliases(ctx, r.repository.Tag)
}); err != nil {
return nil, err
}
}
return ret, err
return obj.Aliases.List(), nil
}
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
@@ -99,6 +109,32 @@ func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag, depth
return ret, nil
}
func (r *tagResolver) StudioCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = studio.CountByTagID(ctx, r.repository.Studio, obj.ID, depth)
return err
}); err != nil {
return 0, err
}
return ret, nil
}
func (r *tagResolver) GroupCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = group.CountByTagID(ctx, r.repository.Group, obj.ID, depth)
return err
}); err != nil {
return 0, err
}
return ret, nil
}
func (r *tagResolver) MovieCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
return r.GroupCount(ctx, obj, depth)
}
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {

View File

@@ -2,6 +2,7 @@ package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"path/filepath"
@@ -643,10 +644,14 @@ func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]int
c := config.GetInstance()
if input != nil {
// #5483 - convert JSON numbers to float64 or int64
input = convertMapJSONNumbers(input)
c.SetUIConfiguration(input)
}
if partial != nil {
// #5483 - convert JSON numbers to float64 or int64
partial = convertMapJSONNumbers(partial)
// merge partial into existing config
existing := c.GetUIConfiguration()
utils.MergeMaps(existing, partial)
@@ -664,6 +669,14 @@ func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, v
c := config.GetInstance()
cfg := utils.NestedMap(c.GetUIConfiguration())
// #5483 - convert JSON numbers to float64 or int64
if m, ok := value.(map[string]interface{}); ok {
value = convertMapJSONNumbers(m)
} else if n, ok := value.(json.Number); ok {
value = jsonNumberToNumber(n)
}
cfg.Set(key, value)
return r.ConfigureUI(ctx, cfg, nil)
@@ -671,6 +684,9 @@ func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, v
func (r *mutationResolver) ConfigurePlugin(ctx context.Context, pluginID string, input map[string]interface{}) (map[string]interface{}, error) {
c := config.GetInstance()
// #5483 - convert JSON numbers to float64 or int64
input = convertMapJSONNumbers(input)
c.SetPluginConfiguration(pluginID, input)
if err := c.Write(); err != nil {

View File

@@ -478,6 +478,61 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input Galler
return true, nil
}
func (r *mutationResolver) SetGalleryCover(ctx context.Context, input GallerySetCoverInput) (bool, error) {
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return false, fmt.Errorf("converting gallery id: %w", err)
}
coverImageID, err := strconv.Atoi(input.CoverImageID)
if err != nil {
return false, fmt.Errorf("converting cover image id: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
gallery, err := qb.Find(ctx, galleryID)
if err != nil {
return err
}
if gallery == nil {
return fmt.Errorf("gallery with id %d not found", galleryID)
}
return r.galleryService.SetCover(ctx, gallery, coverImageID)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) ResetGalleryCover(ctx context.Context, input GalleryResetCoverInput) (bool, error) {
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return false, fmt.Errorf("converting gallery id: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
gallery, err := qb.Find(ctx, galleryID)
if err != nil {
return err
}
if gallery == nil {
return fmt.Errorf("gallery with id %d not found", galleryID)
}
return r.galleryService.ResetCover(ctx, gallery)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret *models.GalleryChapter, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.GalleryChapter.Find(ctx, id)

View File

@@ -0,0 +1,413 @@
package api
import (
"context"
"fmt"
"strconv"
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/group"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/utils"
)
func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Populate a new group from the input
newGroup := models.NewGroup()
newGroup.Name = input.Name
newGroup.Aliases = translator.string(input.Aliases)
newGroup.Duration = input.Duration
newGroup.Rating = input.Rating100
newGroup.Director = translator.string(input.Director)
newGroup.Synopsis = translator.string(input.Synopsis)
var err error
newGroup.Date, err = translator.datePtr(input.Date)
if err != nil {
return nil, fmt.Errorf("converting date: %w", err)
}
newGroup.StudioID, err = translator.intPtrFromString(input.StudioID)
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
newGroup.TagIDs, err = translator.relatedIds(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
newGroup.ContainingGroups, err = translator.groupIDDescriptions(input.ContainingGroups)
if err != nil {
return nil, fmt.Errorf("converting containing group ids: %w", err)
}
newGroup.SubGroups, err = translator.groupIDDescriptions(input.SubGroups)
if err != nil {
return nil, fmt.Errorf("converting containing group ids: %w", err)
}
if input.Urls != nil {
newGroup.URLs = models.NewRelatedStrings(input.Urls)
}
return &newGroup, nil
}
func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
newGroup, err := groupFromGroupCreateInput(ctx, input)
if err != nil {
return nil, err
}
// Process the base 64 encoded image string
var frontimageData []byte
if input.FrontImage != nil {
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
if err != nil {
return nil, fmt.Errorf("processing front image: %w", err)
}
}
// Process the base 64 encoded image string
var backimageData []byte
if input.BackImage != nil {
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
if err != nil {
return nil, fmt.Errorf("processing back image: %w", err)
}
}
// HACK: if back image is being set, set the front image to the default.
// This is because we can't have a null front image with a non-null back image.
if len(frontimageData) == 0 && len(backimageData) != 0 {
frontimageData = static.ReadAll(static.DefaultGroupImage)
}
// Start the transaction and save the group
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err = r.groupService.Create(ctx, newGroup, frontimageData, backimageData); err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.GroupCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.MovieCreatePost, input, nil)
return r.getGroup(ctx, newGroup.ID)
}
func groupPartialFromGroupUpdateInput(translator changesetTranslator, input GroupUpdateInput) (ret models.GroupPartial, err error) {
// Populate group from the input
updatedGroup := models.NewGroupPartial()
updatedGroup.Name = translator.optionalString(input.Name, "name")
updatedGroup.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedGroup.Duration = translator.optionalInt(input.Duration, "duration")
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedGroup.Director = translator.optionalString(input.Director, "director")
updatedGroup.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
updatedGroup.Date, err = translator.optionalDate(input.Date, "date")
if err != nil {
err = fmt.Errorf("converting date: %w", err)
return
}
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
err = fmt.Errorf("converting studio id: %w", err)
return
}
updatedGroup.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
if err != nil {
err = fmt.Errorf("converting tag ids: %w", err)
return
}
updatedGroup.ContainingGroups, err = translator.updateGroupIDDescriptions(input.ContainingGroups, "containing_groups")
if err != nil {
err = fmt.Errorf("converting containing group ids: %w", err)
return
}
updatedGroup.SubGroups, err = translator.updateGroupIDDescriptions(input.SubGroups, "sub_groups")
if err != nil {
err = fmt.Errorf("converting containing group ids: %w", err)
return
}
updatedGroup.URLs = translator.updateStrings(input.Urls, "urls")
return updatedGroup, nil
}
func (r *mutationResolver) GroupUpdate(ctx context.Context, input GroupUpdateInput) (*models.Group, error) {
groupID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, fmt.Errorf("converting id: %w", err)
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedGroup, err := groupPartialFromGroupUpdateInput(translator, input)
if err != nil {
return nil, err
}
var frontimageData []byte
frontImageIncluded := translator.hasField("front_image")
if input.FrontImage != nil {
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
if err != nil {
return nil, fmt.Errorf("processing front image: %w", err)
}
}
var backimageData []byte
backImageIncluded := translator.hasField("back_image")
if input.BackImage != nil {
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
if err != nil {
return nil, fmt.Errorf("processing back image: %w", err)
}
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
frontImage := group.ImageInput{
Image: frontimageData,
Set: frontImageIncluded,
}
backImage := group.ImageInput{
Image: backimageData,
Set: backImageIncluded,
}
_, err = r.groupService.UpdatePartial(ctx, groupID, updatedGroup, frontImage, backImage)
if err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, groupID, hook.GroupUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, groupID, hook.MovieUpdatePost, input, translator.getFields())
return r.getGroup(ctx, groupID)
}
func groupPartialFromBulkGroupUpdateInput(translator changesetTranslator, input BulkGroupUpdateInput) (ret models.GroupPartial, err error) {
updatedGroup := models.NewGroupPartial()
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedGroup.Director = translator.optionalString(input.Director, "director")
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
err = fmt.Errorf("converting studio id: %w", err)
return
}
updatedGroup.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
if err != nil {
err = fmt.Errorf("converting tag ids: %w", err)
return
}
updatedGroup.ContainingGroups, err = translator.updateGroupIDDescriptionsBulk(input.ContainingGroups, "containing_groups")
if err != nil {
err = fmt.Errorf("converting containing group ids: %w", err)
return
}
updatedGroup.SubGroups, err = translator.updateGroupIDDescriptionsBulk(input.SubGroups, "sub_groups")
if err != nil {
err = fmt.Errorf("converting containing group ids: %w", err)
return
}
updatedGroup.URLs = translator.optionalURLsBulk(input.Urls, nil)
return updatedGroup, nil
}
func (r *mutationResolver) BulkGroupUpdate(ctx context.Context, input BulkGroupUpdateInput) ([]*models.Group, error) {
groupIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, fmt.Errorf("converting ids: %w", err)
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Populate group from the input
updatedGroup, err := groupPartialFromBulkGroupUpdateInput(translator, input)
if err != nil {
return nil, err
}
ret := []*models.Group{}
if err := r.withTxn(ctx, func(ctx context.Context) error {
for _, groupID := range groupIDs {
group, err := r.groupService.UpdatePartial(ctx, groupID, updatedGroup, group.ImageInput{}, group.ImageInput{})
if err != nil {
return err
}
ret = append(ret, group)
}
return nil
}); err != nil {
return nil, err
}
var newRet []*models.Group
for _, group := range ret {
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.GroupUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.MovieUpdatePost, input, translator.getFields())
group, err = r.getGroup(ctx, group.ID)
if err != nil {
return nil, err
}
newRet = append(newRet, group)
}
return newRet, nil
}
func (r *mutationResolver) GroupDestroy(ctx context.Context, input GroupDestroyInput) (bool, error) {
id, err := strconv.Atoi(input.ID)
if err != nil {
return false, fmt.Errorf("converting id: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.repository.Group.Destroy(ctx, id)
}); err != nil {
return false, err
}
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
return true, nil
}
func (r *mutationResolver) GroupsDestroy(ctx context.Context, groupIDs []string) (bool, error) {
ids, err := stringslice.StringSliceToIntSlice(groupIDs)
if err != nil {
return false, fmt.Errorf("converting ids: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Group
for _, id := range ids {
if err := qb.Destroy(ctx, id); err != nil {
return err
}
}
return nil
}); err != nil {
return false, err
}
for _, id := range ids {
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, groupIDs, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, groupIDs, nil)
}
return true, nil
}
func (r *mutationResolver) AddGroupSubGroups(ctx context.Context, input GroupSubGroupAddInput) (bool, error) {
groupID, err := strconv.Atoi(input.ContainingGroupID)
if err != nil {
return false, fmt.Errorf("converting group id: %w", err)
}
subGroups, err := groupsDescriptionsFromGroupInput(input.SubGroups)
if err != nil {
return false, fmt.Errorf("converting sub group ids: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.groupService.AddSubGroups(ctx, groupID, subGroups, input.InsertIndex)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) RemoveGroupSubGroups(ctx context.Context, input GroupSubGroupRemoveInput) (bool, error) {
groupID, err := strconv.Atoi(input.ContainingGroupID)
if err != nil {
return false, fmt.Errorf("converting group id: %w", err)
}
subGroupIDs, err := stringslice.StringSliceToIntSlice(input.SubGroupIds)
if err != nil {
return false, fmt.Errorf("converting sub group ids: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.groupService.RemoveSubGroups(ctx, groupID, subGroupIDs)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) ReorderSubGroups(ctx context.Context, input ReorderSubGroupsInput) (bool, error) {
groupID, err := strconv.Atoi(input.GroupID)
if err != nil {
return false, fmt.Errorf("converting group id: %w", err)
}
subGroupIDs, err := stringslice.StringSliceToIntSlice(input.SubGroupIds)
if err != nil {
return false, fmt.Errorf("converting sub group ids: %w", err)
}
insertPointID, err := strconv.Atoi(input.InsertAtID)
if err != nil {
return false, fmt.Errorf("converting insert at id: %w", err)
}
insertAfter := utils.IsTrue(input.InsertAfter)
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.groupService.ReorderSubGroups(ctx, groupID, subGroupIDs, insertPointID, insertAfter)
}); err != nil {
return false, err
}
return true, nil
}

View File

@@ -28,7 +28,7 @@ func (r *mutationResolver) getImage(ctx context.Context, id int) (ret *models.Im
return ret, nil
}
func (r *mutationResolver) ImageUpdate(ctx context.Context, input ImageUpdateInput) (ret *models.Image, err error) {
func (r *mutationResolver) ImageUpdate(ctx context.Context, input models.ImageUpdateInput) (ret *models.Image, err error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
@@ -46,7 +46,7 @@ func (r *mutationResolver) ImageUpdate(ctx context.Context, input ImageUpdateInp
return r.getImage(ctx, ret.ID)
}
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdateInput) (ret []*models.Image, err error) {
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*models.ImageUpdateInput) (ret []*models.Image, err error) {
inputMaps := getUpdateInputMaps(ctx)
// Start the transaction and save the image
@@ -89,7 +89,7 @@ func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdat
return newRet, nil
}
func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInput, translator changesetTranslator) (*models.Image, error) {
func (r *mutationResolver) imageUpdate(ctx context.Context, input models.ImageUpdateInput, translator changesetTranslator) (*models.Image, error) {
imageID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, fmt.Errorf("converting id: %w", err)

View File

@@ -12,10 +12,10 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
// used to refetch movie after hooks run
func (r *mutationResolver) getMovie(ctx context.Context, id int) (ret *models.Movie, err error) {
// used to refetch group after hooks run
func (r *mutationResolver) getGroup(ctx context.Context, id int) (ret *models.Group, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.Find(ctx, id)
ret, err = r.repository.Group.Find(ctx, id)
return err
}); err != nil {
return nil, err
@@ -24,33 +24,43 @@ func (r *mutationResolver) getMovie(ctx context.Context, id int) (ret *models.Mo
return ret, nil
}
func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInput) (*models.Movie, error) {
func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInput) (*models.Group, error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Populate a new movie from the input
newMovie := models.NewMovie()
// Populate a new group from the input
newGroup := models.NewGroup()
newMovie.Name = input.Name
newMovie.Aliases = translator.string(input.Aliases)
newMovie.Duration = input.Duration
newMovie.Rating = input.Rating100
newMovie.Director = translator.string(input.Director)
newMovie.Synopsis = translator.string(input.Synopsis)
newMovie.URL = translator.string(input.URL)
newGroup.Name = input.Name
newGroup.Aliases = translator.string(input.Aliases)
newGroup.Duration = input.Duration
newGroup.Rating = input.Rating100
newGroup.Director = translator.string(input.Director)
newGroup.Synopsis = translator.string(input.Synopsis)
var err error
newMovie.Date, err = translator.datePtr(input.Date)
newGroup.Date, err = translator.datePtr(input.Date)
if err != nil {
return nil, fmt.Errorf("converting date: %w", err)
}
newMovie.StudioID, err = translator.intPtrFromString(input.StudioID)
newGroup.StudioID, err = translator.intPtrFromString(input.StudioID)
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
newGroup.TagIDs, err = translator.relatedIds(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
if input.Urls != nil {
newGroup.URLs = models.NewRelatedStrings(input.Urls)
} else if input.URL != nil {
newGroup.URLs = models.NewRelatedStrings([]string{*input.URL})
}
// Process the base 64 encoded image string
var frontimageData []byte
if input.FrontImage != nil {
@@ -72,27 +82,27 @@ 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)
frontimageData = static.ReadAll(static.DefaultGroupImage)
}
// Start the transaction and save the movie
// Start the transaction and save the group
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Movie
qb := r.repository.Group
err = qb.Create(ctx, &newMovie)
err = qb.Create(ctx, &newGroup)
if err != nil {
return err
}
// update image table
if len(frontimageData) > 0 {
if err := qb.UpdateFrontImage(ctx, newMovie.ID, frontimageData); err != nil {
if err := qb.UpdateFrontImage(ctx, newGroup.ID, frontimageData); err != nil {
return err
}
}
if len(backimageData) > 0 {
if err := qb.UpdateBackImage(ctx, newMovie.ID, backimageData); err != nil {
if err := qb.UpdateBackImage(ctx, newGroup.ID, backimageData); err != nil {
return err
}
}
@@ -102,12 +112,14 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, hook.MovieCreatePost, input, nil)
return r.getMovie(ctx, newMovie.ID)
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.GroupCreatePost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.MovieCreatePost, input, nil)
return r.getGroup(ctx, newGroup.ID)
}
func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInput) (*models.Movie, error) {
movieID, err := strconv.Atoi(input.ID)
func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInput) (*models.Group, error) {
groupID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, fmt.Errorf("converting id: %w", err)
}
@@ -116,26 +128,32 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
inputMap: getUpdateInputMap(ctx),
}
// Populate movie from the input
updatedMovie := models.NewMoviePartial()
// Populate group from the input
updatedGroup := models.NewGroupPartial()
updatedMovie.Name = translator.optionalString(input.Name, "name")
updatedMovie.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedMovie.Duration = translator.optionalInt(input.Duration, "duration")
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")
updatedGroup.Name = translator.optionalString(input.Name, "name")
updatedGroup.Aliases = translator.optionalString(input.Aliases, "aliases")
updatedGroup.Duration = translator.optionalInt(input.Duration, "duration")
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedGroup.Director = translator.optionalString(input.Director, "director")
updatedGroup.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
updatedMovie.Date, err = translator.optionalDate(input.Date, "date")
updatedGroup.Date, err = translator.optionalDate(input.Date, "date")
if err != nil {
return nil, fmt.Errorf("converting date: %w", err)
}
updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
updatedGroup.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
updatedGroup.URLs = translator.optionalURLs(input.Urls, input.URL)
var frontimageData []byte
frontImageIncluded := translator.hasField("front_image")
if input.FrontImage != nil {
@@ -154,24 +172,24 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
}
}
// Start the transaction and save the movie
var movie *models.Movie
// Start the transaction and save the group
var group *models.Group
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Movie
movie, err = qb.UpdatePartial(ctx, movieID, updatedMovie)
qb := r.repository.Group
group, err = qb.UpdatePartial(ctx, groupID, updatedGroup)
if err != nil {
return err
}
// update image table
if frontImageIncluded {
if err := qb.UpdateFrontImage(ctx, movie.ID, frontimageData); err != nil {
if err := qb.UpdateFrontImage(ctx, group.ID, frontimageData); err != nil {
return err
}
}
if backImageIncluded {
if err := qb.UpdateBackImage(ctx, movie.ID, backimageData); err != nil {
if err := qb.UpdateBackImage(ctx, group.ID, backimageData); err != nil {
return err
}
}
@@ -181,12 +199,14 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
return r.getMovie(ctx, movie.ID)
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.GroupUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.MovieUpdatePost, input, translator.getFields())
return r.getGroup(ctx, group.ID)
}
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieUpdateInput) ([]*models.Movie, error) {
movieIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieUpdateInput) ([]*models.Group, error) {
groupIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, fmt.Errorf("converting ids: %w", err)
}
@@ -195,29 +215,36 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
inputMap: getUpdateInputMap(ctx),
}
// Populate movie from the input
updatedMovie := models.NewMoviePartial()
// Populate group from the input
updatedGroup := models.NewGroupPartial()
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedMovie.Director = translator.optionalString(input.Director, "director")
updatedGroup.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedGroup.Director = translator.optionalString(input.Director, "director")
updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
updatedGroup.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}
ret := []*models.Movie{}
updatedGroup.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
updatedGroup.URLs = translator.optionalURLsBulk(input.Urls, nil)
ret := []*models.Group{}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Movie
qb := r.repository.Group
for _, movieID := range movieIDs {
movie, err := qb.UpdatePartial(ctx, movieID, updatedMovie)
for _, groupID := range groupIDs {
group, err := qb.UpdatePartial(ctx, groupID, updatedGroup)
if err != nil {
return err
}
ret = append(ret, movie)
ret = append(ret, group)
}
return nil
@@ -225,16 +252,18 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
return nil, err
}
var newRet []*models.Movie
for _, movie := range ret {
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
var newRet []*models.Group
for _, group := range ret {
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.GroupUpdatePost, input, translator.getFields())
r.hookExecutor.ExecutePostHooks(ctx, group.ID, hook.MovieUpdatePost, input, translator.getFields())
movie, err = r.getMovie(ctx, movie.ID)
group, err = r.getGroup(ctx, group.ID)
if err != nil {
return nil, err
}
newRet = append(newRet, movie)
newRet = append(newRet, group)
}
return newRet, nil
@@ -247,24 +276,26 @@ func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyI
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.repository.Movie.Destroy(ctx, id)
return r.repository.Group.Destroy(ctx, id)
}); err != nil {
return false, err
}
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, input, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
return true, nil
}
func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string) (bool, error) {
ids, err := stringslice.StringSliceToIntSlice(movieIDs)
func (r *mutationResolver) MoviesDestroy(ctx context.Context, groupIDs []string) (bool, error) {
ids, err := stringslice.StringSliceToIntSlice(groupIDs)
if err != nil {
return false, fmt.Errorf("converting ids: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Movie
qb := r.repository.Group
for _, id := range ids {
if err := qb.Destroy(ctx, id); err != nil {
return err
@@ -277,7 +308,9 @@ func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string)
}
for _, id := range ids {
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, movieIDs, nil)
// for backwards compatibility - run both movie and group hooks
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, groupIDs, nil)
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, groupIDs, nil)
}
return true, nil

View File

@@ -12,6 +12,11 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
const (
twitterURL = "https://twitter.com"
instagramURL = "https://instagram.com"
)
// used to refetch performer after hooks run
func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *models.Performer, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
@@ -35,7 +40,6 @@ 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)
newPerformer.Country = translator.string(input.Country)
@@ -47,8 +51,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
newPerformer.CareerLength = translator.string(input.CareerLength)
newPerformer.Tattoos = translator.string(input.Tattoos)
newPerformer.Piercings = translator.string(input.Piercings)
newPerformer.Twitter = translator.string(input.Twitter)
newPerformer.Instagram = translator.string(input.Instagram)
newPerformer.Favorite = translator.bool(input.Favorite)
newPerformer.Rating = input.Rating100
newPerformer.Details = translator.string(input.Details)
@@ -56,7 +58,22 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
newPerformer.Height = input.HeightCm
newPerformer.Weight = input.Weight
newPerformer.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds)
newPerformer.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
newPerformer.URLs = models.NewRelatedStrings([]string{})
if input.URL != nil {
newPerformer.URLs.Add(*input.URL)
}
if input.Twitter != nil {
newPerformer.URLs.Add(utils.URLFromHandle(*input.Twitter, twitterURL))
}
if input.Instagram != nil {
newPerformer.URLs.Add(utils.URLFromHandle(*input.Instagram, instagramURL))
}
if input.Urls != nil {
newPerformer.URLs.Add(input.Urls...)
}
var err error
@@ -91,7 +108,13 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
return err
}
err = qb.Create(ctx, &newPerformer)
i := &models.CreatePerformerInput{
Performer: &newPerformer,
// convert json.Numbers to int/float
CustomFields: convertMapJSONNumbers(input.CustomFields),
}
err = qb.Create(ctx, i)
if err != nil {
return err
}
@@ -112,6 +135,96 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
return r.getPerformer(ctx, newPerformer.ID)
}
func (r *mutationResolver) validateNoLegacyURLs(translator changesetTranslator) error {
// ensure url/twitter/instagram are not included in the input
if translator.hasField("url") {
return fmt.Errorf("url field must not be included if urls is included")
}
if translator.hasField("twitter") {
return fmt.Errorf("twitter field must not be included if urls is included")
}
if translator.hasField("instagram") {
return fmt.Errorf("instagram field must not be included if urls is included")
}
return nil
}
func (r *mutationResolver) handleLegacyURLs(ctx context.Context, performerID int, legacyURL, legacyTwitter, legacyInstagram models.OptionalString, updatedPerformer *models.PerformerPartial) error {
qb := r.repository.Performer
// we need to be careful with URL/Twitter/Instagram
// treat URL as replacing the first non-Twitter/Instagram URL in the list
// twitter should replace any existing twitter URL
// instagram should replace any existing instagram URL
p, err := qb.Find(ctx, performerID)
if err != nil {
return err
}
if err := p.LoadURLs(ctx, qb); err != nil {
return fmt.Errorf("loading performer URLs: %w", err)
}
existingURLs := p.URLs.List()
// performer partial URLs should be empty
if legacyURL.Set {
replaced := false
for i, url := range existingURLs {
if !performer.IsTwitterURL(url) && !performer.IsInstagramURL(url) {
existingURLs[i] = legacyURL.Value
replaced = true
break
}
}
if !replaced {
existingURLs = append(existingURLs, legacyURL.Value)
}
}
if legacyTwitter.Set {
value := utils.URLFromHandle(legacyTwitter.Value, twitterURL)
found := false
// find and replace the first twitter URL
for i, url := range existingURLs {
if performer.IsTwitterURL(url) {
existingURLs[i] = value
found = true
break
}
}
if !found {
existingURLs = append(existingURLs, value)
}
}
if legacyInstagram.Set {
found := false
value := utils.URLFromHandle(legacyInstagram.Value, instagramURL)
// find and replace the first instagram URL
for i, url := range existingURLs {
if performer.IsInstagramURL(url) {
existingURLs[i] = value
found = true
break
}
}
if !found {
existingURLs = append(existingURLs, value)
}
}
updatedPerformer.URLs = &models.UpdateStrings{
Values: existingURLs,
Mode: models.RelationshipUpdateModeSet,
}
return nil
}
func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) {
performerID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -127,7 +240,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.Name = translator.optionalString(input.Name, "name")
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
updatedPerformer.URL = translator.optionalString(input.URL, "url")
updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender")
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.optionalString(input.Country, "country")
@@ -139,8 +251,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedPerformer.Details = translator.optionalString(input.Details, "details")
@@ -149,6 +259,19 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedPerformer.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
if translator.hasField("urls") {
// ensure url/twitter/instagram are not included in the input
if err := r.validateNoLegacyURLs(translator); err != nil {
return nil, err
}
updatedPerformer.URLs = translator.updateStrings(input.Urls, "urls")
}
legacyURL := translator.optionalString(input.URL, "url")
legacyTwitter := translator.optionalString(input.Twitter, "twitter")
legacyInstagram := translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate")
if err != nil {
return nil, fmt.Errorf("converting birthdate: %w", err)
@@ -173,6 +296,11 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
return nil, fmt.Errorf("converting tag ids: %w", err)
}
updatedPerformer.CustomFields = input.CustomFields
// convert json.Numbers to int/float
updatedPerformer.CustomFields.Full = convertMapJSONNumbers(updatedPerformer.CustomFields.Full)
updatedPerformer.CustomFields.Partial = convertMapJSONNumbers(updatedPerformer.CustomFields.Partial)
var imageData []byte
imageIncluded := translator.hasField("image")
if input.Image != nil {
@@ -186,6 +314,12 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer
if legacyURL.Set || legacyTwitter.Set || legacyInstagram.Set {
if err := r.handleLegacyURLs(ctx, performerID, legacyURL, legacyTwitter, legacyInstagram, &updatedPerformer); err != nil {
return err
}
}
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
return err
}
@@ -225,7 +359,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
updatedPerformer := models.NewPerformerPartial()
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
updatedPerformer.URL = translator.optionalString(input.URL, "url")
updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender")
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.optionalString(input.Country, "country")
@@ -237,8 +371,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedPerformer.Details = translator.optionalString(input.Details, "details")
@@ -246,6 +379,19 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
if translator.hasField("urls") {
// ensure url/twitter/instagram are not included in the input
if err := r.validateNoLegacyURLs(translator); err != nil {
return nil, err
}
updatedPerformer.URLs = translator.updateStringsBulk(input.Urls, "urls")
}
legacyURL := translator.optionalString(input.URL, "url")
legacyTwitter := translator.optionalString(input.Twitter, "twitter")
legacyInstagram := translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate")
if err != nil {
return nil, fmt.Errorf("converting birthdate: %w", err)
@@ -277,6 +423,12 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
qb := r.repository.Performer
for _, performerID := range performerIDs {
if legacyURL.Set || legacyTwitter.Set || legacyInstagram.Set {
if err := r.handleLegacyURLs(ctx, performerID, legacyURL, legacyTwitter, legacyInstagram, &updatedPerformer); err != nil {
return err
}
}
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
return err
}

View File

@@ -7,7 +7,10 @@ import (
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput) (ret *models.SavedFilter, err error) {
@@ -67,30 +70,48 @@ func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input Destroy
}
func (r *mutationResolver) SetDefaultFilter(ctx context.Context, input SetDefaultFilterInput) (bool, error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.SavedFilter
// deprecated - write to the config in the meantime
config := config.GetInstance()
if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil {
// clearing
def, err := qb.FindDefault(ctx, input.Mode)
if err != nil {
return err
}
uiConfig := config.GetUIConfiguration()
if uiConfig == nil {
uiConfig = make(map[string]interface{})
}
if def != nil {
return qb.Destroy(ctx, def.ID)
}
m := utils.NestedMap(uiConfig)
return nil
if input.FindFilter == nil && input.ObjectFilter == nil && input.UIOptions == nil {
// clearing
m.Delete("defaultFilters." + strings.ToLower(input.Mode.String()))
config.SetUIConfiguration(m)
if err := config.Write(); err != nil {
return false, err
}
return qb.SetDefault(ctx, &models.SavedFilter{
Mode: input.Mode,
FindFilter: input.FindFilter,
ObjectFilter: input.ObjectFilter,
UIOptions: input.UIOptions,
})
}); err != nil {
return true, nil
}
subMap := make(map[string]interface{})
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
WeaklyTypedInput: true,
Result: &subMap,
})
if err != nil {
return false, err
}
if err := d.Decode(input); err != nil {
return false, err
}
m.Set("defaultFilters."+strings.ToLower(input.Mode.String()), subMap)
config.SetUIConfiguration(m)
if err := config.Write(); err != nil {
return false, err
}

View File

@@ -50,7 +50,7 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr
newScene.Director = translator.string(input.Director)
newScene.Rating = input.Rating100
newScene.Organized = translator.bool(input.Organized)
newScene.StashIDs = models.NewRelatedStashIDs(input.StashIds)
newScene.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
newScene.Date, err = translator.datePtr(input.Date)
if err != nil {
@@ -80,9 +80,17 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
newScene.Movies, err = translator.relatedMovies(input.Movies)
if err != nil {
return nil, fmt.Errorf("converting movies: %w", err)
// prefer groups over movies
if len(input.Groups) > 0 {
newScene.Groups, err = translator.relatedGroups(input.Groups)
if err != nil {
return nil, fmt.Errorf("converting groups: %w", err)
}
} else if len(input.Movies) > 0 {
newScene.Groups, err = translator.relatedGroupsFromMovies(input.Movies)
if err != nil {
return nil, fmt.Errorf("converting movies: %w", err)
}
}
var coverImageData []byte
@@ -216,9 +224,16 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
updatedScene.MovieIDs, err = translator.updateMovieIDs(input.Movies, "movies")
if err != nil {
return nil, fmt.Errorf("converting movies: %w", err)
if translator.hasField("groups") {
updatedScene.GroupIDs, err = translator.updateGroupIDs(input.Groups, "groups")
if err != nil {
return nil, fmt.Errorf("converting groups: %w", err)
}
} else if translator.hasField("movies") {
updatedScene.GroupIDs, err = translator.updateGroupIDsFromMovies(input.Movies, "movies")
if err != nil {
return nil, fmt.Errorf("converting movies: %w", err)
}
}
return &updatedScene, nil
@@ -358,9 +373,16 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
updatedScene.MovieIDs, err = translator.updateMovieIDsBulk(input.MovieIds, "movie_ids")
if err != nil {
return nil, fmt.Errorf("converting movie ids: %w", err)
if translator.hasField("group_ids") {
updatedScene.GroupIDs, err = translator.updateGroupIDsBulk(input.GroupIds, "group_ids")
if err != nil {
return nil, fmt.Errorf("converting group ids: %w", err)
}
} else if translator.hasField("movie_ids") {
updatedScene.GroupIDs, err = translator.updateGroupIDsBulk(input.MovieIds, "movie_ids")
if err != nil {
return nil, fmt.Errorf("converting movie ids: %w", err)
}
}
ret := []*models.Scene{}
@@ -633,6 +655,13 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
newMarker.PrimaryTagID = primaryTagID
newMarker.SceneID = sceneID
if input.EndSeconds != nil {
if err := validateSceneMarkerEndSeconds(newMarker.Seconds, *input.EndSeconds); err != nil {
return nil, err
}
newMarker.EndSeconds = input.EndSeconds
}
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
@@ -658,6 +687,13 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
return r.getSceneMarker(ctx, newMarker.ID)
}
func validateSceneMarkerEndSeconds(seconds, endSeconds float64) error {
if endSeconds < seconds {
return fmt.Errorf("end_seconds (%f) must be greater than or equal to seconds (%f)", endSeconds, seconds)
}
return nil
}
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) {
markerID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -673,6 +709,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
updatedMarker.Title = translator.optionalString(input.Title, "title")
updatedMarker.Seconds = translator.optionalFloat64(input.Seconds, "seconds")
updatedMarker.EndSeconds = translator.optionalFloat64(input.EndSeconds, "end_seconds")
updatedMarker.SceneID, err = translator.optionalIntFromString(input.SceneID, "scene_id")
if err != nil {
return nil, fmt.Errorf("converting scene id: %w", err)
@@ -713,6 +750,26 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
return fmt.Errorf("scene marker with id %d not found", markerID)
}
// Validate end_seconds
shouldValidateEndSeconds := (updatedMarker.Seconds.Set || updatedMarker.EndSeconds.Set) && !updatedMarker.EndSeconds.Null
if shouldValidateEndSeconds {
seconds := existingMarker.Seconds
if updatedMarker.Seconds.Set {
seconds = updatedMarker.Seconds.Value
}
endSeconds := existingMarker.EndSeconds
if updatedMarker.EndSeconds.Set {
endSeconds = &updatedMarker.EndSeconds.Value
}
if endSeconds != nil {
if err := validateSceneMarkerEndSeconds(seconds, *endSeconds); err != nil {
return err
}
}
}
newMarker, err := qb.UpdatePartial(ctx, markerID, updatedMarker)
if err != nil {
return err
@@ -727,7 +784,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
}
// remove the marker preview if the scene changed or if the timestamp was changed
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds {
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds || existingMarker.EndSeconds != newMarker.EndSeconds {
seconds := int(existingMarker.Seconds)
if err := fileDeleter.MarkMarkerFiles(existingScene, seconds); err != nil {
return err
@@ -757,11 +814,16 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
}
func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) {
markerID, err := strconv.Atoi(id)
return r.SceneMarkersDestroy(ctx, []string{id})
}
func (r *mutationResolver) SceneMarkersDestroy(ctx context.Context, markerIDs []string) (bool, error) {
ids, err := stringslice.StringSliceToIntSlice(markerIDs)
if err != nil {
return false, fmt.Errorf("converting id: %w", err)
return false, fmt.Errorf("converting ids: %w", err)
}
var markers []*models.SceneMarker
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
fileDeleter := &scene.FileDeleter{
@@ -774,35 +836,45 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
qb := r.repository.SceneMarker
sqb := r.repository.Scene
marker, err := qb.Find(ctx, markerID)
for _, markerID := range ids {
marker, err := qb.Find(ctx, markerID)
if err != nil {
return err
if err != nil {
return err
}
if marker == nil {
return fmt.Errorf("scene marker with id %d not found", markerID)
}
s, err := sqb.Find(ctx, marker.SceneID)
if err != nil {
return err
}
if s == nil {
return fmt.Errorf("scene with id %d not found", marker.SceneID)
}
markers = append(markers, marker)
if err := scene.DestroyMarker(ctx, s, marker, qb, fileDeleter); err != nil {
return err
}
}
if marker == nil {
return fmt.Errorf("scene marker with id %d not found", markerID)
}
s, err := sqb.Find(ctx, marker.SceneID)
if err != nil {
return err
}
if s == nil {
return fmt.Errorf("scene with id %d not found", marker.SceneID)
}
return scene.DestroyMarker(ctx, s, marker, qb, fileDeleter)
return nil
}); err != nil {
fileDeleter.Rollback()
return false, err
}
// perform the post-commit actions
fileDeleter.Commit()
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerDestroyPost, id, nil)
for _, marker := range markers {
r.hookExecutor.ExecutePostHooks(ctx, marker.ID, hook.SceneMarkerDestroyPost, markerIDs, nil)
}
return true, nil
}
@@ -825,6 +897,24 @@ func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, res
return ret, nil
}
func (r *mutationResolver) SceneResetActivity(ctx context.Context, id string, resetResume *bool, resetDuration *bool) (ret bool, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
return false, fmt.Errorf("converting id: %w", err)
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
ret, err = qb.ResetActivity(ctx, sceneID, utils.IsTrue(resetResume), utils.IsTrue(resetDuration))
return err
}); err != nil {
return false, err
}
return ret, nil
}
// deprecated
func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id string) (ret int, err error) {
sceneID, err := strconv.Atoi(id)

View File

@@ -6,41 +6,65 @@ import (
"strconv"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/scraper/stashbox"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/stashbox"
)
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input StashBoxFingerprintSubmissionInput) (bool, error) {
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return false, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
if err != nil {
return false, err
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
ids, err := stringslice.StringSliceToIntSlice(input.SceneIds)
if err != nil {
return false, err
}
return client.SubmitStashBoxFingerprints(ctx, input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
client := r.newStashBoxClient(*b)
var scenes []*models.Scene
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
scenes, err = r.sceneService.FindMany(ctx, ids, scene.LoadStashIDs, scene.LoadFiles)
return err
}); err != nil {
return false, err
}
return client.SubmitFingerprints(ctx, scenes)
}
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) {
jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, input)
b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint)
if err != nil {
return "", err
}
jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, b, input)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) StashBoxBatchStudioTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) {
jobID := manager.GetInstance().StashBoxBatchStudioTag(ctx, input)
b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint)
if err != nil {
return "", err
}
jobID := manager.GetInstance().StashBoxBatchStudioTag(ctx, b, input)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
if err != nil {
return nil, err
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
client := r.newStashBoxClient(*b)
id, err := strconv.Atoi(input.ID)
if err != nil {
@@ -64,25 +88,83 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
logger.Errorf("Error getting scene cover: %v", err)
}
if err := scene.LoadURLs(ctx, r.repository.Scene); err != nil {
return fmt.Errorf("loading scene URLs: %w", err)
draft, err := r.makeSceneDraft(ctx, scene, cover)
if err != nil {
return err
}
res, err = client.SubmitSceneDraft(ctx, scene, boxes[input.StashBoxIndex].Endpoint, cover)
res, err = client.SubmitSceneDraft(ctx, *draft)
return err
})
return res, err
}
func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
func (r *mutationResolver) makeSceneDraft(ctx context.Context, s *models.Scene, cover []byte) (*stashbox.SceneDraft, error) {
if err := s.LoadURLs(ctx, r.repository.Scene); err != nil {
return nil, fmt.Errorf("loading scene URLs: %w", err)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.stashboxRepository())
if err := s.LoadStashIDs(ctx, r.repository.Scene); err != nil {
return nil, err
}
draft := &stashbox.SceneDraft{
Scene: s,
}
pqb := r.repository.Performer
sqb := r.repository.Studio
if s.StudioID != nil {
var err error
draft.Studio, err = sqb.Find(ctx, *s.StudioID)
if err != nil {
return nil, err
}
if draft.Studio == nil {
return nil, fmt.Errorf("studio with id %d not found", *s.StudioID)
}
if err := draft.Studio.LoadStashIDs(ctx, r.repository.Studio); err != nil {
return nil, err
}
}
// submit all file fingerprints
if err := s.LoadFiles(ctx, r.repository.Scene); err != nil {
return nil, err
}
scenePerformers, err := pqb.FindBySceneID(ctx, s.ID)
if err != nil {
return nil, err
}
for _, p := range scenePerformers {
if err := p.LoadStashIDs(ctx, pqb); err != nil {
return nil, err
}
}
draft.Performers = scenePerformers
draft.Tags, err = r.repository.Tag.FindBySceneID(ctx, s.ID)
if err != nil {
return nil, err
}
draft.Cover = cover
return draft, nil
}
func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
if err != nil {
return nil, err
}
client := r.newStashBoxClient(*b)
id, err := strconv.Atoi(input.ID)
if err != nil {
@@ -101,7 +183,22 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp
return fmt.Errorf("performer with id %d not found", id)
}
res, err = client.SubmitPerformerDraft(ctx, performer, boxes[input.StashBoxIndex].Endpoint)
pqb := r.repository.Performer
if err := performer.LoadAliases(ctx, pqb); err != nil {
return err
}
if err := performer.LoadURLs(ctx, pqb); err != nil {
return err
}
if err := performer.LoadStashIDs(ctx, pqb); err != nil {
return err
}
img, _ := pqb.GetImage(ctx, performer.ID)
res, err = client.SubmitPerformerDraft(ctx, performer, img)
return err
})

View File

@@ -39,7 +39,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
newStudio.Details = translator.string(input.Details)
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
newStudio.Aliases = models.NewRelatedStrings(input.Aliases)
newStudio.StashIDs = models.NewRelatedStashIDs(input.StashIds)
newStudio.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
var err error
@@ -48,6 +48,11 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
return nil, fmt.Errorf("converting parent id: %w", err)
}
newStudio.TagIDs, err = translator.relatedIds(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
// Process the base 64 encoded image string
var imageData []byte
if input.Image != nil {
@@ -114,6 +119,11 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
return nil, fmt.Errorf("converting parent id: %w", err)
}
updatedStudio.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
// Process the base 64 encoded image string
var imageData []byte
imageIncluded := translator.hasField("image")

View File

@@ -33,26 +33,22 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
newTag := models.NewTag()
newTag.Name = input.Name
newTag.SortName = translator.string(input.SortName)
newTag.Aliases = models.NewRelatedStrings(input.Aliases)
newTag.Favorite = translator.bool(input.Favorite)
newTag.Description = translator.string(input.Description)
newTag.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
var err error
var parentIDs []int
if len(input.ParentIds) > 0 {
parentIDs, err = stringslice.StringSliceToIntSlice(input.ParentIds)
if err != nil {
return nil, fmt.Errorf("converting parent ids: %w", err)
}
newTag.ParentIDs, err = translator.relatedIds(input.ParentIds)
if err != nil {
return nil, fmt.Errorf("converting parent tag ids: %w", err)
}
var childIDs []int
if len(input.ChildIds) > 0 {
childIDs, err = stringslice.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return nil, fmt.Errorf("converting child ids: %w", err)
}
newTag.ChildIDs, err = translator.relatedIds(input.ChildIds)
if err != nil {
return nil, fmt.Errorf("converting child tag ids: %w", err)
}
// Process the base 64 encoded image string
@@ -68,8 +64,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Tag
// ensure name is unique
if err := tag.EnsureTagNameUnique(ctx, 0, newTag.Name, qb); err != nil {
if err := tag.ValidateCreate(ctx, newTag, qb); err != nil {
return err
}
@@ -85,36 +80,6 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
}
}
if len(input.Aliases) > 0 {
if err := tag.EnsureAliasesUnique(ctx, newTag.ID, input.Aliases, qb); err != nil {
return err
}
if err := qb.UpdateAliases(ctx, newTag.ID, input.Aliases); err != nil {
return err
}
}
if len(parentIDs) > 0 {
if err := qb.UpdateParentTags(ctx, newTag.ID, parentIDs); err != nil {
return err
}
}
if len(childIDs) > 0 {
if err := qb.UpdateChildTags(ctx, newTag.ID, childIDs); err != nil {
return err
}
}
// FIXME: This should be called before any changes are made, but
// requires a rewrite of ValidateHierarchy.
if len(parentIDs) > 0 || len(childIDs) > 0 {
if err := tag.ValidateHierarchy(ctx, &newTag, parentIDs, childIDs, qb); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
@@ -137,24 +102,22 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
// Populate tag from the input
updatedTag := models.NewTagPartial()
updatedTag.Name = translator.optionalString(input.Name, "name")
updatedTag.SortName = translator.optionalString(input.SortName, "sort_name")
updatedTag.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedTag.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedTag.Description = translator.optionalString(input.Description, "description")
var parentIDs []int
if translator.hasField("parent_ids") {
parentIDs, err = stringslice.StringSliceToIntSlice(input.ParentIds)
if err != nil {
return nil, fmt.Errorf("converting parent ids: %w", err)
}
updatedTag.Aliases = translator.updateStrings(input.Aliases, "aliases")
updatedTag.ParentIDs, err = translator.updateIds(input.ParentIds, "parent_ids")
if err != nil {
return nil, fmt.Errorf("converting parent tag ids: %w", err)
}
var childIDs []int
if translator.hasField("child_ids") {
childIDs, err = stringslice.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return nil, fmt.Errorf("converting child ids: %w", err)
}
updatedTag.ChildIDs, err = translator.updateIds(input.ChildIds, "child_ids")
if err != nil {
return nil, fmt.Errorf("converting child tag ids: %w", err)
}
var imageData []byte
@@ -171,24 +134,10 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Tag
// ensure name is unique
t, err = qb.Find(ctx, tagID)
if err != nil {
if err := tag.ValidateUpdate(ctx, tagID, updatedTag, qb); err != nil {
return err
}
if t == nil {
return fmt.Errorf("tag with id %d not found", tagID)
}
if input.Name != nil && t.Name != *input.Name {
if err := tag.EnsureTagNameUnique(ctx, tagID, *input.Name, qb); err != nil {
return err
}
updatedTag.Name = models.NewOptionalString(*input.Name)
}
t, err = qb.UpdatePartial(ctx, tagID, updatedTag)
if err != nil {
return err
@@ -201,37 +150,6 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
}
}
if translator.hasField("aliases") {
if err := tag.EnsureAliasesUnique(ctx, tagID, input.Aliases, qb); err != nil {
return err
}
if err := qb.UpdateAliases(ctx, tagID, input.Aliases); err != nil {
return err
}
}
if parentIDs != nil {
if err := qb.UpdateParentTags(ctx, tagID, parentIDs); err != nil {
return err
}
}
if childIDs != nil {
if err := qb.UpdateChildTags(ctx, tagID, childIDs); err != nil {
return err
}
}
// FIXME: This should be called before any changes are made, but
// requires a rewrite of ValidateHierarchy.
if parentIDs != nil || childIDs != nil {
if err := tag.ValidateHierarchy(ctx, t, parentIDs, childIDs, qb); err != nil {
logger.Errorf("Error saving tag: %s", err)
return err
}
}
return nil
}); err != nil {
return nil, err
@@ -241,6 +159,75 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
return r.getTag(ctx, t.ID)
}
func (r *mutationResolver) BulkTagUpdate(ctx context.Context, input BulkTagUpdateInput) ([]*models.Tag, error) {
tagIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, fmt.Errorf("converting ids: %w", err)
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Populate scene from the input
updatedTag := models.NewTagPartial()
updatedTag.Description = translator.optionalString(input.Description, "description")
updatedTag.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedTag.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedTag.Aliases = translator.updateStringsBulk(input.Aliases, "aliases")
updatedTag.ParentIDs, err = translator.updateIdsBulk(input.ParentIds, "parent_ids")
if err != nil {
return nil, fmt.Errorf("converting parent tag ids: %w", err)
}
updatedTag.ChildIDs, err = translator.updateIdsBulk(input.ChildIds, "child_ids")
if err != nil {
return nil, fmt.Errorf("converting child tag ids: %w", err)
}
ret := []*models.Tag{}
// Start the transaction and save the scenes
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Tag
for _, tagID := range tagIDs {
if err := tag.ValidateUpdate(ctx, tagID, updatedTag, qb); err != nil {
return err
}
tag, err := qb.UpdatePartial(ctx, tagID, updatedTag)
if err != nil {
return err
}
ret = append(ret, tag)
}
return nil
}); err != nil {
return nil, err
}
// execute post hooks outside of txn
var newRet []*models.Tag
for _, tag := range ret {
r.hookExecutor.ExecutePostHooks(ctx, tag.ID, hook.TagUpdatePost, input, translator.getFields())
tag, err = r.getTag(ctx, tag.ID)
if err != nil {
return nil, err
}
newRet = append(newRet, tag)
}
return newRet, nil
}
func (r *mutationResolver) TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error) {
tagID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -331,7 +318,7 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput)
return err
}
err = tag.ValidateHierarchy(ctx, t, parents, children, qb)
err = tag.ValidateHierarchyExisting(ctx, t, parents, children, qb)
if err != nil {
logger.Errorf("Error merging tag: %s", err)
return err

View File

@@ -9,7 +9,6 @@ import (
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scraper/stashbox"
"golang.org/x/text/collate"
)
@@ -241,7 +240,7 @@ func makeConfigUIResult() map[string]interface{} {
func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input config.StashBoxInput) (*StashBoxValidationResult, error) {
box := models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}
client := stashbox.NewClient(box, r.stashboxRepository())
client := r.newStashBoxClient(box)
user, err := client.GetUser(ctx)

View File

@@ -0,0 +1,59 @@
package api
import (
"context"
"strconv"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *queryResolver) FindGroup(ctx context.Context, id string) (ret *models.Group, err error) {
idInt, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Group.Find(ctx, idInt)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) FindGroups(ctx context.Context, groupFilter *models.GroupFilterType, filter *models.FindFilterType, ids []string) (ret *FindGroupsResultType, err error) {
idInts, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var groups []*models.Group
var err error
var total int
if len(idInts) > 0 {
groups, err = r.repository.Group.FindMany(ctx, idInts)
total = len(groups)
} else {
groups, total, err = r.repository.Group.Query(ctx, groupFilter, filter)
}
if err != nil {
return err
}
ret = &FindGroupsResultType{
Count: total,
Groups: groups,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -2,11 +2,11 @@ package api
import (
"context"
"slices"
"strconv"
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
@@ -95,11 +95,11 @@ func (r *queryResolver) FindImages(
result, err = qb.Query(ctx, models.ImageQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,
Count: sliceutil.Contains(fields, "count"),
Count: slices.Contains(fields, "count"),
},
ImageFilter: imageFilter,
Megapixels: sliceutil.Contains(fields, "megapixels"),
TotalSize: sliceutil.Contains(fields, "filesize"),
Megapixels: slices.Contains(fields, "megapixels"),
TotalSize: slices.Contains(fields, "filesize"),
})
if err == nil {
images, err = result.Resolve(ctx)

View File

@@ -8,14 +8,14 @@ import (
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Movie, err error) {
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Group, err error) {
idInt, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.Find(ctx, idInt)
ret, err = r.repository.Group.Find(ctx, idInt)
return err
}); err != nil {
return nil, err
@@ -24,22 +24,22 @@ 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, ids []string) (ret *FindMoviesResultType, err error) {
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.GroupFilterType, 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 {
var movies []*models.Movie
var groups []*models.Group
var err error
var total int
if len(idInts) > 0 {
movies, err = r.repository.Movie.FindMany(ctx, idInts)
total = len(movies)
groups, err = r.repository.Group.FindMany(ctx, idInts)
total = len(groups)
} else {
movies, total, err = r.repository.Movie.Query(ctx, movieFilter, filter)
groups, total, err = r.repository.Group.Query(ctx, movieFilter, filter)
}
if err != nil {
@@ -48,7 +48,7 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi
ret = &FindMoviesResultType{
Count: total,
Movies: movies,
Movies: groups,
}
return nil
}); err != nil {
@@ -58,9 +58,9 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi
return ret, nil
}
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Movie, err error) {
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Group, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Movie.All(ctx)
ret, err = r.repository.Group.All(ctx)
return err
}); err != nil {
return nil, err

View File

@@ -32,6 +32,11 @@ func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *mod
}
}
// #5682 - convert JSON numbers to float64 or int64
if performerFilter != nil {
performerFilter.CustomFields = convertCustomFieldCriterionInputJSONNumbers(performerFilter.CustomFields)
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var performers []*models.Performer
var err error

View File

@@ -3,8 +3,12 @@ package api
import (
"context"
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *models.SavedFilter, err error) {
@@ -37,11 +41,35 @@ func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.Filte
}
func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.FilterMode) (ret *models.SavedFilter, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
return err
}); err != nil {
// deprecated - read from the config in the meantime
config := config.GetInstance()
uiConfig := config.GetUIConfiguration()
if uiConfig == nil {
return nil, nil
}
m := utils.NestedMap(uiConfig)
filterRaw, _ := m.Get("defaultFilters." + strings.ToLower(mode.String()))
if filterRaw == nil {
return nil, nil
}
ret = &models.SavedFilter{}
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
WeaklyTypedInput: true,
Result: ret,
})
if err != nil {
return nil, err
}
return ret, err
if err := d.Decode(filterRaw); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -2,13 +2,13 @@ package api
import (
"context"
"slices"
"strconv"
"github.com/99designs/gqlgen/graphql"
"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"
)
@@ -119,11 +119,11 @@ func (r *queryResolver) FindScenes(
result, err = r.repository.Scene.Query(ctx, models.SceneQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,
Count: sliceutil.Contains(fields, "count"),
Count: slices.Contains(fields, "count"),
},
SceneFilter: sceneFilter,
TotalDuration: sliceutil.Contains(fields, "duration"),
TotalSize: sliceutil.Contains(fields, "filesize"),
TotalDuration: slices.Contains(fields, "duration"),
TotalSize: slices.Contains(fields, "filesize"),
})
if err == nil {
scenes, err = result.Resolve(ctx)
@@ -174,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: sliceutil.Contains(fields, "count"),
Count: slices.Contains(fields, "count"),
},
SceneFilter: sceneFilter,
TotalDuration: sliceutil.Contains(fields, "duration"),
TotalSize: sliceutil.Contains(fields, "filesize"),
TotalDuration: slices.Contains(fields, "duration"),
TotalSize: slices.Contains(fields, "filesize"),
})
if err != nil {
return err

View File

@@ -4,14 +4,31 @@ import (
"context"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
)
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (ret *FindSceneMarkersResultType, err error) {
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType, ids []string) (ret *FindSceneMarkersResultType, err error) {
idInts, err := stringslice.StringSliceToIntSlice(ids)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
sceneMarkers, total, err := r.repository.SceneMarker.Query(ctx, sceneMarkerFilter, filter)
var sceneMarkers []*models.SceneMarker
var err error
var total int
if len(idInts) > 0 {
sceneMarkers, err = r.repository.SceneMarker.FindMany(ctx, idInts)
total = len(sceneMarkers)
} else {
sceneMarkers, total, err = r.repository.SceneMarker.Query(ctx, sceneMarkerFilter, filter)
}
if err != nil {
return err
}
ret = &FindSceneMarkersResultType{
Count: total,
SceneMarkers: sceneMarkers,

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"slices"
"sort"
"strings"
@@ -11,7 +12,6 @@ import (
"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")
@@ -166,7 +166,7 @@ func (r *queryResolver) InstalledPackages(ctx context.Context, typeArg PackageTy
var ret []*Package
if sliceutil.Contains(graphql.CollectAllFields(ctx), "source_package") {
if slices.Contains(graphql.CollectAllFields(ctx), "source_package") {
ret, err = r.getInstalledPackagesWithUpgrades(ctx, pm)
if err != nil {
return nil, err

View File

@@ -4,16 +4,11 @@ import (
"context"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/match"
"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"
)
@@ -35,7 +30,7 @@ func (r *queryResolver) ScrapePerformerURL(ctx context.Context, url string) (*mo
return marshalScrapedPerformer(content)
}
func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string, query string) ([]*scraper.ScrapedScene, error) {
func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string, query string) ([]*models.ScrapedScene, error) {
if query == "" {
return nil, nil
}
@@ -50,102 +45,10 @@ func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string,
return nil, err
}
filterSceneTags(ret)
return ret, nil
}
func compileRegexps(patterns []string) []*regexp.Regexp {
excludePatterns := patterns
var excludeRegexps []*regexp.Regexp
for _, excludePattern := range excludePatterns {
reg, err := regexp.Compile(strings.ToLower(excludePattern))
if err != nil {
logger.Errorf("Invalid tag exclusion pattern: %v", err)
} else {
excludeRegexps = append(excludeRegexps, reg)
}
}
return excludeRegexps
}
// filterSceneTags removes tags matching excluded tag patterns from the provided scraped scenes
func filterTags(excludeRegexps []*regexp.Regexp, tags []*models.ScrapedTag) (newTags []*models.ScrapedTag, ignoredTags []string) {
if len(excludeRegexps) == 0 {
return tags, nil
}
for _, t := range tags {
ignore := false
for _, reg := range excludeRegexps {
if reg.MatchString(strings.ToLower(t.Name)) {
ignore = true
ignoredTags = sliceutil.AppendUnique(ignoredTags, t.Name)
break
}
}
if !ignore {
newTags = append(newTags, t)
}
}
return
}
// filterSceneTags removes tags matching excluded tag patterns from the provided scraped scenes
func filterSceneTags(scenes []*scraper.ScrapedScene) {
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())
var ignoredTags []string
for _, s := range scenes {
var ignored []string
s.Tags, ignored = filterTags(excludeRegexps, s.Tags)
ignoredTags = sliceutil.AppendUniques(ignoredTags, ignored)
}
if len(ignoredTags) > 0 {
logger.Debugf("Scraping ignored tags: %s", strings.Join(ignoredTags, ", "))
}
}
// filterGalleryTags removes tags matching excluded tag patterns from the provided scraped galleries
func filterGalleryTags(g []*scraper.ScrapedGallery) {
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())
var ignoredTags []string
for _, s := range g {
var ignored []string
s.Tags, ignored = filterTags(excludeRegexps, s.Tags)
ignoredTags = sliceutil.AppendUniques(ignoredTags, ignored)
}
if len(ignoredTags) > 0 {
logger.Debugf("Scraping ignored tags: %s", strings.Join(ignoredTags, ", "))
}
}
// filterGalleryTags removes tags matching excluded tag patterns from the provided scraped galleries
func filterPerformerTags(p []*models.ScrapedPerformer) {
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())
var ignoredTags []string
for _, s := range p {
var ignored []string
s.Tags, ignored = filterTags(excludeRegexps, s.Tags)
ignoredTags = sliceutil.AppendUniques(ignoredTags, ignored)
}
if len(ignoredTags) > 0 {
logger.Debugf("Scraping ignored tags: %s", strings.Join(ignoredTags, ", "))
}
}
func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*scraper.ScrapedScene, error) {
func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*models.ScrapedScene, error) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeScene)
if err != nil {
return nil, err
@@ -156,14 +59,10 @@ func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*scrape
return nil, err
}
if ret != nil {
filterSceneTags([]*scraper.ScrapedScene{ret})
}
return ret, nil
}
func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*scraper.ScrapedGallery, error) {
func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*models.ScrapedGallery, error) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeGallery)
if err != nil {
return nil, err
@@ -174,11 +73,16 @@ func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*scra
return nil, err
}
if ret != nil {
filterGalleryTags([]*scraper.ScrapedGallery{ret})
return ret, nil
}
func (r *queryResolver) ScrapeImageURL(ctx context.Context, url string) (*models.ScrapedImage, error) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeImage)
if err != nil {
return nil, err
}
return ret, nil
return marshalScrapedImage(content)
}
func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models.ScrapedMovie, error) {
@@ -187,23 +91,47 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models
return nil, err
}
return marshalScrapedMovie(content)
}
func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) {
boxes := config.GetInstance().GetStashBoxes()
if index < 0 || index >= len(boxes) {
return nil, fmt.Errorf("%w: invalid stash_box_index %d", ErrInput, index)
ret, err := marshalScrapedMovie(content)
if err != nil {
return nil, err
}
return stashbox.NewClient(*boxes[index], r.stashboxRepository()), nil
return ret, nil
}
// FIXME - in the following resolvers, we're processing the deprecated field and not processing the new endpoint input
func (r *queryResolver) ScrapeGroupURL(ctx context.Context, url string) (*models.ScrapedGroup, error) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeMovie)
if err != nil {
return nil, err
}
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*scraper.ScrapedScene, error) {
var ret []*scraper.ScrapedScene
ret, err := marshalScrapedMovie(content)
if err != nil {
return nil, err
}
// convert to scraped group
group := &models.ScrapedGroup{
StoredID: ret.StoredID,
Name: ret.Name,
Aliases: ret.Aliases,
Duration: ret.Duration,
Date: ret.Date,
Rating: ret.Rating,
Director: ret.Director,
URLs: ret.URLs,
Synopsis: ret.Synopsis,
Studio: ret.Studio,
Tags: ret.Tags,
FrontImage: ret.FrontImage,
BackImage: ret.BackImage,
}
return group, nil
}
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*models.ScrapedScene, error) {
var ret []*models.ScrapedScene
var sceneID int
if input.SceneID != nil {
@@ -245,17 +173,24 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So
if err != nil {
return nil, err
}
case source.StashBoxIndex != nil:
client, err := r.getStashBoxClient(*source.StashBoxIndex)
case source.StashBoxIndex != nil || source.StashBoxEndpoint != nil:
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
if err != nil {
return nil, err
}
client := r.newStashBoxClient(*b)
switch {
case input.SceneID != nil:
ret, err = client.FindStashBoxSceneByFingerprints(ctx, sceneID)
var fps []models.Fingerprints
fps, err = r.getScenesFingerprints(ctx, []int{sceneID})
if err != nil {
return nil, err
}
ret, err = client.FindSceneByFingerprints(ctx, fps[0])
case input.Query != nil:
ret, err = client.QueryStashBoxScene(ctx, *input.Query)
ret, err = client.QueryScene(ctx, *input.Query)
default:
return nil, fmt.Errorf("%w: scene_id or query must be set", ErrInput)
}
@@ -263,44 +198,128 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So
if err != nil {
return nil, err
}
// TODO - this should happen after any scene is scraped
if err := r.matchScenesRelationships(ctx, ret, *source.StashBoxEndpoint); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("%w: scraper_id or stash_box_index must be set", ErrInput)
}
filterSceneTags(ret)
return ret, nil
}
func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.Source, input ScrapeMultiScenesInput) ([][]*scraper.ScrapedScene, error) {
func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.Source, input ScrapeMultiScenesInput) ([][]*models.ScrapedScene, error) {
if source.ScraperID != nil {
return nil, ErrNotImplemented
} else if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
} else if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
if err != nil {
return nil, err
}
client := r.newStashBoxClient(*b)
sceneIDs, err := stringslice.StringSliceToIntSlice(input.SceneIds)
if err != nil {
return nil, err
}
return client.FindStashBoxScenesByFingerprints(ctx, sceneIDs)
fps, err := r.getScenesFingerprints(ctx, sceneIDs)
if err != nil {
return nil, err
}
ret, err := client.FindScenesByFingerprints(ctx, fps)
if err != nil {
return nil, err
}
// match relationships - this mutates the existing scenes so we can
// just flatten the slice and pass it in
flat := sliceutil.Flatten(ret)
if err := r.matchScenesRelationships(ctx, flat, *source.StashBoxEndpoint); err != nil {
return nil, err
}
return ret, nil
}
return nil, errors.New("scraper_id or stash_box_index must be set")
}
func (r *queryResolver) getScenesFingerprints(ctx context.Context, ids []int) ([]models.Fingerprints, error) {
fingerprints := make([]models.Fingerprints, len(ids))
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Scene
for i, sceneID := range ids {
scene, err := qb.Find(ctx, sceneID)
if err != nil {
return err
}
if scene == nil {
return fmt.Errorf("scene with id %d not found", sceneID)
}
if err := scene.LoadFiles(ctx, qb); err != nil {
return err
}
var sceneFPs models.Fingerprints
for _, f := range scene.Files.List() {
sceneFPs = append(sceneFPs, f.Fingerprints...)
}
fingerprints[i] = sceneFPs
}
return nil
}); err != nil {
return nil, err
}
return fingerprints, nil
}
// matchSceneRelationships accepts scraped scenes and attempts to match its relationships to existing stash models.
func (r *queryResolver) matchScenesRelationships(ctx context.Context, ss []*models.ScrapedScene, endpoint string) error {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
matcher := match.SceneRelationships{
PerformerFinder: r.repository.Performer,
TagFinder: r.repository.Tag,
StudioFinder: r.repository.Studio,
}
for _, s := range ss {
if err := matcher.MatchRelationships(ctx, s, endpoint); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.Source, input ScrapeSingleStudioInput) ([]*models.ScrapedStudio, error) {
if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
if err != nil {
return nil, err
}
client := r.newStashBoxClient(*b)
var ret []*models.ScrapedStudio
out, err := client.FindStashBoxStudio(ctx, *input.Query)
out, err := client.FindStudio(ctx, *input.Query)
if err != nil {
return nil, err
@@ -309,6 +328,17 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S
}
if len(ret) > 0 {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
for _, studio := range ret {
if err := match.ScrapedStudioHierarchy(ctx, r.repository.Studio, studio, *source.StashBoxEndpoint); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
@@ -346,58 +376,96 @@ func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source scrape
default:
return nil, ErrNotImplemented
}
// FIXME - we're relying on a deprecated field and not processing the endpoint input
case source.StashBoxIndex != nil:
client, err := r.getStashBoxClient(*source.StashBoxIndex)
case source.StashBoxIndex != nil || source.StashBoxEndpoint != nil:
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
if err != nil {
return nil, err
}
var res []*stashbox.StashBoxPerformerQueryResult
client := r.newStashBoxClient(*b)
var query string
switch {
case input.PerformerID != nil:
res, err = client.FindStashBoxPerformersByNames(ctx, []string{*input.PerformerID})
names, err := r.findPerformerNames(ctx, []string{*input.PerformerID})
if err != nil {
return nil, err
}
query = names[0]
case input.Query != nil:
res, err = client.QueryStashBoxPerformer(ctx, *input.Query)
query = *input.Query
default:
return nil, ErrNotImplemented
}
if query == "" {
return nil, nil
}
ret, err = client.QueryPerformer(ctx, query)
if err != nil {
return nil, err
}
if len(res) > 0 {
ret = res[0].Results
}
default:
return nil, errors.New("scraper_id or stash_box_index must be set")
}
filterPerformerTags(ret)
return ret, nil
}
func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source scraper.Source, input ScrapeMultiPerformersInput) ([][]*models.ScrapedPerformer, error) {
if source.ScraperID != nil {
return nil, ErrNotImplemented
} else if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
} else if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
names, err := r.findPerformerNames(ctx, input.PerformerIds)
if err != nil {
return nil, err
}
return client.FindStashBoxPerformersByPerformerNames(ctx, input.PerformerIds)
b, err := resolveStashBox(source.StashBoxIndex, source.StashBoxEndpoint)
if err != nil {
return nil, err
}
client := r.newStashBoxClient(*b)
return client.QueryPerformers(ctx, names)
}
return nil, errors.New("scraper_id or stash_box_index must be set")
}
func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.Source, input ScrapeSingleGalleryInput) ([]*scraper.ScrapedGallery, error) {
var ret []*scraper.ScrapedGallery
func (r *queryResolver) findPerformerNames(ctx context.Context, performerIDs []string) ([]string, error) {
ids, err := stringslice.StringSliceToIntSlice(performerIDs)
if err != nil {
return nil, err
}
if source.StashBoxIndex != nil {
names := make([]string, len(ids))
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
p, err := r.repository.Performer.FindMany(ctx, ids)
if err != nil {
return err
}
for i, pp := range p {
names[i] = pp.Name
}
return nil
}); err != nil {
return nil, err
}
return names, nil
}
func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.Source, input ScrapeSingleGalleryInput) ([]*models.ScrapedGallery, error) {
var ret []*models.ScrapedGallery
if source.StashBoxIndex != nil || source.StashBoxEndpoint != nil {
return nil, ErrNotSupported
}
@@ -434,10 +502,46 @@ func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.
return nil, ErrNotImplemented
}
filterGalleryTags(ret)
return ret, nil
}
func (r *queryResolver) ScrapeSingleImage(ctx context.Context, source scraper.Source, input ScrapeSingleImageInput) ([]*models.ScrapedImage, error) {
if source.StashBoxIndex != nil {
return nil, ErrNotSupported
}
if source.ScraperID == nil {
return nil, fmt.Errorf("%w: scraper_id must be set", ErrInput)
}
var c scraper.ScrapedContent
switch {
case input.ImageID != nil:
imageID, err := strconv.Atoi(*input.ImageID)
if err != nil {
return nil, fmt.Errorf("%w: image id is not an integer: '%s'", ErrInput, *input.ImageID)
}
c, err = r.scraperCache().ScrapeID(ctx, *source.ScraperID, imageID, scraper.ScrapeContentTypeImage)
if err != nil {
return nil, err
}
return marshalScrapedImages([]scraper.ScrapedContent{c})
case input.ImageInput != nil:
c, err := r.scraperCache().ScrapeFragment(ctx, *source.ScraperID, scraper.Input{Image: input.ImageInput})
if err != nil {
return nil, err
}
return marshalScrapedImages([]scraper.ScrapedContent{c})
default:
return nil, ErrNotImplemented
}
}
func (r *queryResolver) ScrapeSingleMovie(ctx context.Context, source scraper.Source, input ScrapeSingleMovieInput) ([]*models.ScrapedMovie, error) {
return nil, ErrNotSupported
}
func (r *queryResolver) ScrapeSingleGroup(ctx context.Context, source scraper.Source, input ScrapeSingleGroupInput) ([]*models.ScrapedGroup, error) {
return nil, ErrNotSupported
}

View File

@@ -0,0 +1,159 @@
package api
import (
"context"
"errors"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/internal/static"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
type GalleryFinder interface {
models.GalleryGetter
FindByChecksum(ctx context.Context, checksum string) ([]*models.Gallery, error)
}
type GalleryImageFinder interface {
FindByGalleryIDIndex(ctx context.Context, galleryID int, index uint) (*models.Image, error)
image.Queryer
image.CoverQueryer
}
type galleryRoutes struct {
routes
imageRoutes imageRoutes
galleryFinder GalleryFinder
imageFinder GalleryImageFinder
fileGetter models.FileGetter
}
func (rs galleryRoutes) Routes() chi.Router {
r := chi.NewRouter()
r.Route("/{galleryId}", func(r chi.Router) {
r.Use(rs.GalleryCtx)
r.Get("/cover", rs.Cover)
r.Get("/preview/{imageIndex}", rs.Preview)
})
return r
}
func (rs galleryRoutes) Cover(w http.ResponseWriter, r *http.Request) {
g := r.Context().Value(galleryKey).(*models.Gallery)
var i *models.Image
_ = rs.withReadTxn(r, func(ctx context.Context) error {
// Find cover image first
i, _ = image.FindGalleryCover(ctx, rs.imageFinder, g.ID, config.GetInstance().GetGalleryCoverRegex())
if i == nil {
return nil
}
// serveThumbnail needs files populated
if err := i.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
if !errors.Is(err, context.Canceled) {
logger.Errorf("error loading primary file for image %d: %v", i.ID, err)
}
// set image to nil so that it doesn't try to use the primary file
i = nil
}
return nil
})
if i == nil {
// fallback to default image
image := static.ReadAll(static.DefaultGalleryImage)
utils.ServeImage(w, r, image)
return
}
rs.imageRoutes.serveThumbnail(w, r, i, &g.UpdatedAt)
}
func (rs galleryRoutes) Preview(w http.ResponseWriter, r *http.Request) {
g := r.Context().Value(galleryKey).(*models.Gallery)
indexQueryParam := chi.URLParam(r, "imageIndex")
var i *models.Image
index, err := strconv.Atoi(indexQueryParam)
if err != nil || index < 0 {
http.Error(w, "bad index", 400)
return
}
_ = rs.withReadTxn(r, func(ctx context.Context) error {
qb := rs.imageFinder
i, _ = qb.FindByGalleryIDIndex(ctx, g.ID, uint(index))
if i == nil {
return nil
}
// TODO - handle errors?
// serveThumbnail needs files populated
if err := i.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
if !errors.Is(err, context.Canceled) {
logger.Errorf("error loading primary file for image %d: %v", i.ID, err)
}
// set image to nil so that it doesn't try to use the primary file
i = nil
}
return nil
})
if i == nil {
http.Error(w, http.StatusText(404), 404)
return
}
rs.imageRoutes.serveThumbnail(w, r, i, nil)
}
func (rs galleryRoutes) GalleryCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
galleryIdentifierQueryParam := chi.URLParam(r, "galleryId")
galleryID, _ := strconv.Atoi(galleryIdentifierQueryParam)
var gallery *models.Gallery
_ = rs.withReadTxn(r, func(ctx context.Context) error {
qb := rs.galleryFinder
if galleryID == 0 {
galleries, _ := qb.FindByChecksum(ctx, galleryIdentifierQueryParam)
if len(galleries) > 0 {
gallery = galleries[0]
}
} else {
gallery, _ = qb.Find(ctx, galleryID)
}
if gallery != nil {
if err := gallery.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
if !errors.Is(err, context.Canceled) {
logger.Errorf("error loading primary file for gallery %d: %v", galleryID, err)
}
// set image to nil so that it doesn't try to use the primary file
gallery = nil
}
}
return nil
})
if gallery == nil {
http.Error(w, http.StatusText(404), 404)
return
}
ctx := context.WithValue(r.Context(), galleryKey, gallery)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -14,22 +14,22 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
type MovieFinder interface {
models.MovieGetter
GetFrontImage(ctx context.Context, movieID int) ([]byte, error)
GetBackImage(ctx context.Context, movieID int) ([]byte, error)
type GroupFinder interface {
models.GroupGetter
GetFrontImage(ctx context.Context, groupID int) ([]byte, error)
GetBackImage(ctx context.Context, groupID int) ([]byte, error)
}
type movieRoutes struct {
type groupRoutes struct {
routes
movieFinder MovieFinder
groupFinder GroupFinder
}
func (rs movieRoutes) Routes() chi.Router {
func (rs groupRoutes) Routes() chi.Router {
r := chi.NewRouter()
r.Route("/{movieId}", func(r chi.Router) {
r.Use(rs.MovieCtx)
r.Route("/{groupId}", func(r chi.Router) {
r.Use(rs.GroupCtx)
r.Get("/frontimage", rs.FrontImage)
r.Get("/backimage", rs.BackImage)
})
@@ -37,77 +37,77 @@ func (rs movieRoutes) Routes() chi.Router {
return r
}
func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
movie := r.Context().Value(movieKey).(*models.Movie)
func (rs groupRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
group := r.Context().Value(groupKey).(*models.Group)
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
image, err = rs.movieFinder.GetFrontImage(ctx, movie.ID)
image, err = rs.groupFinder.GetFrontImage(ctx, group.ID)
return err
})
if errors.Is(readTxnErr, context.Canceled) {
return
}
if readTxnErr != nil {
logger.Warnf("read transaction error on fetch movie front image: %v", readTxnErr)
logger.Warnf("read transaction error on fetch group front image: %v", readTxnErr)
}
}
// fallback to default image
if len(image) == 0 {
image = static.ReadAll(static.DefaultMovieImage)
image = static.ReadAll(static.DefaultGroupImage)
}
utils.ServeImage(w, r, image)
}
func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
movie := r.Context().Value(movieKey).(*models.Movie)
func (rs groupRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
group := r.Context().Value(groupKey).(*models.Group)
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
readTxnErr := rs.withReadTxn(r, func(ctx context.Context) error {
var err error
image, err = rs.movieFinder.GetBackImage(ctx, movie.ID)
image, err = rs.groupFinder.GetBackImage(ctx, group.ID)
return err
})
if errors.Is(readTxnErr, context.Canceled) {
return
}
if readTxnErr != nil {
logger.Warnf("read transaction error on fetch movie back image: %v", readTxnErr)
logger.Warnf("read transaction error on fetch group back image: %v", readTxnErr)
}
}
// fallback to default image
if len(image) == 0 {
image = static.ReadAll(static.DefaultMovieImage)
image = static.ReadAll(static.DefaultGroupImage)
}
utils.ServeImage(w, r, image)
}
func (rs movieRoutes) MovieCtx(next http.Handler) http.Handler {
func (rs groupRoutes) GroupCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
movieID, err := strconv.Atoi(chi.URLParam(r, "movieId"))
groupID, err := strconv.Atoi(chi.URLParam(r, "groupId"))
if err != nil {
http.Error(w, http.StatusText(404), 404)
return
}
var movie *models.Movie
var group *models.Group
_ = rs.withReadTxn(r, func(ctx context.Context) error {
movie, _ = rs.movieFinder.Find(ctx, movieID)
group, _ = rs.groupFinder.Find(ctx, groupID)
return nil
})
if movie == nil {
if group == nil {
http.Error(w, http.StatusText(404), 404)
return
}
ctx := context.WithValue(r.Context(), movieKey, movie)
ctx := context.WithValue(r.Context(), groupKey, group)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"os/exec"
"strconv"
"time"
"github.com/go-chi/chi/v5"
@@ -46,14 +47,22 @@ func (rs imageRoutes) Routes() chi.Router {
}
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
mgr := manager.GetInstance()
img := r.Context().Value(imageKey).(*models.Image)
rs.serveThumbnail(w, r, img, nil)
}
func (rs imageRoutes) serveThumbnail(w http.ResponseWriter, r *http.Request, img *models.Image, modTime *time.Time) {
mgr := manager.GetInstance()
filepath := mgr.Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
// if the thumbnail doesn't exist, encode on the fly
exists, _ := fsutil.FileExists(filepath)
if exists {
utils.ServeStaticFile(w, r, filepath)
if modTime == nil {
utils.ServeStaticFile(w, r, filepath)
} else {
utils.ServeStaticFileModTime(w, r, filepath, *modTime)
}
} else {
const useDefault = true

View File

@@ -9,8 +9,8 @@ import (
// marshalScrapedScenes converts ScrapedContent into ScrapedScene. If conversion fails, an
// error is returned to the caller.
func marshalScrapedScenes(content []scraper.ScrapedContent) ([]*scraper.ScrapedScene, error) {
var ret []*scraper.ScrapedScene
func marshalScrapedScenes(content []scraper.ScrapedContent) ([]*models.ScrapedScene, error) {
var ret []*models.ScrapedScene
for _, c := range content {
if c == nil {
// graphql schema requires scenes to be non-nil
@@ -18,9 +18,9 @@ func marshalScrapedScenes(content []scraper.ScrapedContent) ([]*scraper.ScrapedS
}
switch s := c.(type) {
case *scraper.ScrapedScene:
case *models.ScrapedScene:
ret = append(ret, s)
case scraper.ScrapedScene:
case models.ScrapedScene:
ret = append(ret, &s)
default:
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedScene", models.ErrConversion)
@@ -55,8 +55,8 @@ func marshalScrapedPerformers(content []scraper.ScrapedContent) ([]*models.Scrap
// marshalScrapedGalleries converts ScrapedContent into ScrapedGallery. If
// conversion fails, an error is returned.
func marshalScrapedGalleries(content []scraper.ScrapedContent) ([]*scraper.ScrapedGallery, error) {
var ret []*scraper.ScrapedGallery
func marshalScrapedGalleries(content []scraper.ScrapedContent) ([]*models.ScrapedGallery, error) {
var ret []*models.ScrapedGallery
for _, c := range content {
if c == nil {
// graphql schema requires galleries to be non-nil
@@ -64,9 +64,9 @@ func marshalScrapedGalleries(content []scraper.ScrapedContent) ([]*scraper.Scrap
}
switch g := c.(type) {
case *scraper.ScrapedGallery:
case *models.ScrapedGallery:
ret = append(ret, g)
case scraper.ScrapedGallery:
case models.ScrapedGallery:
ret = append(ret, &g)
default:
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedGallery", models.ErrConversion)
@@ -76,6 +76,27 @@ func marshalScrapedGalleries(content []scraper.ScrapedContent) ([]*scraper.Scrap
return ret, nil
}
func marshalScrapedImages(content []scraper.ScrapedContent) ([]*models.ScrapedImage, error) {
var ret []*models.ScrapedImage
for _, c := range content {
if c == nil {
// graphql schema requires images to be non-nil
continue
}
switch g := c.(type) {
case *models.ScrapedImage:
ret = append(ret, g)
case models.ScrapedImage:
ret = append(ret, &g)
default:
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedImage", models.ErrConversion)
}
}
return ret, nil
}
// marshalScrapedMovies converts ScrapedContent into ScrapedMovie. If conversion
// fails, an error is returned.
func marshalScrapedMovies(content []scraper.ScrapedContent) ([]*models.ScrapedMovie, error) {
@@ -110,7 +131,7 @@ func marshalScrapedPerformer(content scraper.ScrapedContent) (*models.ScrapedPer
}
// marshalScrapedScene will marshal a single scraped scene
func marshalScrapedScene(content scraper.ScrapedContent) (*scraper.ScrapedScene, error) {
func marshalScrapedScene(content scraper.ScrapedContent) (*models.ScrapedScene, error) {
s, err := marshalScrapedScenes([]scraper.ScrapedContent{content})
if err != nil {
return nil, err
@@ -120,7 +141,7 @@ func marshalScrapedScene(content scraper.ScrapedContent) (*scraper.ScrapedScene,
}
// marshalScrapedGallery will marshal a single scraped gallery
func marshalScrapedGallery(content scraper.ScrapedContent) (*scraper.ScrapedGallery, error) {
func marshalScrapedGallery(content scraper.ScrapedContent) (*models.ScrapedGallery, error) {
g, err := marshalScrapedGalleries([]scraper.ScrapedContent{content})
if err != nil {
return nil, err
@@ -129,6 +150,16 @@ func marshalScrapedGallery(content scraper.ScrapedContent) (*scraper.ScrapedGall
return g[0], nil
}
// marshalScrapedImage will marshal a single scraped image
func marshalScrapedImage(content scraper.ScrapedContent) (*models.ScrapedImage, error) {
g, err := marshalScrapedImages([]scraper.ScrapedContent{content})
if err != nil {
return nil, err
}
return g[0], nil
}
// marshalScrapedMovie will marshal a single scraped movie
func marshalScrapedMovie(content scraper.ScrapedContent) (*models.ScrapedMovie, error) {
m, err := marshalScrapedMovies([]scraper.ScrapedContent{content})

View File

@@ -27,6 +27,7 @@ import (
"github.com/go-chi/httplog"
"github.com/gorilla/websocket"
"github.com/vearutop/statigz"
"github.com/vektah/gqlparser/v2/ast"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/build"
@@ -40,10 +41,11 @@ import (
)
const (
loginEndpoint = "/login"
logoutEndpoint = "/logout"
gqlEndpoint = "/graphql"
playgroundEndpoint = "/playground"
loginEndpoint = "/login"
loginLocaleEndpoint = loginEndpoint + "/locale"
logoutEndpoint = "/logout"
gqlEndpoint = "/graphql"
playgroundEndpoint = "/playground"
)
type Server struct {
@@ -75,7 +77,8 @@ func (dir osFS) Open(name string) (fs.File, error) {
return os.DirFS(string(dir)).Open(name)
}
// Called at startup
// Initialize creates a new [Server] instance.
// It assumes that the [manager.Manager] instance has been initialised.
func Initialize() (*Server, error) {
mgr := manager.GetInstance()
cfg := mgr.Config
@@ -157,11 +160,13 @@ func Initialize() (*Server, error) {
sceneService := mgr.SceneService
imageService := mgr.ImageService
galleryService := mgr.GalleryService
groupService := mgr.GroupService
resolver := &Resolver{
repository: repo,
sceneService: sceneService,
imageService: imageService,
galleryService: galleryService,
groupService: groupService,
hookExecutor: pluginCache,
}
@@ -182,7 +187,7 @@ func Initialize() (*Server, error) {
MaxUploadSize: cfg.GetMaxUploadSize(),
})
gqlSrv.SetQueryCache(gqlLru.New(1000))
gqlSrv.SetQueryCache(gqlLru.New[*ast.QueryDocument](1000))
gqlSrv.Use(gqlExtension.Introspection{})
gqlSrv.SetErrorPresenter(gqlErrorHandler)
@@ -207,9 +212,10 @@ func Initialize() (*Server, error) {
r.Mount("/performer", server.getPerformerRoutes())
r.Mount("/scene", server.getSceneRoutes())
r.Mount("/gallery", server.getGalleryRoutes())
r.Mount("/image", server.getImageRoutes())
r.Mount("/studio", server.getStudioRoutes())
r.Mount("/movie", server.getMovieRoutes())
r.Mount("/group", server.getGroupRoutes())
r.Mount("/tag", server.getTagRoutes())
r.Mount("/downloads", server.getDownloadsRoutes())
r.Mount("/plugin", server.getPluginRoutes())
@@ -223,6 +229,7 @@ func Initialize() (*Server, error) {
r.Get(loginEndpoint, handleLogin())
r.Post(loginEndpoint, handleLoginPost())
r.Get(logoutEndpoint, handleLogout())
r.Get(loginLocaleEndpoint, handleLoginLocale(cfg))
r.HandleFunc(loginEndpoint+"/*", func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.TrimPrefix(r.URL.Path, loginEndpoint)
w.Header().Set("Cache-Control", "no-cache")
@@ -288,6 +295,9 @@ func Initialize() (*Server, error) {
return server, nil
}
// Start starts the server. It listens on the configured address and port.
// It calls ListenAndServeTLS if TLS is configured, otherwise it calls ListenAndServe.
// Calls to Start are blocked until the server is shutdown.
func (s *Server) Start() error {
logger.Infof("stash is listening on " + s.Addr)
logger.Infof("stash is running at " + s.displayAddress)
@@ -299,6 +309,7 @@ func (s *Server) Start() error {
}
}
// Shutdown gracefully shuts down the server without interrupting any active connections.
func (s *Server) Shutdown() {
err := s.Server.Shutdown(context.TODO())
if err != nil {
@@ -326,6 +337,16 @@ func (s *Server) getSceneRoutes() chi.Router {
}.Routes()
}
func (s *Server) getGalleryRoutes() chi.Router {
repo := s.manager.Repository
return galleryRoutes{
routes: routes{txnManager: repo.TxnManager},
imageFinder: repo.Image,
galleryFinder: repo.Gallery,
fileGetter: repo.File,
}.Routes()
}
func (s *Server) getImageRoutes() chi.Router {
repo := s.manager.Repository
return imageRoutes{
@@ -343,11 +364,11 @@ func (s *Server) getStudioRoutes() chi.Router {
}.Routes()
}
func (s *Server) getMovieRoutes() chi.Router {
func (s *Server) getGroupRoutes() chi.Router {
repo := s.manager.Repository
return movieRoutes{
return groupRoutes{
routes: routes{txnManager: repo.TxnManager},
movieFinder: repo.Movie,
groupFinder: repo.Group,
}.Routes()
}

View File

@@ -17,7 +17,11 @@ import (
"github.com/stashapp/stash/ui"
)
const returnURLParam = "returnURL"
const (
returnURLParam = "returnURL"
defaultLocale = "en-GB"
)
func getLoginPage() []byte {
data, err := fs.ReadFile(ui.LoginUIBox, "login.html")
@@ -58,6 +62,47 @@ func serveLoginPage(w http.ResponseWriter, r *http.Request, returnURL string, lo
utils.ServeStaticContent(w, r, buffer.Bytes())
}
func handleLoginLocale(cfg *config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// get the locale from the config
lang := cfg.GetLanguage()
if lang == "" {
lang = defaultLocale
}
data, err := getLoginLocale(lang)
if err != nil {
logger.Debugf("Failed to load login locale file for language %s: %v", lang, err)
// try again with the default language
if lang != defaultLocale {
data, err = getLoginLocale(defaultLocale)
if err != nil {
logger.Errorf("Failed to load login locale file for default language %s: %v", defaultLocale, err)
}
}
// if there's still an error, response with an internal server error
if err != nil {
http.Error(w, "Failed to load login locale file", http.StatusInternalServerError)
return
}
}
// write a script to set the locale string map as a global variable
localeScript := fmt.Sprintf("var localeStrings = %s;", data)
w.Header().Set("Content-Type", "application/javascript")
_, _ = w.Write([]byte(localeScript))
}
}
func getLoginLocale(lang string) ([]byte, error) {
data, err := fs.ReadFile(ui.LoginUIBox, "locales/"+lang+".json")
if err != nil {
return nil, err
}
return data, nil
}
func handleLogin() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
returnURL := r.URL.Query().Get(returnURLParam)
@@ -78,31 +123,26 @@ func handleLogin() http.HandlerFunc {
func handleLoginPost() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
url := r.FormValue(returnURLParam)
if url == "" {
url = getProxyPrefix(r) + "/"
}
err := manager.GetInstance().SessionStore.Login(w, r)
if err != nil {
// always log the error
logger.Errorf("Error logging in: %v", err)
logger.Errorf("Error logging in: %v from IP: %s", err, r.RemoteAddr)
}
var invalidCredentialsError *session.InvalidCredentialsError
if errors.As(err, &invalidCredentialsError) {
// serve login page with an error
serveLoginPage(w, r, url, "Username or password is invalid")
http.Error(w, "Username or password is invalid", http.StatusUnauthorized)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
// don't expose the error to the user
http.Error(w, "An unexpected error occurred. See logs", http.StatusInternalServerError)
return
}
http.Redirect(w, r, url, http.StatusFound)
w.WriteHeader(http.StatusOK)
}
}

46
internal/api/stash_box.go Normal file
View File

@@ -0,0 +1,46 @@
package api
import (
"fmt"
"strings"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/stashbox"
)
func (r *Resolver) newStashBoxClient(box models.StashBox) *stashbox.Client {
return stashbox.NewClient(box, stashbox.ExcludeTagPatterns(manager.GetInstance().Config.GetScraperExcludeTagPatterns()))
}
func resolveStashBoxFn(indexField, endpointField string) func(index *int, endpoint *string) (*models.StashBox, error) {
return func(index *int, endpoint *string) (*models.StashBox, error) {
boxes := config.GetInstance().GetStashBoxes()
// prefer endpoint over index
if endpoint != nil {
for _, box := range boxes {
if strings.EqualFold(*endpoint, box.Endpoint) {
return box, nil
}
}
return nil, fmt.Errorf("stash box not found")
}
if index != nil {
if *index < 0 || *index >= len(boxes) {
return nil, fmt.Errorf("invalid %s %d", indexField, index)
}
return boxes[*index], nil
}
return nil, fmt.Errorf("%s not provided", endpointField)
}
}
var (
resolveStashBox = resolveStashBoxFn("stash_box_index", "stash_box_endpoint")
resolveStashBoxBatchTagInput = resolveStashBoxFn("endpoint", "stash_box_endpoint")
)

View File

@@ -0,0 +1,2 @@
// Package urlbuilders provides the builders used to build URLs to pass to clients.
package urlbuilders

View File

@@ -0,0 +1,27 @@
package urlbuilders
import (
"strconv"
"github.com/stashapp/stash/pkg/models"
)
type GalleryURLBuilder struct {
BaseURL string
GalleryID string
}
func NewGalleryURLBuilder(baseURL string, gallery *models.Gallery) GalleryURLBuilder {
return GalleryURLBuilder{
BaseURL: baseURL,
GalleryID: strconv.Itoa(gallery.ID),
}
}
func (b GalleryURLBuilder) GetPreviewURL() string {
return b.BaseURL + "/gallery/" + b.GalleryID + "/preview"
}
func (b GalleryURLBuilder) GetCoverURL() string {
return b.BaseURL + "/gallery/" + b.GalleryID + "/cover"
}

View File

@@ -0,0 +1,33 @@
package urlbuilders
import (
"strconv"
"github.com/stashapp/stash/pkg/models"
)
type GroupURLBuilder struct {
BaseURL string
GroupID string
UpdatedAt string
}
func NewGroupURLBuilder(baseURL string, group *models.Group) GroupURLBuilder {
return GroupURLBuilder{
BaseURL: baseURL,
GroupID: strconv.Itoa(group.ID),
UpdatedAt: strconv.FormatInt(group.UpdatedAt.Unix(), 10),
}
}
func (b GroupURLBuilder) GetGroupFrontImageURL(hasImage bool) string {
url := b.BaseURL + "/group/" + b.GroupID + "/frontimage?t=" + b.UpdatedAt
if !hasImage {
url += "&default=true"
}
return url
}
func (b GroupURLBuilder) GetGroupBackImageURL() string {
return b.BaseURL + "/group/" + b.GroupID + "/backimage?t=" + b.UpdatedAt
}

View File

@@ -1,32 +0,0 @@
package urlbuilders
import (
"github.com/stashapp/stash/pkg/models"
"strconv"
)
type MovieURLBuilder struct {
BaseURL string
MovieID string
UpdatedAt string
}
func NewMovieURLBuilder(baseURL string, movie *models.Movie) MovieURLBuilder {
return MovieURLBuilder{
BaseURL: baseURL,
MovieID: strconv.Itoa(movie.ID),
UpdatedAt: strconv.FormatInt(movie.UpdatedAt.Unix(), 10),
}
}
func (b MovieURLBuilder) GetMovieFrontImageURL(hasImage bool) string {
url := b.BaseURL + "/movie/" + b.MovieID + "/frontimage?t=" + b.UpdatedAt
if !hasImage {
url += "&default=true"
}
return url
}
func (b MovieURLBuilder) GetMovieBackImageURL() string {
return b.BaseURL + "/movie/" + b.MovieID + "/backimage?t=" + b.UpdatedAt
}

9
internal/autotag/doc.go Normal file
View File

@@ -0,0 +1,9 @@
// Package autotag provides the autotagging functionality for the application.
//
// The autotag functionality sets media metadata based on the media's path.
// The functions in this package are in the form of {ObjectType}{TagTypes},
// where the ObjectType is the single object instance to run on, and TagTypes
// are the related types.
// For example, PerformerScenes finds and tags scenes with a provided performer,
// whereas ScenePerformers tags a single scene with any Performers that match.
package autotag

View File

@@ -2,11 +2,11 @@ package autotag
import (
"context"
"slices"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil"
)
type GalleryFinderUpdater interface {
@@ -53,7 +53,7 @@ func GalleryPerformers(ctx context.Context, s *models.Gallery, rw GalleryPerform
}
existing := s.PerformerIDs.List()
if sliceutil.Contains(existing, otherID) {
if slices.Contains(existing, otherID) {
return false, nil
}
@@ -91,7 +91,7 @@ func GalleryTags(ctx context.Context, s *models.Gallery, rw GalleryTagUpdater, t
}
existing := s.TagIDs.List()
if sliceutil.Contains(existing, otherID) {
if slices.Contains(existing, otherID) {
return false, nil
}

View File

@@ -2,11 +2,11 @@ package autotag
import (
"context"
"slices"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil"
)
type ImageFinderUpdater interface {
@@ -44,7 +44,7 @@ func ImagePerformers(ctx context.Context, s *models.Image, rw ImagePerformerUpda
}
existing := s.PerformerIDs.List()
if sliceutil.Contains(existing, otherID) {
if slices.Contains(existing, otherID) {
return false, nil
}
@@ -82,7 +82,7 @@ func ImageTags(ctx context.Context, s *models.Image, rw ImageTagUpdater, tagRead
}
existing := s.TagIDs.List()
if sliceutil.Contains(existing, otherID) {
if slices.Contains(existing, otherID) {
return false, nil
}

View File

@@ -91,7 +91,7 @@ func createPerformer(ctx context.Context, pqb models.PerformerWriter) error {
Name: testName,
}
err := pqb.Create(ctx, &performer)
err := pqb.Create(ctx, &models.CreatePerformerInput{Performer: &performer})
if err != nil {
return err
}

View File

@@ -2,13 +2,13 @@ package autotag
import (
"context"
"slices"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/txn"
)
@@ -63,7 +63,7 @@ func (tagger *Tagger) PerformerScenes(ctx context.Context, p *models.Performer,
}
existing := o.PerformerIDs.List()
if sliceutil.Contains(existing, p.ID) {
if slices.Contains(existing, p.ID) {
return false, nil
}
@@ -92,7 +92,7 @@ func (tagger *Tagger) PerformerImages(ctx context.Context, p *models.Performer,
}
existing := o.PerformerIDs.List()
if sliceutil.Contains(existing, p.ID) {
if slices.Contains(existing, p.ID) {
return false, nil
}
@@ -121,7 +121,7 @@ func (tagger *Tagger) PerformerGalleries(ctx context.Context, p *models.Performe
}
existing := o.PerformerIDs.List()
if sliceutil.Contains(existing, p.ID) {
if slices.Contains(existing, p.ID) {
return false, nil
}

View File

@@ -2,11 +2,11 @@ package autotag
import (
"context"
"slices"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil"
)
type SceneFinderUpdater interface {
@@ -44,7 +44,7 @@ func ScenePerformers(ctx context.Context, s *models.Scene, rw ScenePerformerUpda
}
existing := s.PerformerIDs.List()
if sliceutil.Contains(existing, otherID) {
if slices.Contains(existing, otherID) {
return false, nil
}
@@ -82,7 +82,7 @@ func SceneTags(ctx context.Context, s *models.Scene, rw SceneTagUpdater, tagRead
}
existing := s.TagIDs.List()
if sliceutil.Contains(existing, otherID) {
if slices.Contains(existing, otherID) {
return false, nil
}

View File

@@ -2,13 +2,13 @@ package autotag
import (
"context"
"slices"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/txn"
)
@@ -61,7 +61,7 @@ func (tagger *Tagger) TagScenes(ctx context.Context, p *models.Tag, paths []stri
}
existing := o.TagIDs.List()
if sliceutil.Contains(existing, p.ID) {
if slices.Contains(existing, p.ID) {
return false, nil
}
@@ -90,7 +90,7 @@ func (tagger *Tagger) TagImages(ctx context.Context, p *models.Tag, paths []stri
}
existing := o.TagIDs.List()
if sliceutil.Contains(existing, p.ID) {
if slices.Contains(existing, p.ID) {
return false, nil
}
@@ -119,7 +119,7 @@ func (tagger *Tagger) TagGalleries(ctx context.Context, p *models.Tag, paths []s
}
existing := o.TagIDs.List()
if sliceutil.Contains(existing, p.ID) {
if slices.Contains(existing, p.ID) {
return false, nil
}

View File

@@ -1,3 +1,4 @@
// Package build provides the version information for the application.
package build
import (

View File

@@ -1,3 +1,4 @@
// Package desktop provides desktop integration functionality for the application.
package desktop
import (

View File

@@ -30,6 +30,7 @@ import (
"os"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
@@ -40,7 +41,6 @@ import (
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil"
)
var pageSize = 100
@@ -192,7 +192,7 @@ func (me *contentDirectoryService) Handle(action string, argsXML []byte, r *http
obj, err := me.objectFromID(browse.ObjectID)
if err != nil {
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error())
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, "cannot find object with id %q: %v", browse.ObjectID, err.Error())
}
switch browse.BrowseFlag {
@@ -316,13 +316,13 @@ func (me *contentDirectoryService) handleBrowseDirectChildren(obj object, host s
objs = me.getPerformerScenes(childPath(paths), host)
}
// Movies
if obj.Path == "movies" {
objs = me.getMovies()
// Groups - deprecated
if obj.Path == "groups" {
objs = me.getGroups()
}
if strings.HasPrefix(obj.Path, "movies/") {
objs = me.getMovieScenes(childPath(paths), host)
if strings.HasPrefix(obj.Path, "groups/") {
objs = me.getGroupScenes(childPath(paths), host)
}
// Rating
@@ -433,7 +433,7 @@ func getRootObjects() []interface{} {
objs = append(objs, makeStorageFolder("performers", "performers", rootID))
objs = append(objs, makeStorageFolder("tags", "tags", rootID))
objs = append(objs, makeStorageFolder("studios", "studios", rootID))
objs = append(objs, makeStorageFolder("movies", "movies", rootID))
objs = append(objs, makeStorageFolder("groups", "groups", rootID))
objs = append(objs, makeStorageFolder("rating", "rating", rootID))
return objs
@@ -521,7 +521,7 @@ func (me *contentDirectoryService) getPageVideos(sceneFilter *models.SceneFilter
}
func getPageFromID(paths []string) *int {
i := sliceutil.Index(paths, "page")
i := slices.Index(paths, "page")
if i == -1 || i+1 >= len(paths) {
return nil
}
@@ -658,18 +658,18 @@ func (me *contentDirectoryService) getPerformerScenes(paths []string, host strin
return me.getVideos(sceneFilter, parentID, host)
}
func (me *contentDirectoryService) getMovies() []interface{} {
func (me *contentDirectoryService) getGroups() []interface{} {
var objs []interface{}
r := me.repository
if err := r.WithReadTxn(context.TODO(), func(ctx context.Context) error {
movies, err := r.MovieFinder.All(ctx)
groups, err := r.GroupFinder.All(ctx)
if err != nil {
return err
}
for _, s := range movies {
objs = append(objs, makeStorageFolder("movies/"+strconv.Itoa(s.ID), s.Name, "movies"))
for _, s := range groups {
objs = append(objs, makeStorageFolder("groups/"+strconv.Itoa(s.ID), s.Name, "groups"))
}
return nil
@@ -680,15 +680,15 @@ func (me *contentDirectoryService) getMovies() []interface{} {
return objs
}
func (me *contentDirectoryService) getMovieScenes(paths []string, host string) []interface{} {
func (me *contentDirectoryService) getGroupScenes(paths []string, host string) []interface{} {
sceneFilter := &models.SceneFilterType{
Movies: &models.MultiCriterionInput{
Groups: &models.HierarchicalMultiCriterionInput{
Modifier: models.CriterionModifierIncludes,
Value: []string{paths[0]},
},
}
parentID := "movies/" + strings.Join(paths, "/")
parentID := "groups/" + strings.Join(paths, "/")
page := getPageFromID(paths)
if page != nil {

View File

@@ -40,6 +40,7 @@ import (
"path"
"strconv"
"strings"
"sync"
"time"
"github.com/anacrolix/dms/soap"
@@ -67,8 +68,8 @@ type PerformerFinder interface {
All(ctx context.Context) ([]*models.Performer, error)
}
type MovieFinder interface {
All(ctx context.Context) ([]*models.Movie, error)
type GroupFinder interface {
All(ctx context.Context) ([]*models.Group, error)
}
const (
@@ -229,6 +230,10 @@ func (me *Server) ssdpInterface(if_ net.Interface) {
stopped := make(chan struct{})
go func() {
defer close(stopped)
// FIXME - this currently blocks forever unless it encounters an error
// See https://github.com/anacrolix/dms/pull/150
// Needs to be fixed upstream
//nolint:staticcheck
if err := s.Serve(); err != nil {
logger.Errorf("%q: %q\n", if_.Name, err)
}
@@ -274,6 +279,8 @@ type Server struct {
sceneServer sceneServer
ipWhitelistManager *ipWhitelistManager
VideoSortOrder string
subscribeLock sync.Mutex
}
// UPnP SOAP service.
@@ -537,13 +544,14 @@ func (me *Server) contentDirectoryEventSubHandler(w http.ResponseWriter, r *http
// The following code is a work in progress. It partially implements
// the spec on eventing but hasn't been completed as I have nothing to
// test it with.
service := me.services["ContentDirectory"]
switch {
case r.Method == "SUBSCRIBE" && r.Header.Get("SID") == "":
urls := upnp.ParseCallbackURLs(r.Header.Get("CALLBACK"))
var timeout int
_, _ = fmt.Sscanf(r.Header.Get("TIMEOUT"), "Second-%d", &timeout)
sid, timeout, _ := service.Subscribe(urls, timeout)
sid, timeout, _ := me.subscribe(urls, timeout)
w.Header()["SID"] = []string{sid}
w.Header()["TIMEOUT"] = []string{fmt.Sprintf("Second-%d", timeout)}
// TODO: Shouldn't have to do this to get headers logged.
@@ -559,6 +567,16 @@ func (me *Server) contentDirectoryEventSubHandler(w http.ResponseWriter, r *http
}
}
// wrapper around service.Subscribe which requires concurrency protection
// TODO - this should be addressed upstream
func (me *Server) subscribe(urls []*url.URL, timeout int) (sid string, actualTimeout int, err error) {
me.subscribeLock.Lock()
defer me.subscribeLock.Unlock()
service := me.services["ContentDirectory"]
return service.Subscribe(urls, timeout)
}
func (me *Server) initMux(mux *http.ServeMux) {
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("content-type", "text/html")

3
internal/dlna/doc.go Normal file
View File

@@ -0,0 +1,3 @@
// Package dlna provides the DLNA functionality for the application.
// Much of this code is adapted from https://github.com/anacrolix/dms
package dlna

View File

@@ -22,7 +22,7 @@ type Repository struct {
StudioFinder StudioFinder
TagFinder TagFinder
PerformerFinder PerformerFinder
MovieFinder MovieFinder
GroupFinder GroupFinder
}
func NewRepository(repo models.Repository) Repository {
@@ -33,7 +33,7 @@ func NewRepository(repo models.Repository) Repository {
StudioFinder: repo.Studio,
TagFinder: repo.Tag,
PerformerFinder: repo.Performer,
MovieFinder: repo.Movie,
GroupFinder: repo.Group,
}
}

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