Compare commits

...

193 Commits

Author SHA1 Message Date
WithoutPants
3acb21d4e1 Merge pull request #1390 from stashapp/develop
Merge to master for 0.7
2021-05-15 16:17:08 +10:00
WithoutPants
e0623eb302 Fix initial setup issue issues (#1380)
* Refactor initial setup behaviour
* Adjust wizard
2021-05-13 22:15:21 +10:00
InfiniteTF
5a37e6cf52 Add search modal for stash-box performer scraper (#1373)
* Cache tagger fingerprint lookups between renders
* Show search modal for stash-box performer scraper
2021-05-10 09:33:08 +10:00
WithoutPants
3f97b3a1cb Remove unnecessary graphql fields (#1370)
* Remove unnecessary graphql fields
* Optimise joined queries
* Tag resolver query optimisation
2021-05-09 19:25:57 +10:00
InfiniteTF
81cf3d3337 Fix sorting of tagger fingerprint matches (#1369) 2021-05-07 13:00:29 +10:00
WithoutPants
bdac352250 Update demo video link 2021-05-05 14:14:39 +10:00
InfiniteTF
31981d4116 Add in-memory screenshot generation for sprites and phash (#1316) 2021-05-05 13:22:05 +10:00
WithoutPants
08c294414d Fix initial plugins/scrapers paths when initialising in home directory (#1358)
* Only set default values once config file present
* Fix configLocation presentation when using home
2021-05-04 07:42:33 +10:00
bnkai
2ab42e9cd3 Populate image/gallery title during scan (#1359) 2021-05-03 14:21:51 +10:00
InfiniteTF
896c3874af Stash-Box Performer Tagger (#1277)
* Add bulk stash-box performer task
* Add stash-box performer scraper to scrape with menu
2021-05-03 14:21:20 +10:00
WithoutPants
a3609079bb Autotag support for images and galleries (#1345)
* Add compound queries for images and galleries
* Implement image and gallery auto tagging
2021-05-03 13:09:46 +10:00
Jeremy Meyers
2c52fd711b Several syntactical and content changes (#1347) 2021-05-03 12:49:15 +10:00
WithoutPants
d7a04ced00 Revert always show preview videos on small devices (#1340) 2021-05-03 08:23:19 +10:00
WithoutPants
3f0c965400 Fix development releases 2021-05-01 11:19:21 +10:00
InfiniteTF
4a04dfe4a2 Fix scene tagger bugs (#1357) 2021-04-30 14:55:18 +10:00
bnkai
597576f5e6 Get distinct values from scraper (#1338)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-04-29 11:38:55 +10:00
julien0221
502d99de1b Added new filters (date and title) to galleries (#1344)
* Added new filters (date and title) to galleries
* Added image_count on filter for galleries
2021-04-29 11:31:51 +10:00
julien0221
4d13e8d7f7 Fixed rating filter on studios (#1342) 2021-04-29 11:20:59 +10:00
WithoutPants
210feb4034 Apply scene queuing for all scene listviews (#1332)
* Apply queue population for all scene list views
* Add missing localisation strings
2021-04-28 09:27:47 +10:00
WithoutPants
fe0c5615a6 Memo-ise list hook functions (#1329) 2021-04-28 09:12:35 +10:00
julien0221
70b66d91a0 Added rating to performers and studios (#1308) 2021-04-26 13:48:32 +10:00
stg-annon
eefc628cf0 update docs to match current functionality (#1339)
was wondering why `per_page:0` was not working it seems to have been updated to `per_page:-1` to return all results
2021-04-26 13:37:31 +10:00
bnkai
aedadc3857 Add lbToKg pp action to the scraper (#1337) 2021-04-26 13:31:25 +10:00
WithoutPants
2eb2d865dc Auto tag rewrite (#1324) 2021-04-26 12:51:31 +10:00
julien0221
f66010a367 Fixed 0 for weight when a new performer is created and fixed the search is null (#1336) 2021-04-26 12:13:50 +10:00
bnkai
7836a37d6e Fix various generate issues (#1322)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-04-22 13:51:51 +10:00
WithoutPants
bf3f658091 Movie scene sort (#1325)
* Add movie_scene_number sort order
* Sort movie scenes by scene number by default
2021-04-22 12:22:51 +10:00
peolic
1767390e0d Overwrite new performer image after clearing current image (#1321) 2021-04-21 14:19:40 +10:00
Jeremy Meyers
79a180ba73 Fix README formatting (#1328) 2021-04-21 10:33:33 +10:00
WithoutPants
8705f78591 Duplicate checker UI improvements (#1309)
* Add tools settings page
* Add tools and move dupe checker
* Make negative number get all
* Show missing phashes
* Add multi-edit button
* Show scene details
2021-04-20 18:58:28 +10:00
WithoutPants
39512e1452 Separate UI (#1299)
* Add custom_ui_location to serve UI from filesystem
2021-04-20 17:12:40 +10:00
peolic
9200f167bf Add studio *_count filters and sort options (#1307) 2021-04-20 16:48:36 +10:00
peolic
1759a99f65 Fix creating performer from gallery scrape dialog (#1320) 2021-04-18 13:22:02 +02:00
InfiniteTF
cd0a9a1d62 Fix performer scraping (#1314) 2021-04-17 08:52:18 +10:00
InfiniteTF
e3fa8f7b24 Fix fingerprint search when scene only has phash match (#1312) 2021-04-16 19:15:47 +02:00
bnkai
a5e9e7abce Update README with currently used ffmpeg URLs (#1304) 2021-04-16 16:20:20 +10:00
julien0221
d673c4ce03 added details, deathdate, hair color, weight to performers and added details to studios (#1274)
* added details to performers and studios
* added deathdate, hair_color and weight to performers
* Simplify performer/studio create mutations
* Add changelog and recategorised

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-04-16 16:06:35 +10:00
bnkai
cd6b6b74eb Add http headers support to scraper (#1273) 2021-04-16 15:42:56 +10:00
WithoutPants
0b40017b09 Sort performers in popover and card views (#1294) 2021-04-15 11:33:20 +10:00
InfiniteTF
e59018acfb Skip validation of existing paths when adding new paths (#1301) 2021-04-15 11:01:31 +10:00
WithoutPants
ea54a67798 Add scene/image/gallery popover count buttons for performer/studio/tag cards (#1293)
* Add counts to graphql schema
* Add count resolvers and query refactor
* Add count popover buttons
2021-04-15 10:46:31 +10:00
InfiniteTF
e6aaa196f3 Load settings panels on demand (#1302) 2021-04-15 10:01:44 +10:00
stashist
34f114faff Simplify GH build pipeline. (#1268)
The toolchain is already bundled in the stashapp/compiler image.
Rather than introducing a second one via GH actions standardize on that
one instead.

Also
* Clear up what "Cross Compile" actually does
* Still pull stashapp/compiler separately for easier debugability.
2021-04-13 16:11:19 +10:00
Elad Lachmi
f443223d16 [Feature] Added slideshow to gallery in wall display mode (#1224) 2021-04-13 14:59:37 +10:00
julien0221
6a4421f8e1 Whitespace is not trimmed from the end of query strings (#1263)
* fixed whitespace not trimmed query string
* fixed whitespace trimming on backend
* added query trim tests and fixed double space
2021-04-13 10:32:52 +10:00
WithoutPants
f5dc654f6b Support streaming via API key (#1279)
* Support api key via url query parameter
* Add api key to stream URL
2021-04-12 11:05:49 +10:00
WithoutPants
f6ffda7504 Setup and migration UI refactor (#1190)
* Make config instance-based
* Remove config dependency in paths
* Refactor config init
* Allow startup without database
* Get system status at UI initialise
* Add setup wizard
* Cache and Metadata optional. Database mandatory
* Handle metadata not set during full import/export
* Add links
* Remove config check middleware
* Stash not mandatory
* Panic on missing mandatory config fields
* Redirect setup to main page if setup not required
* Add migration UI
* Remove unused stuff
* Move UI initialisation into App
* Don't create metadata paths on RefreshConfig
* Add folder selector for generated in setup
* Env variable to set and create config file.
Make docker images use a fixed config file.
* Set config file during setup
2021-04-12 09:31:33 +10:00
InfiniteTF
c38660d209 Add phash generation and dupe checking (#1158) 2021-04-12 09:04:40 +10:00
WithoutPants
a2582047ca Join count filter criteria (#1254)
Co-authored-by: mrbrdo <mrbrdo@gmail.com>
Co-authored-by: peolic <66393006+peolic@users.noreply.github.com>
2021-04-09 18:46:00 +10:00
peolic
6a0c73b3a1 Remove external resource from Login page (#1275) 2021-04-09 16:42:52 +10:00
julien0221
d042ec42ee Added Auto scroll user back to the top when page navigation is clicked (#1270) 2021-04-09 15:27:48 +10:00
julien0221
25311247ed added an url filter option in scenes (#1266)
* added an url filter option in scenes
* added url filter on gallery, movies, performers and studios
* Add empty string filter to stringCriterionHandler
* Add unit tests

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-04-09 15:05:11 +10:00
peolic
60af076fff Fix "Clear Image" button (#1249)
* Fix "Clear Image" button (Performer Edit)
* Fix "Clear Image" button (New Performer)
* Fix "Clear Image" button (Edit Studio)
* Fix "Clear Image" button (Edit Tag)
2021-04-09 14:41:28 +10:00
stashist
4462b3cc8e Handle /healthz for liveness checks. (#1264) 2021-04-09 10:06:02 +10:00
bnkai
2edcdeaeb9 Support today, yesterday when using parseDate in scrapers (#1261) 2021-04-07 09:09:04 +10:00
peolic
d8ba4a08c0 Update various GQL image fields' comments (#1271) 2021-04-07 08:58:41 +10:00
julien0221
72b027a887 Added random for studios, movies and tags (#1250) 2021-04-07 08:32:20 +10:00
peolic
7671465334 Fix performer age timezone issues (#1251)
* Parse date string manually
2021-04-02 10:09:10 +11:00
bnkai
2c1300cae0 Upgrade x/image (#1248) 2021-04-01 16:43:42 +11:00
peolic
35718ce59a Disable sounds on scene/marker wall previews by default (#1247) 2021-04-01 16:10:56 +11:00
WithoutPants
1412b554a0 Api key (#1241) 2021-03-31 16:08:52 +11:00
UncleRoger33
2c2e56d33a Add format to performer field placeholder (#1232)
* Update README.md

Extra letter "p" in the title removed and "(FAQ)" suffix added.

Co-authored-by: peolic <66393006+peolic@users.noreply.github.com>
2021-03-31 15:55:15 +11:00
WithoutPants
ccb96c3795 Movie UI refresh (#1227)
* Improve movie UI
* Return nil when no back image set
2021-03-31 14:54:58 +11:00
WithoutPants
d5e9030768 Scene queuing (#1214)
* Add missing localisation strings
* Ignore container error in scene streams
* Implement missing FindScenes by ID
2021-03-31 14:36:11 +11:00
peolic
496900df42 Fix inaccurate age calculation (#1237) 2021-03-30 14:25:56 +11:00
peolic
7acae34ed4 Fix performer search columns (#1236)
* Fix performer search columns
* Update changelog
* Move changelog to new version

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-03-30 14:04:57 +11:00
WithoutPants
d30a68567e Update libraries and regenerate yarn.lock (#1231)
* Update and regenerate yarn.lock
* Remove eslint check for react function order
2021-03-30 12:33:57 +11:00
WithoutPants
8a3d2e8e06 Merge pull request #1240 from stashapp/master
Merge master back to develop for tag
2021-03-29 14:15:24 +11:00
WithoutPants
cad96b7872 Update build.yml 2021-03-29 12:59:03 +11:00
WithoutPants
de538be79c Merge pull request #1239 from stashapp/develop
Merge to master for 0.6
2021-03-29 12:05:59 +11:00
bnkai
4299f113e0 Fix Freeones search (#1230) 2021-03-25 10:01:56 +11:00
WithoutPants
b39fe3ed2b Correct tag link for gallery and images tags (#1221) 2021-03-24 11:03:52 +11:00
bnkai
68d4a4fe42 Add User Agent to image download reqs (#1222) 2021-03-24 08:12:11 +11:00
WithoutPants
73a8bad1bc Add missing tag writer for performer importer (#1213) 2021-03-18 21:45:31 +11:00
WithoutPants
960f843259 Fix filter building with sub-filters (#1212)
* Fix bracketing on sub-filters
* Add vscode to gitignore
2021-03-18 21:45:18 +11:00
WithoutPants
d93011a828 Add write mutex and max connection lifetime (#1211) 2021-03-18 21:45:01 +11:00
bnkai
215737d6c5 Add configFilePath and scrapersPath to configuration query (#1205) 2021-03-18 11:07:56 +11:00
InfiniteTF
6369a500b3 Update go-sqlite3 to 1.14.6 (#1209) 2021-03-17 11:17:27 +11:00
InfiniteTF
58243cded0 Remove slim graphql endpoints (#1207) 2021-03-17 11:17:01 +11:00
WithoutPants
7e6127975d Handle NULL in regex criteria (#1208) 2021-03-16 11:13:14 +11:00
WithoutPants
f7cd9cb00d Performer edit UI further updates (#1206)
* Make performer edit single column
* Make aliases textarea
* Add textareas to performer scrape dialog
2021-03-16 11:10:49 +11:00
InfiniteTF
ecac7a8013 Add timestamp suffix to all image urls (#1200) 2021-03-13 11:49:20 +11:00
InfiniteTF
a619b9dd48 Improve performer scrape search modal (#1198) 2021-03-13 11:48:04 +11:00
WithoutPants
b63e8ef929 Add Galleries tab to Tag details page (#1195) 2021-03-12 08:27:59 +11:00
WithoutPants
23d85655a8 Refactor tag query (#1194) 2021-03-11 22:17:37 +11:00
Jeremy Meyers
53cb9a1b7b Documentation Updates to README.md (#1179) 2021-03-11 15:56:22 +11:00
WithoutPants
a3a531d122 Fix IsPathInDir (#1192) 2021-03-11 13:37:13 +11:00
WithoutPants
55aee21cff Upload Image from url (#1193) 2021-03-11 12:56:34 +11:00
WithoutPants
b3966b3c76 Remove streaming resolutions over max configured (#1187) 2021-03-11 12:51:42 +11:00
SpedNSFW
b647a75151 Toggle visibility of unmatched scenes in Tagger (#1176) 2021-03-10 14:26:48 +11:00
gitgiggety
baeeb2d649 Hide create option when searching in filters (#1169)
Hide the "Create '<term>'" option when using filters. This as it doesn't make sense to create a new performer/tag/studio in the context of searching for one. As obviously there won't be any results after searching as it has just been created and not assigned to anything yet.
2021-03-10 14:08:45 +11:00
gitgiggety
f794c6ae45 Add scrape gallery from fragment to UI (#1166) 2021-03-10 13:47:22 +11:00
WithoutPants
a0676d5c30 Performer tags (#1132)
* Add scraping support for performer tags
* Add performer count to tag cards
* Refactor sqlite test setup
* Add performer tag filtering in gallery and image
* Add bulk update performer
* Add Performers tab to tag page
* Add count filters and sort bys for tags
* Move scene count to icon in performer card #1148
2021-03-10 12:25:51 +11:00
InfiniteTF
698e21a04f Fix scene form layout bugs (#1177) 2021-03-07 09:19:30 +11:00
WithoutPants
9d1b716f48 Performer UI improvements (#1168)
* Refactor performer edit page with Formik
* Upgrade react-scripts
* Make eslint errors warnings in dev environment
* Refactor performer details
* Prompt if leaving dirty performer edit page
2021-03-05 15:46:20 +11:00
InfiniteTF
c2c06d8f8d Fix tagger parsing bugs (#1172) 2021-03-05 15:03:08 +11:00
WithoutPants
e5c5cde974 Detect cover images in subdirectories (#1144) 2021-03-04 10:52:45 +11:00
WithoutPants
16da483674 Change gallery icon to images (#1167) 2021-03-04 08:27:42 +11:00
SpedNSFW
bde5d07afb Find correct python executable (#1156)
* find correct python executable
For script scrapers using python, both python and python3 are valid depending on the OS and running environment. To save users from having any issues, this change will find the correct executable for them.

Co-authored-by: bnkai <bnkai@users.noreply.github.com>
2021-03-03 08:01:01 +11:00
WithoutPants
1850a2b533 Add sqlite filter builder. Add AND, OR, NOT filters to scene filter (#1115)
* Add resolution enum extension
* Add filter builder
* Use filterBuilder for scene query
* Optimise joins
* Add binary operators to scene query
* Use Query for auto-tag
2021-03-02 11:27:36 +11:00
bnkai
117e6326db Expose url for URLReplace in JSON scrapeByURL and scrapeByFragment (#1150)
* Expose url for URLReplace in JSON scrapeByURL and scrapeByFragment
* Apply queryURLReplace to xpath scrapers

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-03-02 09:19:56 +11:00
bnkai
fe990e00c1 Check if gallery is already associated during scanning (#1154) 2021-03-01 16:37:55 +11:00
gitgiggety
4825de7d35 Fix SQL error in 8K resolution filter (#1159) 2021-03-01 16:06:57 +11:00
WithoutPants
7cfff46d02 Gallery filter fix (#1147)
* Fix gallery performer and tags filters
* Add unit tests
2021-03-01 13:30:40 +11:00
bnkai
44ea777019 Add check version support for armv7, arm64 (#1142) 2021-03-01 12:37:46 +11:00
bnkai
711496e9f4 Add full timestamp for console/file logging (#1130) 2021-03-01 12:28:09 +11:00
InfiniteTF
01da28010d Gallery view persistence (#1105)
* Persist gallery image view separately from primary image view
2021-03-01 12:10:05 +11:00
InfiniteTF
7e0db2aad4 Change NULL filters to filter empty strings as well (#1137) 2021-03-01 11:48:25 +11:00
bnkai
144cd6e4f2 Skip insecure certificates check when scraping (#1120)
* Ignore insecure certificates when scraping
* add ScraperCertCheck to scraper config options
2021-03-01 11:47:39 +11:00
WithoutPants
a9ac176e91 Mobile UI improvements (#1104)
* Use dropdown for o-counter instead of hover
* Always show previews on non-hoverable device
* Add IntersectionObserver polyfill
* Prevent video previews playing fullscreen
2021-02-26 16:13:34 +11:00
WithoutPants
af6b21a428 Update latest_develop tag 2021-02-24 14:36:39 +11:00
WithoutPants
8ec25ef161 Add commitish for dev release 2021-02-24 13:48:56 +11:00
WithoutPants
777956f0ab GitHub actions (#1146) 2021-02-24 11:26:48 +11:00
SpedNSFW
acbdee76de Random strings for cookie values (#1122) 2021-02-23 13:40:43 +11:00
InfiniteTF
14230d7b52 Enable keepalive for websocket connection (#1134) 2021-02-23 13:03:02 +11:00
WithoutPants
f7a8899d90 Add rescan option to overflow dropdown (#1119)
* Make scan options optional
* Add scene rescan
* Add image rescan
* Add gallery rescan
* Add changelog
2021-02-23 12:56:01 +11:00
WithoutPants
7fbb92d071 Merge master back to develop for tag 2021-02-23 10:46:28 +11:00
WithoutPants
2ec595dedf Merge to master for 0.5 2021-02-22 18:04:33 +11:00
WithoutPants
f9b2a4be5e Update changelog 2021-02-22 16:00:53 +11:00
WithoutPants
ac117c2934 Fix performer input values being lost when failing to create (#1133)
* Fix lost performer input values
* Fix unsetting image studio
2021-02-19 20:09:59 +11:00
InfiniteTF
dd03c63da2 Add country synonyms for Iran and Moldova (#1121) 2021-02-19 14:27:21 +11:00
Tweeticoats
3f9b395b89 build arm and x86_64 docker containers (#1097) 2021-02-19 10:38:13 +11:00
InfiniteTF
deebeab9e8 Fix performer details gender (#1124) 2021-02-14 11:18:26 +01:00
InfiniteTF
647ae2d7f9 Fix cover image in scene edit tab (#1123) 2021-02-13 19:03:29 +01:00
InfiniteTF
758eccc659 Fix scene edit state resetting when scene is updated (#1112)
* Fix organized click resetting scene form state
* Fix state resetting for images/galleries
* Fix favoriting a performer resetting edit state
2021-02-11 14:33:16 +11:00
WithoutPants
bbc34bd1bf Exclude media in generated directory (#1118) 2021-02-11 11:06:04 +11:00
WithoutPants
7609969491 Add donate button to navbar (#1117) 2021-02-11 09:12:35 +11:00
InfiniteTF
8d8a8530e8 Migrate generated files when a scene is rescanned (#1106) 2021-02-10 10:50:34 +11:00
bnkai
bcbbd1474c fix check version (#1103) 2021-02-09 21:00:27 +11:00
bnkai
984a0c9247 Tweak scraper script error printing (#1107) 2021-02-09 19:07:53 +11:00
SpedNSFW
714ae541d4 fix json unmarshal error return (#1109) 2021-02-09 19:04:42 +11:00
JoeSmithStarkers
9da11603c2 Add selective cross compile (#921) 2021-02-04 09:41:43 +11:00
bnkai
918b739b6c Add checksums for releases (#1093) 2021-02-04 09:41:05 +11:00
InfiniteTF
25b600f768 Fix tagger performance issue and date parsing (#1096) 2021-02-04 08:22:52 +11:00
WithoutPants
0dd2e269ee Don't delete downloads directory at startup if generated not set (#1098)
* Don't empty directories if generated not set
* Rename downloads to download_stage
2021-02-02 20:32:37 +11:00
WithoutPants
6d48cd1c97 Minor UI tweaks (#1099)
* Show one decimal point for sizes GB and over
* Don't open 0.4 version section by default
2021-02-02 20:25:36 +11:00
WithoutPants
4e9ebe055b Fix scene filename parser Q filter value (#1100) 2021-02-02 20:25:08 +11:00
WithoutPants
e4d91a0226 String regex filter criteria and selective autotag (#1082)
* Add regex string filter criterion
* Use query interface for auto tagging
* Use Query interface for filename parser
* Remove query regex interfaces
* Add selective auto tag
* Use page size 0 as no limit
2021-02-02 07:57:56 +11:00
InfiniteTF
4fd022a93b Decouple galleries from scenes (#1057) 2021-02-02 07:56:54 +11:00
Belley
86bfb64a0d Fix freeones scraper (#1091) 2021-02-01 08:15:50 +11:00
InfiniteTF
6114caa938 Fix import file copying (#1085) 2021-02-01 08:15:10 +11:00
InfiniteTF
23d2668b38 Update javascript polyfills (#1083) 2021-01-30 09:16:28 +11:00
InfiniteTF
89fcd6d775 Make file upload limits configurable (#1079) 2021-01-29 20:27:02 +11:00
bnkai
df8675c2e7 Add Dry Run option to clean task (#1081) 2021-01-29 15:03:34 +11:00
InfiniteTF
203fc3e4b2 Force reload of custom css if it's updated (#1073) 2021-01-29 15:00:47 +11:00
InfiniteTF
0d7663c13d Fix performer scrape select, and studio/gallery select glitching (#1080) 2021-01-28 21:00:58 +01:00
InfiniteTF
d1274d559d Update javascript libraries (#1075) 2021-01-27 11:25:29 +11:00
bnkai
088f32a116 * fix database reset (#1076) 2021-01-26 10:37:42 +11:00
WithoutPants
3b41894dbd Add backup database functionality (#1069) 2021-01-21 22:02:09 +11:00
WithoutPants
093b997eb1 Login to docker before building image (#1067) 2021-01-19 11:11:10 +11:00
WithoutPants
1e04deb3d4 Data layer restructuring (#997)
* Move query builders to sqlite package
* Add transaction system
* Wrap model resolvers in transaction
* Add error return value for StringSliceToIntSlice
* Update/refactor mutation resolvers
* Convert query builders
* Remove unused join types
* Add stash id unit tests
* Use WAL journal mode
2021-01-18 12:23:20 +11:00
InfiniteTF
7bae990c67 Fix interface state storage (#1064) 2021-01-15 15:15:59 +11:00
bnkai
defb23aaa2 Fix vtt sprite generation ( issue #1033 ) (#1035) 2021-01-14 12:53:42 +11:00
InfiniteTF
aad4ddc46d Add batch delete for performers/tags/studios/movies (#1053)
* Add batch delete for performers/tags/studios/movies
* Fix ListFilter styling bug
2021-01-13 11:57:53 +11:00
InfiniteTF
8a3d940aa7 Prevent studio from being selected as its own parent (#1051) 2021-01-12 17:11:01 +11:00
InfiniteTF
3d83fa449d Fix z-index of performer scrape selector and correct no options message (#1050) 2021-01-12 15:05:01 +11:00
InfiniteTF
0a123548a0 Cache fixes (#1048)
* Bust cache for studio/tag image updates
* Reset cache when a scan/clean has been run
2021-01-12 15:03:50 +11:00
SpedNSFW
03a9d65cfe extend resolutions (#1036)
* extend resolutions
 - Simplifies logic
 - Adds more options including 540p, 1440p, and resolutions common to VR such as 1920p
 - Supports vertical/portrait videos and images
* implement new resolution filters
2021-01-07 16:10:59 +11:00
bnkai
1882b44951 Strip file extension from scene title when scanning (#1022) 2021-01-07 11:38:30 +11:00
dependabot[bot]
4fc0d47087 Bump axios from 0.21.0 to 0.21.1 in /ui/v2.5 (#1045)
Bumps [axios](https://github.com/axios/axios) from 0.21.0 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.0...v0.21.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-06 13:18:44 +11:00
InfiniteTF
6b1d229a6d Equalise card styles for movies/galleries/images (#1015) 2021-01-05 13:12:16 +11:00
Jeremy Meyers
d84d48bc29 Update AutoTagging.md (#1037) 2021-01-04 09:02:10 +11:00
InfiniteTF
d50238cf41 Various bugfixes for scene tagger (#1014)
* Tagger fixes
* Validate stash-box endpoint pattern
2020-12-28 13:28:29 +11:00
InfiniteTF
5c10712aab Fix performer page lightbox (#1020) 2020-12-24 11:10:09 +01:00
InfiniteTF
232a69c518 Add gallery wall view, and new lightbox (#1008) 2020-12-24 11:17:15 +11:00
WithoutPants
c8bcaaf27d Fix scene file name parser update (#998)
* Fix conversion of input maps
* Only set changed scene values in parser update
2020-12-24 09:03:23 +11:00
InfiniteTF
e84c92355e Fix integer overflow for scene size on 32bit systems (#994)
* Fix integer overflow for scene size on 32bit systems
* Cast to double in sqlite to prevent potential overflow
* Add migration to reset scene sizes and scan logic to repopulate if empty
2020-12-22 10:29:53 +11:00
bnkai
e883e5fe27 Add Mouse Click support for the CDP scraper (#827) 2020-12-22 09:42:31 +11:00
InfiniteTF
dd2086a912 Fix galleryCreate mutation (#1004) 2020-12-20 12:35:41 +01:00
WithoutPants
aadbcaeec2 Organised flag (#988)
* Add organized boolean to scene model (#729)
* Add organized button to scene page
* Add flag to galleries and images
* Import/export changes
* Make organized flag not null
* Ignore organized scenes for autotag

Co-authored-by: com1234 <com1234@notarealemail.com>
2020-12-18 08:06:49 +11:00
InfiniteTF
99bd7bc750 Fix broken isMissing filters (#1000) 2020-12-17 20:27:44 +01:00
InfiniteTF
f264baa330 Add includes/excludes modifiers to path filter (#996) 2020-12-17 14:00:07 +11:00
WithoutPants
931d3a0974 Fix login redirect to remember current page (#989) 2020-12-13 14:00:01 +11:00
InfiniteTF
4a08bd351a Fix scene gallery selection (#990) 2020-12-11 11:15:32 +11:00
aGlkZGVu
fad64ba126 Implement user customizable menu items (#974) 2020-12-09 11:59:09 +11:00
bnkai
938559ca11 Fix studio logo getting nulled when editing a studio (#986) 2020-12-07 08:08:49 +11:00
WithoutPants
86747acc78 Use changesets correctly when updating objects (#976) 2020-12-04 12:42:56 +11:00
WithoutPants
6eea33aec9 Fix manual tables (#978) 2020-12-02 08:26:49 +11:00
SpedNSFW
c45780dc59 Add missing gallery components (#969)
* add gallery scrapers to settings page
* add galleries to performers page
* add galleries to studios page
2020-12-01 19:38:53 +11:00
bnkai
a96ab9ce6f Add support for setting cookies in the scraper (#934) 2020-12-01 16:34:09 +11:00
bnkai
aecbd236bc Tune image referrer path (#968) 2020-11-30 10:50:43 +11:00
InfiniteTF
a7d333786f Fix studio parent getting wiped in edit mode (#973) 2020-11-30 10:50:04 +11:00
InfiniteTF
a45c1111be Add TruncatedText component (#932) 2020-11-27 13:01:37 +11:00
WithoutPants
54c9f167ba Show studio as text in scene cards where studio image isn't set (#965) 2020-11-27 08:01:56 +11:00
WithoutPants
1d910419d1 Replace natural_sort with third party call (#964) 2020-11-27 08:01:36 +11:00
peolic
89277f1e25 Fix oversized movie images on scrape dialog (#961) 2020-11-26 10:33:41 +11:00
InfiniteTF
7d37e3e564 Add type policies for entity fetch queries (#958) 2020-11-26 09:50:36 +11:00
JoeSmithStarkers
df37ddcc2c Added natural sort for scene and image titles (#943)
* Added natural sort for scene and images
* Use natural sort for movie names

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2020-11-25 20:09:07 +11:00
InfiniteTF
794ea00d37 Upgrade create-react-app and assorted libraries (#914)
* Update create-react-app, react, typescript and eslint versions
* Various library updates
2020-11-25 13:20:48 +11:00
JoeSmithStarkers
e3eb550a7d Parallel scanning/generation, and combined scanning/preview/sprite (#820)
* Implement parallel scanning and generation, and combined scanning/preview/sprite generation.
* Added UI component for preview/sprite generation during scan, and configurable number of parallel tasks.
* Add v050 changelog entry
2020-11-25 12:45:10 +11:00
WithoutPants
502e9297ad Merge pull request #960 from stashapp/master
Merge master to develop for tag
2020-11-24 22:24:49 +11:00
WithoutPants
56b47ff9be Merge pull request #959 from stashapp/develop 2020-11-24 09:50:55 +11:00
WithoutPants
93068e1ded Merge pull request #955 from stashapp/develop
0.4 release
2020-11-23 09:40:54 +11:00
634 changed files with 78905 additions and 43612 deletions

144
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,144 @@
name: Build
on:
push:
branches: [ develop, master ]
pull_request:
branches: [ develop ]
release:
types: [ published ]
env:
COMPILER_IMAGE: stashapp/compiler:4
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Checkout
run: git fetch --prune --unshallow --tags
- name: Pull compiler image
run: docker pull $COMPILER_IMAGE
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node_modules
with:
path: ui/v2.5/node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock') }}
- name: Pre-install
run: docker run --rm --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated -w /stash $COMPILER_IMAGE /bin/bash -c "make pre-ui"
- name: Generate
run: docker run --rm --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated -w /stash $COMPILER_IMAGE /bin/bash -c "make generate"
# TODO: Replace with `make validate` once `revive` is bundled in COMPILER_IMAGE
- name: Validate
run: docker run --rm --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated -w /stash $COMPILER_IMAGE /bin/bash -c "make ui-validate fmt-check vet it"
- name: Build UI
run: docker run --rm --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated -w /stash $COMPILER_IMAGE /bin/bash -c "make ui-only"
- name: Compile for all supported platforms
run: ./scripts/cross-compile.sh
- name: Generate checksums
run: |
git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
sha1sum dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
echo "STASH_VERSION=$(git describe --tags --exclude latest_develop)" >> $GITHUB_ENV
echo "RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_ENV
- 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
with:
name: stash-win.exe
path: dist/stash-win.exe
- name: Upload OSX binary
# only upload binaries for pull requests
if: ${{ github.event_name == 'pull_request' && github.base_ref != 'refs/heads/develop' && github.base_ref != 'refs/heads/master'}}
uses: actions/upload-artifact@v2
with:
name: stash-osx
path: dist/stash-osx
- 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
with:
name: stash-linux
path: dist/stash-linux
- name: Update latest_develop tag
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
run : git tag -f latest_develop; git push -f --tags
- name: Development Release
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
uses: marvinpinto/action-automatic-releases@v1.1.2
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: true
automatic_release_tag: latest_develop
title: "${{ env.STASH_VERSION }}: Latest development build"
files: |
dist/stash-osx
dist/stash-win.exe
dist/stash-linux
dist/stash-linux-arm64v8
dist/stash-linux-arm32v7
dist/stash-pi
CHECKSUMS_SHA1
- name: Master release
if: ${{ github.event_name == 'release' && github.ref != 'refs/tags/latest_develop' }}
uses: meeDamian/github-release@2.0
with:
token: "${{ secrets.GITHUB_TOKEN }}"
allow_override: true
files: |
dist/stash-osx
dist/stash-win.exe
dist/stash-linux
dist/stash-linux-arm64v8
dist/stash-linux-arm32v7
dist/stash-pi
CHECKSUMS_SHA1
gzip: false
- name: Development Docker
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
env:
DOCKER_CLI_EXPERIMENTAL: enabled
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
docker info
docker buildx create --name builder --use
docker buildx inspect --bootstrap
docker buildx ls
bash ./docker/ci/x86_64/docker_push.sh development
- name: Release Docker
if: ${{ github.event_name == 'release' && github.ref != 'refs/tags/latest_develop' }}
env:
DOCKER_CLI_EXPERIMENTAL: enabled
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
docker info
docker buildx create --name builder --use
docker buildx inspect --bootstrap
docker buildx ls
bash ./docker/ci/x86_64/docker_push.sh latest

1
.gitignore vendored
View File

@@ -35,6 +35,7 @@ ui/v2.5/src/core/generated-*.tsx
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.vscode
# Generated files
.idea/**/contentModel.xml

View File

@@ -12,6 +12,24 @@ env:
global:
- GO111MODULE=on
before_install:
- set -e
# Configure environment so changes are picked up when the Docker daemon is restarted after upgrading
- echo '{"experimental":true}' | sudo tee /etc/docker/daemon.json
- export DOCKER_CLI_EXPERIMENTAL=enabled
# Upgrade to Docker CE 19.03 for BuildKit support
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
- sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
- sudo apt-get update
- sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
# install binfmt docker container, this container uses qemu to run arm programs transparently allowng docker to build arm 6,7,8 containers.
- docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
# Show info to simplify debugging and create a builder that can build the platforms we need
- docker info
- docker buildx create --name builder --use
- docker buildx inspect --bootstrap
- docker buildx ls
install:
- echo -e "machine github.com\n login $CI_USER_TOKEN" > ~/.netrc
- nvm install 12
- travis_retry make pre-ui
@@ -25,6 +43,8 @@ script:
after_success:
- docker pull stashapp/compiler:4
- sh ./scripts/cross-compile.sh
- git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
- sha1sum dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
- 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then sh ./scripts/upload-pull-request.sh; fi'
before_deploy:
# push the latest tag when on the develop branch
@@ -47,6 +67,7 @@ deploy:
- dist/stash-linux-arm64v8
- dist/stash-linux-arm32v7
- dist/stash-pi
- CHECKSUMS_SHA1
skip_cleanup: true
overwrite: true
name: "${STASH_VERSION}: Latest development build"
@@ -58,7 +79,7 @@ deploy:
# docker image build for develop release
- provider: script
skip_cleanup: true
script: bash ./docker/ci/x86_64/docker_push.sh development-x86_64
script: bash ./docker/ci/x86_64/docker_push.sh development
on:
repo: stashapp/stash
branch: develop
@@ -73,6 +94,7 @@ deploy:
- dist/stash-linux-arm64v8
- dist/stash-linux-arm32v7
- dist/stash-pi
- CHECKSUMS_SHA1
# make the release a draft so the maintainers can confirm before releasing
draft: true
skip_cleanup: true

View File

@@ -6,45 +6,61 @@
https://stashapp.cc
**Stash is a Go app which organizes and serves your porn.**
**Stash is a locally hosted web-based app written in Go which organizes and serves your porn.**
See a demo [here](https://vimeo.com/275537038) (password is stashapp).
* It can gather information about videos in your collection from the internet, and is extensible through the use of community-built plugins for a large number of content producers.
* It supports a wide variety of both video and image formats.
* You can tag videos and find them later.
* It provides statistics about performers, tags, studios and other things.
An in-app manual is available, and the manual pages can be viewed [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en).
You can [watch a SFW demo video](https://vimeo.com/545323354) to see it in action.
# Docker install
For further information you can [read the in-app manual](ui/v2.5/src/docs/en).
# Installing stash
## via Docker
Follow [this README.md in the docker directory.](docker/production/README.md)
# Bare-metal Install
## Pre-Compiled Binaries
Stash supports macOS, Windows, and Linux. Download the [latest release here](https://github.com/stashapp/stash/releases).
The Stash server runs on macOS, Windows, and Linux. Download the [latest release here](https://github.com/stashapp/stash/releases).
Run the executable (double click the exe on windows or run `./stash-osx` / `./stash-linux` from the terminal on macOS / Linux) and navigate to either https://localhost:9999 or http://localhost:9999 to get started.
*Note for Windows users:* Running the app might present a security prompt since the binary isn't signed yet. Just click more info and then the "run anyway" button.
*Note for Windows users:* Running the app might present a security prompt since the binary isn't yet signed. Bypass this by clicking "more info" and then the "run anyway" button.
#### FFMPEG
If stash is unable to find or download FFMPEG then download it yourself from the link for your platform:
* [macOS](https://ffmpeg.zeranoe.com/builds/macos64/static/ffmpeg-4.0-macos64-static.zip)
* [Windows](https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-4.0-win64-static.zip)
* [Linux](https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz)
* [macOS ffmpeg](https://evermeet.cx/ffmpeg/ffmpeg-4.3.1.zip), [macOS ffprobe](https://evermeet.cx/ffmpeg/ffprobe-4.3.1.zip)
* [Windows](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip)
* [Linux](https://www.johnvansickle.com/ffmpeg/)
The `ffmpeg(.exe)` and `ffprobe(.exe)` files should be placed in `~/.stash` on macOS / Linux or `C:\Users\YourUsername\.stash` on Windows.
# Usage
## Quickstart Guide
1) Download and install Stash and its dependencies
2) Run Stash. It will prompt you for some configuration options and a directory to index (you can also do this step afterward)
3) After configuration, launch your web browser and navigate to the URL shown within the Stash app.
**Note that Stash does not currently retrieve and organize information about your entire library automatically.** You will need to help it along through the use of [scrapers](blob/develop/ui/v2.5/src/docs/en/Scraping.md). The Stash community has developed scrapers for many popular data sources which can be downloaded and installed from [this repository](https://github.com/stashapp/CommunityScrapers).
The simplest way to tag a large number of files is by using the [Tagger](https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/docs/en/Tagger.md) which uses filename keywords to help identify the file and pull in scene and performer information from our stash-box database. Note that this data source is not comprehensive and you may need to use the scrapers to identify some of your media.
## CLI
Stash provides some command line options. See what is currently available by running `stash --help`.
Stash runs as a command-line app and local web server. There are some command-line options available, which you can see by running `stash --help`.
For example, to run stash locally on port 80 run it like this (OSX / Linux) `stash --host 127.0.0.1 --port 80`
## SSL (HTTPS)
Stash supports HTTPS with some additional work. First you must generate a SSL certificate and key combo. Here is an example using openssl:
Stash can run over HTTPS with some additional work. First you must generate a SSL certificate and key combo. Here is an example using openssl:
`openssl req -x509 -newkey rsa:4096 -sha256 -days 7300 -nodes -keyout stash.key -out stash.crt -extensions san -config <(echo "[req]"; echo distinguished_name=req; echo "[san]"; echo subjectAltName=DNS:stash.server,IP:127.0.0.1) -subj /CN=stash.server`
@@ -52,25 +68,32 @@ This command would need customizing for your environment. [This link](https://s
Once you have a certificate and key file name them `stash.crt` and `stash.key` and place them in the `~/.stash` directory. Stash detects these and starts up using HTTPS rather than HTTP.
# FAQ
# Customization
> I'm unable to run the app on OSX or Linux
## Themes and CSS Customization
There is a [directory of community-created themes](https://github.com/stashapp/stash/wiki/Themes) on our Wiki, along with instructions on how to install them.
Try running `chmod u+x stash-osx` or `chmod u+x stash-linux` to make the file executable.
You can also make Stash interface fit your desired style with [Custom CSS snippets](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets) and [CSS Tweaks](https://github.com/stashapp/stash/wiki/CSS-Tweaks).
> I have a question not answered here.
# Support (FAQ)
Join the [Discord server](https://discord.gg/2TsNFKt).
Answers to other Frequently Asked Questions can be found [on our Wiki](https://github.com/stashapp/stash/wiki/FAQ)
# Development
For issues not addressed there, there are a few options.
## Install
* Read the [Wiki](https://github.com/stashapp/stash/wiki)
* Check the in-app documentation (also available [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en)
* Join the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support.
# Compiling From Source Code
## Pre-requisites
* [Go](https://golang.org/dl/)
* [Revive](https://github.com/mgechev/revive) - Configurable linter
* Go Install: `go get github.com/mgechev/revive`
* [Packr2](https://github.com/gobuffalo/packr/tree/v2.0.2/v2) - Static asset bundler
* Go Install: `go get github.com/gobuffalo/packr/v2/packr2@v2.0.2`
* [Packr2](https://github.com/gobuffalo/packr/) - Static asset bundler
* Go Install: `go get github.com/gobuffalo/packr/v2/packr2`
* [Binary Download](https://github.com/gobuffalo/packr/releases)
* [Yarn](https://yarnpkg.com/en/docs/install) - Yarn package manager
* Run `yarn install --frozen-lockfile` in the `stash/ui/v2.5` folder (before running make generate for first time).
@@ -117,15 +140,8 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
## Cross compiling
This project uses a modification of [this](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment
This project uses a modification of the [CI-GoReleaser](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment
where the app can be cross-compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following
command to open a bash shell to the container to poke around:
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashappdev/compiler:latest /bin/bash`
## Customization
You can make Stash interface fit your desired style with [Custom CSS snippets](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets) and [CSS Tweaks](https://github.com/stashapp/stash/wiki/CSS-Tweaks).
[Stash Plex Theme](https://github.com/stashapp/stash/wiki/Stash-Plex-Theme) is a community created theme inspired by popular Plex Interface.

View File

@@ -53,6 +53,8 @@ FROM ubuntu:20.04 as app
RUN apt-get update && apt-get -y install ca-certificates
COPY --from=compiler /stash/stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999
CMD ["stash"]

View File

@@ -1,25 +1,19 @@
# must be built from /dist directory
FROM ubuntu:20.04 as prep
LABEL MAINTAINER="https://discord.gg/Uz29ny"
RUN apt-get update && \
apt-get -y install curl xz-utils && \
apt-get autoclean -y && \
rm -rf /var/lib/apt/lists/*
FROM --platform=$BUILDPLATFORM ubuntu:20.04 AS prep
ARG TARGETPLATFORM
WORKDIR /
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN curl --http1.1 -o /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
tar xf /ffmpeg.tar.xz && \
rm ffmpeg.tar.xz && \
mv /ffmpeg*/ /ffmpeg/
COPY stash-* /
RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-pi; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then BIN=stash-linux-arm32v7; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then BIN=stash-linux-arm64v8; \
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then BIN=stash-linux; \
fi; \
mv $BIN /stash
FROM ubuntu:20.04 as app
RUN apt-get update && apt-get -y install ca-certificates
COPY --from=prep /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
COPY /stash-linux /usr/bin/stash
run apt update && apt install -y python3 python3 python-is-python3 python3-requests ffmpeg && rm -rf /var/lib/apt/lists/*
COPY --from=prep /stash /usr/bin/
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999
CMD ["stash"]

View File

@@ -1,9 +1,8 @@
#!/bin/bash
DOCKER_TAG=$1
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
# must build the image from dist directory
docker build -t stashapp/stash:$DOCKER_TAG -f ./docker/ci/x86_64/Dockerfile ./dist
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push --output type=image,name=stashapp/stash:$DOCKER_TAG,push=true -f docker/ci/x86_64/Dockerfile dist/
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
docker push stashapp/stash:$DOCKER_TAG

View File

@@ -20,5 +20,8 @@ RUN curl --http1.1 -o /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/f
FROM ubuntu:20.04 as app
RUN apt-get update && apt-get -y install ca-certificates
COPY --from=prep /stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999
CMD ["stash"]

View File

@@ -20,5 +20,8 @@ RUN curl --http1.1 -o /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/f
FROM ubuntu:20.04 as app
RUN apt-get update && apt-get -y install ca-certificates
COPY --from=prep /stash /ffmpeg/ffmpeg /ffmpeg/ffprobe /usr/bin/
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999
CMD ["stash"]

10
go.mod
View File

@@ -4,10 +4,12 @@ require (
github.com/99designs/gqlgen v0.12.2
github.com/Yamashou/gqlgenc v0.0.0-20200902035953-4dbef3551953
github.com/antchfx/htmlquery v1.2.3
github.com/bmatcuk/doublestar/v2 v2.0.1
github.com/chromedp/cdproto v0.0.0-20200608134039-8a80cdaf865c
github.com/chromedp/chromedp v0.5.3
github.com/corona10/goimagehash v1.0.3
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.0
github.com/fvbommel/sortorder v1.0.2
github.com/go-chi/chi v4.0.2+incompatible
github.com/gobuffalo/packr/v2 v2.0.2
github.com/golang-migrate/migrate/v4 v4.3.1
@@ -18,8 +20,9 @@ require (
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/jmoiron/sqlx v1.2.0
github.com/json-iterator/go v1.1.9
github.com/mattn/go-sqlite3 v1.13.0
github.com/mattn/go-sqlite3 v1.14.6
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
github.com/remeh/sizedwaitgroup v1.0.0
github.com/rs/cors v1.6.0
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
github.com/sirupsen/logrus v1.4.2
@@ -30,8 +33,9 @@ require (
github.com/vektah/gqlparser/v2 v2.0.1
github.com/vektra/mockery/v2 v2.2.1
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/image v0.0.0-20190802002840-cff245a6509b
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd
golang.org/x/tools v0.0.0-20200915031644-64986481280e // indirect
gopkg.in/yaml.v2 v2.3.0
)

17
go.sum
View File

@@ -62,8 +62,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bmatcuk/doublestar/v2 v2.0.1 h1:EFT91DmIMRcrUEcYUW7AqSAwKvNzP5+CoDmNVBbcQOU=
github.com/bmatcuk/doublestar/v2 v2.0.1/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -85,6 +83,8 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/corona10/goimagehash v1.0.3 h1:NZM518aKLmoNluluhfHGxT3LGOnrojrxhGn63DR/CZA=
github.com/corona10/goimagehash v1.0.3/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.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -101,6 +101,7 @@ github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKX
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
@@ -128,6 +129,8 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk=
github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo=
github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
@@ -538,8 +541,8 @@ github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c=
github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
@@ -572,6 +575,8 @@ github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007 h1:Ohgj9L0EYOgXxkDp+
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007/go.mod h1:wKCOWMb6iNlvKiOToY2cNuaovSXvIiv1zDi9QDR7aGQ=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -614,6 +619,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.1.0 h1:g0fH8RicVgNl+zVZDCDfbdWxAWoAEJyI7I3TZYXFiig=
@@ -800,6 +807,8 @@ golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86h
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
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=

View File

@@ -2,13 +2,14 @@ fragment ConfigGeneralData on ConfigGeneralResult {
stashes {
path
excludeVideo
excludeImage
excludeImage
}
databasePath
generatedPath
cachePath
calculateMD5
videoFileNamingAlgorithm
parallelTasks
previewSegments
previewSegmentDuration
previewExcludeStart
@@ -16,6 +17,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
previewPreset
maxTranscodeSize
maxStreamingTranscodeSize
apiKey
username
password
maxSessionAge
@@ -30,6 +32,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
excludes
imageExcludes
scraperUserAgent
scraperCertCheck
scraperCDPPath
stashBoxes {
name
@@ -39,6 +42,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
}
fragment ConfigInterfaceData on ConfigInterfaceResult {
menuItems
soundOnPreview
wallShowTitle
wallPlayback
@@ -48,6 +52,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
css
cssEnabled
language
slideshowDelay
}
fragment ConfigData on ConfigResult {

View File

@@ -1,4 +1,4 @@
fragment GallerySlimData on Gallery {
fragment SlimGalleryData on Gallery {
id
checksum
path
@@ -7,20 +7,36 @@ fragment GallerySlimData on Gallery {
url
details
rating
organized
image_count
cover {
...SlimImageData
file {
size
width
height
}
paths {
thumbnail
}
}
studio {
...StudioData
id
name
image_path
}
tags {
...TagData
id
name
}
performers {
...PerformerData
id
name
gender
favorite
image_path
}
scene {
scenes {
id
title
path

View File

@@ -7,6 +7,7 @@ fragment GalleryData on Gallery {
url
details
rating
organized
images {
...SlimImageData
}
@@ -14,18 +15,16 @@ fragment GalleryData on Gallery {
...SlimImageData
}
studio {
...StudioData
...SlimStudioData
}
tags {
...TagData
...SlimTagData
}
performers {
...PerformerData
}
scene {
id
title
path
scenes {
...SlimSceneData
}
}

View File

@@ -3,6 +3,7 @@ fragment SlimImageData on Image {
checksum
title
rating
organized
o_counter
path
@@ -37,6 +38,7 @@ fragment SlimImageData on Image {
performers {
id
name
gender
favorite
image_path
}

View File

@@ -3,6 +3,7 @@ fragment ImageData on Image {
checksum
title
rating
organized
o_counter
path
@@ -22,11 +23,11 @@ fragment ImageData on Image {
}
studio {
...StudioData
...SlimStudioData
}
tags {
...TagData
...SlimTagData
}
performers {

View File

@@ -9,7 +9,7 @@ fragment MovieData on Movie {
director
studio {
...StudioData
...SlimStudioData
}
synopsis

View File

@@ -3,8 +3,14 @@ fragment SlimPerformerData on Performer {
name
gender
image_path
favorite
tags {
id
name
}
stash_ids {
endpoint
stash_id
}
rating
}

View File

@@ -20,8 +20,20 @@ fragment PerformerData on Performer {
favorite
image_path
scene_count
image_count
gallery_count
tags {
...SlimTagData
}
stash_ids {
stash_id
endpoint
}
rating
details
death_date
hair_color
weight
}

View File

@@ -8,7 +8,9 @@ fragment SlimSceneData on Scene {
date
rating
o_counter
organized
path
phash
file {
size
@@ -28,6 +30,7 @@ fragment SlimSceneData on Scene {
webp
vtt
chapters_vtt
sprite
}
scene_markers {
@@ -36,7 +39,7 @@ fragment SlimSceneData on Scene {
seconds
}
gallery {
galleries {
id
path
title
@@ -65,6 +68,7 @@ fragment SlimSceneData on Scene {
performers {
id
name
gender
favorite
image_path
}

View File

@@ -8,7 +8,9 @@ fragment SceneData on Scene {
date
rating
o_counter
organized
path
phash
file {
size
@@ -34,12 +36,12 @@ fragment SceneData on Scene {
...SceneMarkerData
}
gallery {
...GalleryData
galleries {
...SlimGalleryData
}
studio {
...StudioData
...SlimStudioData
}
movies {
@@ -50,7 +52,7 @@ fragment SceneData on Scene {
}
tags {
...TagData
...SlimTagData
}
performers {

View File

@@ -15,7 +15,14 @@ fragment ScrapedPerformerData on ScrapedPerformer {
tattoos
piercings
aliases
tags {
...ScrapedSceneTagData
}
image
details
death_date
hair_color
weight
}
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
@@ -36,8 +43,15 @@ fragment ScrapedScenePerformerData on ScrapedScenePerformer {
tattoos
piercings
aliases
tags {
...ScrapedSceneTagData
}
remote_site_id
images
details
death_date
hair_color
weight
}
fragment ScrapedMovieStudioData on ScrapedMovieStudio {
@@ -183,3 +197,10 @@ fragment ScrapedStashBoxSceneData on ScrapedScene {
...ScrapedSceneMovieData
}
}
fragment ScrapedStashBoxPerformerData on StashBoxPerformerQueryResult {
query
results {
...ScrapedScenePerformerData
}
}

View File

@@ -9,4 +9,6 @@ fragment SlimStudioData on Studio {
parent_studio {
id
}
details
rating
}

View File

@@ -10,6 +10,8 @@ fragment StudioData on Studio {
url
image_path
scene_count
image_count
gallery_count
}
child_studios {
id
@@ -18,11 +20,17 @@ fragment StudioData on Studio {
url
image_path
scene_count
image_count
gallery_count
}
image_path
scene_count
image_count
gallery_count
stash_ids {
stash_id
endpoint
}
details
rating
}

View File

@@ -0,0 +1,5 @@
fragment SlimTagData on Tag {
id
name
image_path
}

View File

@@ -4,4 +4,7 @@ fragment TagData on Tag {
image_path
scene_count
scene_marker_count
image_count
gallery_count
performer_count
}

View File

@@ -1,3 +1,11 @@
mutation Setup($input: SetupInput!) {
setup(input: $input)
}
mutation Migrate($input: MigrateInput!) {
migrate(input: $input)
}
mutation ConfigureGeneral($input: ConfigGeneralInput!) {
configureGeneral(input: $input) {
...ConfigGeneralData
@@ -8,4 +16,8 @@ mutation ConfigureInterface($input: ConfigInterfaceInput!) {
configureInterface(input: $input) {
...ConfigInterfaceData
}
}
}
mutation GenerateAPIKey($input: GenerateAPIKeyInput!) {
generateAPIKey(input: $input)
}

View File

@@ -1,79 +1,23 @@
mutation GalleryCreate(
$title: String!,
$details: String,
$url: String,
$date: String,
$rating: Int,
$scene_id: ID,
$studio_id: ID,
$performer_ids: [ID!] = [],
$tag_ids: [ID!] = []) {
$input: GalleryCreateInput!) {
galleryCreate(input: {
title: $title,
details: $details,
url: $url,
date: $date,
rating: $rating,
scene_id: $scene_id,
studio_id: $studio_id,
tag_ids: $tag_ids,
performer_ids: $performer_ids
}) {
galleryCreate(input: $input) {
...GalleryData
}
}
mutation GalleryUpdate(
$id: ID!,
$title: String,
$details: String,
$url: String,
$date: String,
$rating: Int,
$scene_id: ID,
$studio_id: ID,
$performer_ids: [ID!] = [],
$tag_ids: [ID!] = []) {
$input: GalleryUpdateInput!) {
galleryUpdate(input: {
id: $id,
title: $title,
details: $details,
url: $url,
date: $date,
rating: $rating,
scene_id: $scene_id,
studio_id: $studio_id,
tag_ids: $tag_ids,
performer_ids: $performer_ids
}) {
galleryUpdate(input: $input) {
...GalleryData
}
}
mutation BulkGalleryUpdate(
$ids: [ID!] = [],
$url: String,
$date: String,
$details: String,
$rating: Int,
$scene_id: ID,
$studio_id: ID,
$tag_ids: BulkUpdateIds,
$performer_ids: BulkUpdateIds) {
$input: BulkGalleryUpdateInput!) {
bulkGalleryUpdate(input: {
ids: $ids,
details: $details,
url: $url,
date: $date,
rating: $rating,
scene_id: $scene_id,
studio_id: $studio_id,
tag_ids: $tag_ids,
performer_ids: $performer_ids
}) {
bulkGalleryUpdate(input: $input) {
...GalleryData
}
}

View File

@@ -1,43 +1,15 @@
mutation ImageUpdate(
$id: ID!,
$title: String,
$rating: Int,
$studio_id: ID,
$gallery_ids: [ID!] = [],
$performer_ids: [ID!] = [],
$tag_ids: [ID!] = []) {
$input: ImageUpdateInput!) {
imageUpdate(input: {
id: $id,
title: $title,
rating: $rating,
studio_id: $studio_id,
gallery_ids: $gallery_ids,
performer_ids: $performer_ids,
tag_ids: $tag_ids
}) {
imageUpdate(input: $input) {
...SlimImageData
}
}
mutation BulkImageUpdate(
$ids: [ID!] = [],
$title: String,
$rating: Int,
$studio_id: ID,
$gallery_ids: BulkUpdateIds,
$performer_ids: BulkUpdateIds,
$tag_ids: BulkUpdateIds) {
$input: BulkImageUpdateInput!) {
bulkImageUpdate(input: {
ids: $ids,
title: $title,
rating: $rating,
studio_id: $studio_id,
gallery_ids: $gallery_ids,
performer_ids: $performer_ids,
tag_ids: $tag_ids
}) {
bulkImageUpdate(input: $input) {
...SlimImageData
}
}

View File

@@ -26,8 +26,8 @@ mutation MetadataAutoTag($input: AutoTagMetadataInput!) {
metadataAutoTag(input: $input)
}
mutation MetadataClean {
metadataClean
mutation MetadataClean($input: CleanMetadataInput!) {
metadataClean(input: $input)
}
mutation MigrateHashNaming {
@@ -36,4 +36,8 @@ mutation MigrateHashNaming {
mutation StopJob {
stopJob
}
}
mutation BackupDatabase($input: BackupDatabaseInput!) {
backupDatabase(input: $input)
}

View File

@@ -16,25 +16,16 @@ mutation MovieCreate(
}
}
mutation MovieUpdate(
$id: ID!
$name: String,
$aliases: String,
$duration: Int,
$date: String,
$rating: Int,
$studio_id: ID,
$director: String,
$synopsis: String,
$url: String,
$front_image: String,
$back_image: String) {
movieUpdate(input: { id: $id, name: $name, aliases: $aliases, duration: $duration, date: $date, rating: $rating, studio_id: $studio_id, director: $director, synopsis: $synopsis, url: $url, front_image: $front_image, back_image: $back_image }) {
mutation MovieUpdate($input: MovieUpdateInput!) {
movieUpdate(input: $input) {
...MovieData
}
}
mutation MovieDestroy($id: ID!) {
movieDestroy(input: { id: $id })
}
}
mutation MoviesDestroy($ids: [ID!]!) {
moviesDestroy(ids: $ids)
}

View File

@@ -1,93 +1,23 @@
mutation PerformerCreate(
$name: String!,
$url: String,
$gender: GenderEnum,
$birthdate: String,
$ethnicity: String,
$country: String,
$eye_color: String,
$height: String,
$measurements: String,
$fake_tits: String,
$career_length: String,
$tattoos: String,
$piercings: String,
$aliases: String,
$twitter: String,
$instagram: String,
$favorite: Boolean,
$stash_ids: [StashIDInput!],
$image: String) {
$input: PerformerCreateInput!) {
performerCreate(input: {
name: $name,
url: $url,
gender: $gender,
birthdate: $birthdate,
ethnicity: $ethnicity,
country: $country,
eye_color: $eye_color,
height: $height,
measurements: $measurements,
fake_tits: $fake_tits,
career_length: $career_length,
tattoos: $tattoos,
piercings: $piercings,
aliases: $aliases,
twitter: $twitter,
instagram: $instagram,
favorite: $favorite,
stash_ids: $stash_ids,
image: $image
}) {
performerCreate(input: $input) {
...PerformerData
}
}
mutation PerformerUpdate(
$id: ID!,
$name: String,
$url: String,
$gender: GenderEnum,
$birthdate: String,
$ethnicity: String,
$country: String,
$eye_color: String,
$height: String,
$measurements: String,
$fake_tits: String,
$career_length: String,
$tattoos: String,
$piercings: String,
$aliases: String,
$twitter: String,
$instagram: String,
$favorite: Boolean,
$stash_ids: [StashIDInput!],
$image: String) {
$input: PerformerUpdateInput!) {
performerUpdate(input: {
id: $id,
name: $name,
url: $url,
gender: $gender,
birthdate: $birthdate,
ethnicity: $ethnicity,
country: $country,
eye_color: $eye_color,
height: $height,
measurements: $measurements,
fake_tits: $fake_tits,
career_length: $career_length,
tattoos: $tattoos,
piercings: $piercings,
aliases: $aliases,
twitter: $twitter,
instagram: $instagram,
favorite: $favorite,
stash_ids: $stash_ids,
image: $image
}) {
performerUpdate(input: $input) {
...PerformerData
}
}
mutation BulkPerformerUpdate(
$input: BulkPerformerUpdateInput!) {
bulkPerformerUpdate(input: $input) {
...PerformerData
}
}
@@ -95,3 +25,7 @@ mutation PerformerUpdate(
mutation PerformerDestroy($id: ID!) {
performerDestroy(input: { id: $id })
}
mutation PerformersDestroy($ids: [ID!]!) {
performersDestroy(ids: $ids)
}

View File

@@ -1,62 +1,16 @@
mutation SceneUpdate(
$id: ID!,
$title: String,
$details: String,
$url: String,
$date: String,
$rating: Int,
$studio_id: ID,
$gallery_id: ID,
$performer_ids: [ID!] = [],
$movies: [SceneMovieInput!] = [],
$tag_ids: [ID!] = [],
$stash_ids: [StashIDInput!],
$cover_image: String) {
$input: SceneUpdateInput!) {
sceneUpdate(input: {
id: $id,
title: $title,
details: $details,
url: $url,
date: $date,
rating: $rating,
studio_id: $studio_id,
gallery_id: $gallery_id,
performer_ids: $performer_ids,
movies: $movies,
tag_ids: $tag_ids,
stash_ids: $stash_ids,
cover_image: $cover_image
}) {
...SceneData
sceneUpdate(input: $input) {
...SceneData
}
}
mutation BulkSceneUpdate(
$ids: [ID!] = [],
$title: String,
$details: String,
$url: String,
$date: String,
$rating: Int,
$studio_id: ID,
$gallery_id: ID,
$performer_ids: BulkUpdateIds,
$tag_ids: BulkUpdateIds) {
$input: BulkSceneUpdateInput!) {
bulkSceneUpdate(input: {
ids: $ids,
title: $title,
details: $details,
url: $url,
date: $date,
rating: $rating,
studio_id: $studio_id,
gallery_id: $gallery_id,
performer_ids: $performer_ids,
tag_ids: $tag_ids
}) {
...SceneData
bulkSceneUpdate(input: $input) {
...SceneData
}
}

View File

@@ -1,3 +1,7 @@
mutation SubmitStashBoxFingerprints($input: StashBoxFingerprintSubmissionInput!) {
submitStashBoxFingerprints(input: $input)
}
mutation StashBoxBatchPerformerTag($input: StashBoxBatchPerformerTagInput!) {
stashBoxBatchPerformerTag(input: $input)
}

View File

@@ -1,24 +1,11 @@
mutation StudioCreate(
$name: String!,
$url: String,
$image: String,
$stash_ids: [StashIDInput!],
$parent_id: ID) {
studioCreate(input: { name: $name, url: $url, image: $image, stash_ids: $stash_ids, parent_id: $parent_id }) {
mutation StudioCreate($input: StudioCreateInput!) {
studioCreate(input: $input) {
...StudioData
}
}
mutation StudioUpdate(
$id: ID!
$name: String,
$url: String,
$image: String,
$stash_ids: [StashIDInput!],
$parent_id: ID) {
studioUpdate(input: { id: $id, name: $name, url: $url, image: $image, stash_ids: $stash_ids, parent_id: $parent_id }) {
mutation StudioUpdate($input: StudioUpdateInput!) {
studioUpdate(input: $input) {
...StudioData
}
}
@@ -26,3 +13,7 @@ mutation StudioUpdate(
mutation StudioDestroy($id: ID!) {
studioDestroy(input: { id: $id })
}
mutation StudiosDestroy($ids: [ID!]!) {
studiosDestroy(ids: $ids)
}

View File

@@ -8,8 +8,12 @@ mutation TagDestroy($id: ID!) {
tagDestroy(input: { id: $id })
}
mutation TagUpdate($id: ID!, $name: String!, $image: String) {
tagUpdate(input: { id: $id, name: $name, image: $image }) {
mutation TagsDestroy($ids: [ID!]!) {
tagsDestroy(ids: $ids)
}
mutation TagUpdate($input: TagUpdateInput!) {
tagUpdate(input: $input) {
...TagData
}
}
}

View File

@@ -2,7 +2,7 @@ query FindGalleries($filter: FindFilterType, $gallery_filter: GalleryFilterType)
findGalleries(gallery_filter: $gallery_filter, filter: $filter) {
count
galleries {
...GallerySlimData
...SlimGalleryData
}
}
}

View File

@@ -13,37 +13,29 @@ query AllTags {
}
query AllPerformersForFilter {
allPerformersSlim {
allPerformers {
...SlimPerformerData
}
}
query AllStudiosForFilter {
allStudiosSlim {
allStudios {
...SlimStudioData
}
}
query AllMoviesForFilter {
allMoviesSlim {
allMovies {
...SlimMovieData
}
}
query AllTagsForFilter {
allTagsSlim {
allTags {
id
name
}
}
query ValidGalleriesForScene($scene_id: ID!) {
validGalleriesForScene(scene_id: $scene_id) {
id
path
title
}
}
query Stats {
stats {
scene_count,

View File

@@ -16,6 +16,12 @@ query FindScenesByPathRegex($filter: FindFilterType) {
}
}
query FindDuplicateScenes($distance: Int) {
findDuplicateScenes(distance: $distance) {
...SlimSceneData
}
}
query FindScene($id: ID!, $checksum: String) {
findScene(id: $id, checksum: $checksum) {
...SceneData
@@ -45,7 +51,7 @@ query ParseSceneFilenames($filter: FindFilterType!, $config: SceneParserInput!)
date
rating
studio_id
gallery_id
gallery_ids
movies {
movie_id
}

View File

@@ -15,6 +15,10 @@ query ScrapeFreeones($performer_name: String!) {
tattoos
piercings
aliases
details
death_date
hair_color
weight
}
}

View File

@@ -90,8 +90,14 @@ query ScrapeMovieURL($url: String!) {
}
}
query QueryStashBoxScene($input: StashBoxQueryInput!) {
query QueryStashBoxScene($input: StashBoxSceneQueryInput!) {
queryStashBoxScene(input: $input) {
...ScrapedStashBoxSceneData
}
}
query QueryStashBoxPerformer($input: StashBoxPerformerQueryInput!) {
queryStashBoxPerformer(input: $input) {
...ScrapedStashBoxPerformerData
}
}

View File

@@ -5,3 +5,13 @@ query JobStatus {
message
}
}
query SystemStatus {
systemStatus {
databaseSchema
databasePath
appSchema
status
configPath
}
}

View File

@@ -9,6 +9,9 @@ type Query {
findScenesByPathRegex(filter: FindFilterType): FindScenesResultType!
""" Returns any groups of scenes that are perceptual duplicates within the queried distance """
findDuplicateScenes(distance: Int): [[Scene!]!]!
"""Return valid stream paths"""
sceneStreams(id: ID): [SceneStreamEndpoint!]!
@@ -50,8 +53,6 @@ type Query {
"""Get marker strings"""
markerStrings(q: String, sort: String): [MarkerStringsResultType]!
"""Get the list of valid galleries for a given scene ID"""
validGalleriesForScene(scene_id: ID): [Gallery!]!
"""Get stats"""
stats: StatsResultType!
"""Organize scene markers by tag for a given scene ID"""
@@ -90,7 +91,8 @@ type Query {
scrapeFreeonesPerformerList(query: String!): [String!]!
"""Query StashBox for scenes"""
queryStashBoxScene(input: StashBoxQueryInput!): [ScrapedScene!]!
queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]!
queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]!
# Plugins
"""List loaded plugins"""
@@ -105,7 +107,7 @@ type Query {
directory(path: String): Directory!
# Metadata
systemStatus: SystemStatus!
jobStatus: MetadataUpdateStatus!
# Get everything
@@ -117,11 +119,6 @@ type Query {
# Get everything with minimal metadata
allPerformersSlim: [Performer!]!
allStudiosSlim: [Studio!]!
allMoviesSlim: [Movie!]!
allTagsSlim: [Tag!]!
# Version
version: Version!
@@ -130,6 +127,9 @@ type Query {
}
type Mutation {
setup(input: SetupInput!): Boolean!
migrate(input: MigrateInput!): Boolean!
sceneUpdate(input: SceneUpdateInput!): Scene
bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!]
sceneDestroy(input: SceneDestroyInput!): Boolean!
@@ -175,23 +175,31 @@ type Mutation {
performerCreate(input: PerformerCreateInput!): Performer
performerUpdate(input: PerformerUpdateInput!): Performer
performerDestroy(input: PerformerDestroyInput!): Boolean!
performersDestroy(ids: [ID!]!): Boolean!
bulkPerformerUpdate(input: BulkPerformerUpdateInput!): [Performer!]
studioCreate(input: StudioCreateInput!): Studio
studioUpdate(input: StudioUpdateInput!): Studio
studioDestroy(input: StudioDestroyInput!): Boolean!
studiosDestroy(ids: [ID!]!): Boolean!
movieCreate(input: MovieCreateInput!): Movie
movieUpdate(input: MovieUpdateInput!): Movie
movieDestroy(input: MovieDestroyInput!): Boolean!
moviesDestroy(ids: [ID!]!): Boolean!
tagCreate(input: TagCreateInput!): Tag
tagUpdate(input: TagUpdateInput!): Tag
tagDestroy(input: TagDestroyInput!): Boolean!
tagsDestroy(ids: [ID!]!): Boolean!
"""Change general configuration options"""
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
"""Generate and set (or clear) API key"""
generateAPIKey(input: GenerateAPIKeyInput!): String!
"""Returns a link to download the result"""
exportObjects(input: ExportObjectsInput!): String
@@ -209,7 +217,7 @@ type Mutation {
"""Start auto-tagging. Returns the job ID"""
metadataAutoTag(input: AutoTagMetadataInput!): String!
"""Clean metadata. Returns the job ID"""
metadataClean: String!
metadataClean(input: CleanMetadataInput!): String!
"""Migrate generated files for the current hash naming"""
migrateHashNaming: String!
@@ -222,8 +230,14 @@ type Mutation {
stopJob: Boolean!
""" Submit fingerprints to stash-box instance """
"""Submit fingerprints to stash-box instance"""
submitStashBoxFingerprints(input: StashBoxFingerprintSubmissionInput!): Boolean!
"""Backup the database. Optionally returns a link to download the database file"""
backupDatabase(input: BackupDatabaseInput!): String
"""Run batch performer tag task. Returns the job ID."""
stashBoxBatchPerformerTag(input: StashBoxBatchPerformerTagInput!): String!
}
type Subscription {

View File

@@ -1,3 +1,13 @@
input SetupInput {
"""Empty to indicate $HOME/.stash/config.yml default"""
configLocation: String!
stashes: [StashConfigInput!]!
"""Empty to indicate default"""
databaseFile: String!
"""Empty to indicate default"""
generatedLocation: String!
}
enum StreamingResolutionEnum {
"240p", LOW
"480p", STANDARD
@@ -35,6 +45,8 @@ input ConfigGeneralInput {
calculateMD5: Boolean!
"""Hash algorithm to use for generated file naming"""
videoFileNamingAlgorithm: HashAlgorithm!
"""Number of parallel tasks to start during scan/generate"""
parallelTasks: Int
"""Number of segments in a preview file"""
previewSegments: Int
"""Preview segment duration, in seconds"""
@@ -79,6 +91,8 @@ input ConfigGeneralInput {
scraperUserAgent: String
"""Scraper CDP path. Path to chrome executable or remote address"""
scraperCDPPath: String
"""Whether the scraper should check for invalid certificates"""
scraperCertCheck: Boolean!
"""Stash-box instances used for tagging"""
stashBoxes: [StashBoxInput!]!
}
@@ -90,12 +104,18 @@ type ConfigGeneralResult {
databasePath: String!
"""Path to generated files"""
generatedPath: String!
"""Path to the config file used"""
configFilePath: String!
"""Path to scrapers"""
scrapersPath: String!
"""Path to cache"""
cachePath: String!
"""Whether to calculate MD5 checksums for scene video files"""
calculateMD5: Boolean!
"""Hash algorithm to use for generated file naming"""
videoFileNamingAlgorithm: HashAlgorithm!
"""Number of parallel tasks to start during scan/generate"""
parallelTasks: Int!
"""Number of segments in a preview file"""
previewSegments: Int!
"""Preview segment duration, in seconds"""
@@ -110,6 +130,8 @@ type ConfigGeneralResult {
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""API Key"""
apiKey: String!
"""Username"""
username: String!
"""Password"""
@@ -140,11 +162,15 @@ type ConfigGeneralResult {
scraperUserAgent: String
"""Scraper CDP path. Path to chrome executable or remote address"""
scraperCDPPath: String
"""Whether the scraper should check for invalid certificates"""
scraperCertCheck: Boolean!
"""Stash-box instances used for tagging"""
stashBoxes: [StashBox!]!
}
input ConfigInterfaceInput {
"""Ordered list of items that should be shown in the menu"""
menuItems: [String!]
"""Enable sound on mouseover previews"""
soundOnPreview: Boolean
"""Show title and tags in wall view"""
@@ -160,10 +186,15 @@ input ConfigInterfaceInput {
"""Custom CSS"""
css: String
cssEnabled: Boolean
"""Interface language"""
language: String
"""Slideshow Delay"""
slideshowDelay: Int
}
type ConfigInterfaceResult {
"""Ordered list of items that should be shown in the menu"""
menuItems: [String!]
"""Enable sound on mouseover previews"""
soundOnPreview: Boolean
"""Show title and tags in wall view"""
@@ -174,13 +205,15 @@ type ConfigInterfaceResult {
maximumLoopDuration: Int
"""If true, video will autostart on load in the scene player"""
autostartVideo: Boolean
"""If true, studio overlays will be shown as text instead of logo images"""
"""If true, studio overlays will be shown as text instead of logo images"""
showStudioAsText: Boolean
"""Custom CSS"""
css: String
cssEnabled: Boolean
"""Interface language"""
language: String
"""Slideshow Delay"""
slideshowDelay: Int
}
"""All configuration settings"""
@@ -208,3 +241,7 @@ type StashConfig {
excludeVideo: Boolean!
excludeImage: Boolean!
}
input GenerateAPIKeyInput {
clear: Boolean
}

View File

@@ -6,17 +6,26 @@ enum SortDirectionEnum {
input FindFilterType {
q: String
page: Int
"""use per_page = -1 to indicate all results. Defaults to 25."""
per_page: Int
sort: String
direction: SortDirectionEnum
}
enum ResolutionEnum {
"144p", VERY_LOW
"240p", LOW
"360p", R360P
"480p", STANDARD
"540p", WEB_HD
"720p", STANDARD_HD
"1080p", FULL_HD
"1440p", QUAD_HD
"1920p", VR_HD
"4k", FOUR_K
"5k", FIVE_K
"6k", SIX_K
"8k", EIGHT_K
}
input PerformerFilterType {
@@ -38,7 +47,7 @@ input PerformerFilterType {
measurements: StringCriterionInput
"""Filter by fake tits value"""
fake_tits: StringCriterionInput
"""Filter by career length"""
"""Filter by career length"""
career_length: StringCriterionInput
"""Filter by tattoos"""
tattoos: StringCriterionInput
@@ -50,8 +59,28 @@ input PerformerFilterType {
gender: GenderCriterionInput
"""Filter to only include performers missing this property"""
is_missing: String
"""Filter to only include performers with these tags"""
tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter by scene count"""
scene_count: IntCriterionInput
"""Filter by image count"""
image_count: IntCriterionInput
"""Filter by gallery count"""
gallery_count: IntCriterionInput
"""Filter by StashID"""
stash_id: String
stash_id: StringCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by url"""
url: StringCriterionInput
"""Filter by hair color"""
hair_color: StringCriterionInput
"""Filter by weight"""
weight: IntCriterionInput
"""Filter by death year"""
death_year: IntCriterionInput
}
input SceneMarkerFilterType {
@@ -66,10 +95,16 @@ input SceneMarkerFilterType {
}
input SceneFilterType {
AND: SceneFilterType
OR: SceneFilterType
NOT: SceneFilterType
"""Filter by path"""
path: StringCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by organized"""
organized: Boolean
"""Filter by o-counter"""
o_counter: IntCriterionInput
"""Filter by resolution"""
@@ -86,10 +121,18 @@ input SceneFilterType {
movies: MultiCriterionInput
"""Filter to only include scenes with these tags"""
tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include scenes with performers with these tags"""
performer_tags: MultiCriterionInput
"""Filter to only include scenes with these performers"""
performers: MultiCriterionInput
"""Filter by performer count"""
performer_count: IntCriterionInput
"""Filter by StashID"""
stash_id: String
stash_id: StringCriterionInput
"""Filter by url"""
url: StringCriterionInput
}
input MovieFilterType {
@@ -97,18 +140,34 @@ input MovieFilterType {
studios: MultiCriterionInput
"""Filter to only include movies missing this property"""
is_missing: String
"""Filter by url"""
url: StringCriterionInput
}
input StudioFilterType {
"""Filter to only include studios with this parent studio"""
parents: MultiCriterionInput
"""Filter by StashID"""
stash_id: String
stash_id: StringCriterionInput
"""Filter to only include studios missing this property"""
is_missing: String
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by scene count"""
scene_count: IntCriterionInput
"""Filter by image count"""
image_count: IntCriterionInput
"""Filter by gallery count"""
gallery_count: IntCriterionInput
"""Filter by url"""
url: StringCriterionInput
}
input GalleryFilterType {
AND: GalleryFilterType
OR: GalleryFilterType
NOT: GalleryFilterType
"""Filter by path"""
path: StringCriterionInput
"""Filter to only include galleries missing this property"""
@@ -117,34 +176,63 @@ input GalleryFilterType {
is_zip: Boolean
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by organized"""
organized: Boolean
"""Filter by average image resolution"""
average_resolution: ResolutionEnum
"""Filter to only include scenes with this studio"""
"""Filter to only include galleries with this studio"""
studios: MultiCriterionInput
"""Filter to only include scenes with these tags"""
"""Filter to only include galleries with these tags"""
tags: MultiCriterionInput
"""Filter to only include scenes with these performers"""
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include galleries with performers with these tags"""
performer_tags: MultiCriterionInput
"""Filter to only include galleries with these performers"""
performers: MultiCriterionInput
"""Filter by performer count"""
performer_count: IntCriterionInput
"""Filter by number of images in this gallery"""
image_count: IntCriterionInput
"""Filter by url"""
url: StringCriterionInput
}
input TagFilterType {
AND: TagFilterType
OR: TagFilterType
NOT: TagFilterType
"""Filter to only include tags missing this property"""
is_missing: String
"""Filter by number of scenes with this tag"""
scene_count: IntCriterionInput
"""Filter by number of images with this tag"""
image_count: IntCriterionInput
"""Filter by number of galleries with this tag"""
gallery_count: IntCriterionInput
"""Filter by number of performers with this tag"""
performer_count: IntCriterionInput
"""Filter by number of markers with this tag"""
marker_count: IntCriterionInput
}
input ImageFilterType {
AND: ImageFilterType
OR: ImageFilterType
NOT: ImageFilterType
"""Filter by path"""
path: StringCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by organized"""
organized: Boolean
"""Filter by o-counter"""
o_counter: IntCriterionInput
"""Filter by resolution"""
@@ -155,8 +243,14 @@ input ImageFilterType {
studios: MultiCriterionInput
"""Filter to only include images with these tags"""
tags: MultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include images with performers with these tags"""
performer_tags: MultiCriterionInput
"""Filter to only include images with these performers"""
performers: MultiCriterionInput
"""Filter by performer count"""
performer_count: IntCriterionInput
"""Filter to only include images with these galleries"""
galleries: MultiCriterionInput
}
@@ -178,6 +272,10 @@ enum CriterionModifier {
INCLUDES_ALL,
INCLUDES,
EXCLUDES,
"""MATCHES REGEX"""
MATCHES_REGEX,
"""NOT MATCHES REGEX"""
NOT_MATCHES_REGEX,
}
input StringCriterionInput {

View File

@@ -8,7 +8,8 @@ type Gallery {
date: String
details: String
rating: Int
scene: Scene
organized: Boolean!
scenes: [Scene!]!
studio: Studio
image_count: Int!
tags: [Tag!]!
@@ -31,7 +32,8 @@ input GalleryCreateInput {
date: String
details: String
rating: Int
scene_id: ID
organized: Boolean
scene_ids: [ID!]
studio_id: ID
tag_ids: [ID!]
performer_ids: [ID!]
@@ -45,7 +47,8 @@ input GalleryUpdateInput {
date: String
details: String
rating: Int
scene_id: ID
organized: Boolean
scene_ids: [ID!]
studio_id: ID
tag_ids: [ID!]
performer_ids: [ID!]
@@ -58,7 +61,8 @@ input BulkGalleryUpdateInput {
date: String
details: String
rating: Int
scene_id: ID
organized: Boolean
scene_ids: BulkUpdateIds
studio_id: ID
tag_ids: BulkUpdateIds
performer_ids: BulkUpdateIds

View File

@@ -4,6 +4,7 @@ type Image {
title: String
rating: Int
o_counter: Int
organized: Boolean!
path: String!
file: ImageFileType! # Resolver
@@ -31,6 +32,7 @@ input ImageUpdateInput {
id: ID!
title: String
rating: Int
organized: Boolean
studio_id: ID
performer_ids: [ID!]
@@ -43,6 +45,7 @@ input BulkImageUpdateInput {
ids: [ID!]
title: String
rating: Int
organized: Boolean
studio_id: ID
performer_ids: BulkUpdateIds

View File

@@ -7,6 +7,7 @@ input GenerateMetadataInput {
previewOptions: GeneratePreviewOptionsInput
markers: Boolean!
transcodes: Boolean!
phashes: Boolean!
"""scene ids to generate for"""
sceneIDs: [ID!]
@@ -32,10 +33,28 @@ input GeneratePreviewOptionsInput {
input ScanMetadataInput {
paths: [String!]
useFileMetadata: Boolean!
"""Set name, date, details from metadata (if present)"""
useFileMetadata: Boolean
"""Strip file extension from title"""
stripFileExtension: Boolean
"""Generate previews during scan"""
scanGeneratePreviews: Boolean
"""Generate image previews during scan"""
scanGenerateImagePreviews: Boolean
"""Generate sprites during scan"""
scanGenerateSprites: Boolean
"""Generate phashes during scan"""
scanGeneratePhashes: Boolean
}
input CleanMetadataInput {
"""Do a dry run. Don't delete any files"""
dryRun: Boolean!
}
input AutoTagMetadataInput {
"""Paths to tag, null for all files"""
paths: [String!]
"""IDs of performers to tag files with, or "*" for all"""
performers: [String!]
"""IDs of studios to tag files with, or "*" for all"""
@@ -83,3 +102,25 @@ input ImportObjectsInput {
duplicateBehaviour: ImportDuplicateEnum!
missingRefBehaviour: ImportMissingRefEnum!
}
input BackupDatabaseInput {
download: Boolean
}
enum SystemStatusEnum {
SETUP
NEEDS_MIGRATION
OK
}
type SystemStatus {
databaseSchema: Int
databasePath: String
configPath: String
appSchema: Int!
status: SystemStatusEnum!
}
input MigrateInput {
backupPath: String!
}

View File

@@ -28,8 +28,9 @@ input MovieCreateInput {
director: String
synopsis: String
url: String
"""This should be base64 encoded"""
"""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
}
@@ -44,8 +45,9 @@ input MovieUpdateInput {
director: String
synopsis: String
url: String
"""This should be base64 encoded"""
"""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
}

View File

@@ -27,11 +27,19 @@ type Performer {
piercings: String
aliases: String
favorite: Boolean!
tags: [Tag!]!
image_path: String # Resolver
scene_count: Int # Resolver
image_count: Int # Resolver
gallery_count: Int # Resolver
scenes: [Scene!]!
stash_ids: [StashID!]!
rating: Int
details: String
death_date: String
hair_color: String
weight: Int
}
input PerformerCreateInput {
@@ -52,9 +60,15 @@ input PerformerCreateInput {
twitter: String
instagram: String
favorite: Boolean
"""This should be base64 encoded"""
tag_ids: [ID!]
"""This should be a URL or a base64 encoded data URL"""
image: String
stash_ids: [StashIDInput!]
rating: Int
details: String
death_date: String
hair_color: String
weight: Int
}
input PerformerUpdateInput {
@@ -76,9 +90,42 @@ input PerformerUpdateInput {
twitter: String
instagram: String
favorite: Boolean
"""This should be base64 encoded"""
tag_ids: [ID!]
"""This should be a URL or a base64 encoded data URL"""
image: String
stash_ids: [StashIDInput!]
rating: Int
details: String
death_date: String
hair_color: String
weight: Int
}
input BulkPerformerUpdateInput {
clientMutationId: String
ids: [ID!]
url: String
gender: GenderEnum
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
twitter: String
instagram: String
favorite: Boolean
tag_ids: BulkUpdateIds
rating: Int
details: String
death_date: String
hair_color: String
weight: Int
}
input PerformerDestroyInput {

View File

@@ -16,6 +16,7 @@ type ScenePathsType {
webp: String # Resolver
vtt: String # Resolver
chapters_vtt: String # Resolver
sprite: String # Resolver
}
type SceneMovie {
@@ -32,14 +33,16 @@ type Scene {
url: String
date: String
rating: Int
organized: Boolean!
o_counter: Int
path: String!
phash: String
file: SceneFileType! # Resolver
paths: ScenePathsType! # Resolver
scene_markers: [SceneMarker!]!
gallery: Gallery
galleries: [Gallery!]!
studio: Studio
movies: [SceneMovie!]!
tags: [Tag!]!
@@ -60,12 +63,13 @@ input SceneUpdateInput {
url: String
date: String
rating: Int
organized: Boolean
studio_id: ID
gallery_id: ID
gallery_ids: [ID!]
performer_ids: [ID!]
movies: [SceneMovieInput!]
tag_ids: [ID!]
"""This should be base64 encoded"""
"""This should be a URL or a base64 encoded data URL"""
cover_image: String
stash_ids: [StashIDInput!]
}
@@ -89,8 +93,9 @@ input BulkSceneUpdateInput {
url: String
date: String
rating: Int
organized: Boolean
studio_id: ID
gallery_id: ID
gallery_ids: BulkUpdateIds
performer_ids: BulkUpdateIds
tag_ids: BulkUpdateIds
}
@@ -131,7 +136,7 @@ type SceneParserResult {
date: String
rating: Int
studio_id: ID
gallery_id: ID
gallery_ids: [ID!]
performer_ids: [ID!]
movies: [SceneMovieID!]
tag_ids: [ID!]

View File

@@ -17,8 +17,9 @@ type ScrapedMovie {
synopsis: String
studio: ScrapedMovieStudio
"""This should be base64 encoded"""
"""This should be a base64 encoded data URL"""
front_image: String
"""This should be a base64 encoded data URL"""
back_image: String
}

View File

@@ -16,9 +16,15 @@ type ScrapedPerformer {
tattoos: String
piercings: String
aliases: String
# Should be ScrapedPerformerTag - but would be identical types
tags: [ScrapedSceneTag!]
"""This should be base64 encoded"""
"""This should be a base64 encoded data URL"""
image: String
details: String
death_date: String
hair_color: String
weight: String
}
input ScrapedPerformerInput {
@@ -39,5 +45,10 @@ input ScrapedPerformerInput {
piercings: String
aliases: String
# not including tags for the input
# not including image for the input
details: String
death_date: String
hair_color: String
weight: String
}

View File

@@ -45,9 +45,14 @@ type ScrapedScenePerformer {
tattoos: String
piercings: String
aliases: String
tags: [ScrapedSceneTag!]
remote_site_id: String
images: [String!]
details: String
death_date: String
hair_color: String
weight: String
}
type ScrapedSceneMovie {
@@ -84,7 +89,7 @@ type ScrapedScene {
url: String
date: String
"""This should be base64 encoded"""
"""This should be a base64 encoded data URL"""
image: String
file: SceneFileType # Resolver
@@ -110,7 +115,7 @@ type ScrapedGallery {
performers: [ScrapedScenePerformer!]
}
input StashBoxQueryInput {
input StashBoxSceneQueryInput {
"""Index of the configured stash-box instance to use"""
stash_box_index: Int!
"""Instructs query by scene fingerprints"""
@@ -119,8 +124,30 @@ input StashBoxQueryInput {
q: String
}
input StashBoxPerformerQueryInput {
"""Index of the configured stash-box instance to use"""
stash_box_index: Int!
"""Instructs query by scene fingerprints"""
performer_ids: [ID!]
"""Query by query string"""
q: String
}
type StashBoxPerformerQueryResult {
query: String!
results: [ScrapedScenePerformer!]!
}
type StashBoxFingerprint {
algorithm: String!
hash: String!
duration: Int!
}
input StashBoxBatchPerformerTagInput {
endpoint: Int!
exclude_fields: [String!]
refresh: Boolean!
performer_ids: [ID!]
performer_names: [String!]
}

View File

@@ -1,8 +1,8 @@
type StatsResultType {
scene_count: Int!
scenes_size: Int!
scenes_size: Float!
image_count: Int!
images_size: Int!
images_size: Float!
gallery_count: Int!
performer_count: Int!
studio_count: Int!

View File

@@ -8,16 +8,22 @@ type Studio {
image_path: String # Resolver
scene_count: Int # Resolver
image_count: Int # Resolver
gallery_count: Int # Resolver
stash_ids: [StashID!]!
rating: Int
details: String
}
input StudioCreateInput {
name: String!
url: String
parent_id: ID
"""This should be base64 encoded"""
"""This should be a URL or a base64 encoded data URL"""
image: String
stash_ids: [StashIDInput!]
rating: Int
details: String
}
input StudioUpdateInput {
@@ -25,9 +31,11 @@ input StudioUpdateInput {
name: String
url: String
parent_id: ID,
"""This should be base64 encoded"""
"""This should be a URL or a base64 encoded data URL"""
image: String
stash_ids: [StashIDInput!]
rating: Int
details: String
}
input StudioDestroyInput {

View File

@@ -5,12 +5,15 @@ type Tag {
image_path: String # Resolver
scene_count: Int # Resolver
scene_marker_count: Int # Resolver
image_count: Int # Resolver
gallery_count: Int # Resolver
performer_count: Int
}
input TagCreateInput {
name: String!
"""This should be base64 encoded"""
"""This should be a URL or a base64 encoded data URL"""
image: String
}
@@ -18,7 +21,7 @@ input TagUpdateInput {
id: ID!
name: String!
"""This should be base64 encoded"""
"""This should be a URL or a base64 encoded data URL"""
image: String
}

View File

@@ -75,6 +75,11 @@ fragment PerformerFragment on Performer {
piercings {
...BodyModificationFragment
}
details
death_date {
...FuzzyDateFragment
}
weight
}
fragment PerformerAppearanceFragment on PerformerAppearance {
@@ -134,6 +139,18 @@ query SearchScene($term: String!) {
}
}
query SearchPerformer($term: String!) {
searchPerformer(term: $term) {
...PerformerFragment
}
}
query FindPerformerByID($id: ID!) {
findPerformer(id: $id) {
...PerformerFragment
}
}
mutation SubmitFingerprint($input: FingerprintSubmission!) {
submitFingerprint(input: $input)
}

View File

@@ -3,9 +3,7 @@ package main
import (
"github.com/stashapp/stash/pkg/api"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
_ "github.com/golang-migrate/migrate/v4/source/file"
@@ -13,12 +11,6 @@ import (
func main() {
manager.Initialize()
// perform the post-migration for new databases
if database.Initialize(config.GetDatabasePath()) {
manager.GetInstance().PostMigrate()
}
api.Start()
blockForever()
}

View File

@@ -0,0 +1,141 @@
package api
import (
"context"
"database/sql"
"strconv"
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/models"
)
const updateInputField = "input"
func getArgumentMap(ctx context.Context) map[string]interface{} {
rctx := graphql.GetResolverContext(ctx)
reqCtx := graphql.GetRequestContext(ctx)
return rctx.Field.ArgumentMap(reqCtx.Variables)
}
func getUpdateInputMap(ctx context.Context) map[string]interface{} {
args := getArgumentMap(ctx)
input, _ := args[updateInputField]
var ret map[string]interface{}
if input != nil {
ret, _ = input.(map[string]interface{})
}
if ret == nil {
ret = make(map[string]interface{})
}
return ret
}
func getUpdateInputMaps(ctx context.Context) []map[string]interface{} {
args := getArgumentMap(ctx)
input, _ := args[updateInputField]
var ret []map[string]interface{}
if input != nil {
// convert []interface{} into []map[string]interface{}
iSlice, _ := input.([]interface{})
for _, i := range iSlice {
m, _ := i.(map[string]interface{})
if m != nil {
ret = append(ret, m)
}
}
}
return ret
}
type changesetTranslator struct {
inputMap map[string]interface{}
}
func (t changesetTranslator) hasField(field string) bool {
if t.inputMap == nil {
return false
}
_, found := t.inputMap[field]
return found
}
func (t changesetTranslator) nullString(value *string, field string) *sql.NullString {
if !t.hasField(field) {
return nil
}
ret := &sql.NullString{}
if value != nil {
ret.String = *value
ret.Valid = true
}
return ret
}
func (t changesetTranslator) sqliteDate(value *string, field string) *models.SQLiteDate {
if !t.hasField(field) {
return nil
}
ret := &models.SQLiteDate{}
if value != nil {
ret.String = *value
ret.Valid = true
}
return ret
}
func (t changesetTranslator) nullInt64(value *int, field string) *sql.NullInt64 {
if !t.hasField(field) {
return nil
}
ret := &sql.NullInt64{}
if value != nil {
ret.Int64 = int64(*value)
ret.Valid = true
}
return ret
}
func (t changesetTranslator) nullInt64FromString(value *string, field string) *sql.NullInt64 {
if !t.hasField(field) {
return nil
}
ret := &sql.NullInt64{}
if value != nil {
ret.Int64, _ = strconv.ParseInt(*value, 10, 64)
ret.Valid = true
}
return ret
}
func (t changesetTranslator) nullBool(value *bool, field string) *sql.NullBool {
if !t.hasField(field) {
return nil
}
ret := &sql.NullBool{}
if value != nil {
ret.Bool = *value
ret.Valid = true
}
return ret
}

View File

@@ -10,6 +10,8 @@ import (
"runtime"
"time"
"golang.org/x/sys/cpu"
"github.com/stashapp/stash/pkg/logger"
)
@@ -18,6 +20,7 @@ const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases
const apiTags string = "https://api.github.com/repos/stashapp/stash/tags"
const apiAcceptHeader string = "application/vnd.github.v3+json"
const developmentTag string = "latest_develop"
const defaultSHLength int = 7 // default length of SHA short hash returned by <git rev-parse --short HEAD>
// ErrNoVersion indicates that no version information has been embedded in the
// stash binary
@@ -25,10 +28,12 @@ var ErrNoVersion = errors.New("no stash version")
var stashReleases = func() map[string]string {
return map[string]string{
"windows/amd64": "stash-win.exe",
"linux/amd64": "stash-linux",
"darwin/amd64": "stash-osx",
"linux/amd64": "stash-linux",
"windows/amd64": "stash-win.exe",
"linux/arm": "stash-pi",
"linux/arm64": "stash-linux-arm64v8",
"linux/armv7": "stash-linux-arm32v7",
}
}
@@ -140,7 +145,13 @@ func makeGithubRequest(url string, output interface{}) error {
// which is the latest pre-release build.
func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease string, err error) {
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
arch := runtime.GOARCH // https://en.wikipedia.org/wiki/Comparison_of_ARM_cores
isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4 // armv6 doesn't support any of these features
if arch == "arm" && isARMv7 {
arch = "armv7"
}
platform := fmt.Sprintf("%s/%s", runtime.GOOS, arch)
wantedRelease := stashReleases()[platform]
version, _, _ := GetVersion()
@@ -191,14 +202,20 @@ func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease strin
}
func getReleaseHash(release githubReleasesResponse, shortHash bool, usePreRelease bool) string {
shaLength := len(release.Target_commitish)
// the /latest API call doesn't return the hash in target_commitish
// also add sanity check in case Target_commitish is not 40 characters
if !usePreRelease || len(release.Target_commitish) != 40 {
if !usePreRelease || shaLength != 40 {
return getShaFromTags(shortHash, release.Tag_name)
}
if shortHash {
return release.Target_commitish[0:7] //shorthash is first 7 digits of git commit hash
last := defaultSHLength // default length of git short hash
_, gitShort, _ := GetVersion() // retrieve it to check actual length
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
last = len(gitShort)
}
return release.Target_commitish[0:last]
}
return release.Target_commitish
@@ -229,14 +246,20 @@ func getShaFromTags(shortHash bool, name string) string {
logger.Errorf("Github Tags Api %v", err)
return ""
}
_, gitShort, _ := GetVersion() // retrieve short hash to check actual length
for _, tag := range tags {
if tag.Name == name {
if len(tag.Commit.Sha) != 40 {
shaLength := len(tag.Commit.Sha)
if shaLength != 40 {
return ""
}
if shortHash {
return tag.Commit.Sha[0:7] //shorthash is first 7 digits of git commit hash
last := defaultSHLength // default length of git short hash
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
last = len(gitShort)
}
return tag.Commit.Sha[0:last]
}
return tag.Commit.Sha

View File

@@ -1,96 +0,0 @@
package api
import (
"fmt"
"html/template"
"net/http"
"os"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
)
type migrateData struct {
ExistingVersion uint
MigrateVersion uint
BackupPath string
}
func getMigrateData() migrateData {
return migrateData{
ExistingVersion: database.Version(),
MigrateVersion: database.AppSchemaVersion(),
BackupPath: database.DatabaseBackupPath(),
}
}
func getMigrateHandler(w http.ResponseWriter, r *http.Request) {
if !database.NeedsMigration() {
http.Redirect(w, r, "/", 301)
return
}
data, _ := setupUIBox.Find("migrate.html")
templ, err := template.New("Migrate").Parse(string(data))
if err != nil {
http.Error(w, fmt.Sprintf("error: %s", err), 500)
return
}
err = templ.Execute(w, getMigrateData())
if err != nil {
http.Error(w, fmt.Sprintf("error: %s", err), 500)
}
}
func doMigrateHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, fmt.Sprintf("error: %s", err), 500)
}
formBackupPath := r.Form.Get("backuppath")
// always backup so that we can roll back to the previous version if
// migration fails
backupPath := formBackupPath
if formBackupPath == "" {
backupPath = database.DatabaseBackupPath()
}
// perform database backup
if err = database.Backup(backupPath); err != nil {
http.Error(w, fmt.Sprintf("error backing up database: %s", err), 500)
return
}
err = database.RunMigrations()
if err != nil {
errStr := fmt.Sprintf("error performing migration: %s", err)
// roll back to the backed up version
restoreErr := database.RestoreFromBackup(backupPath)
if restoreErr != nil {
errStr = fmt.Sprintf("ERROR: unable to restore database from backup after migration failure: %s\n%s", restoreErr.Error(), errStr)
} else {
errStr = "An error occurred migrating the database to the latest schema version. The backup database file was automatically renamed to restore the database.\n" + errStr
}
http.Error(w, errStr, 500)
return
}
// perform post-migration operations
manager.GetInstance().PostMigrate()
// if no backup path was provided, then delete the created backup
if formBackupPath == "" {
err = os.Remove(backupPath)
if err != nil {
logger.Warnf("error removing unwanted database backup (%s): %s", backupPath, err.Error())
}
}
http.Redirect(w, r, "/", 301)
}

View File

@@ -5,12 +5,13 @@ import (
"sort"
"strconv"
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)
type Resolver struct{}
type Resolver struct {
txnManager models.TransactionManager
}
func (r *Resolver) Gallery() models.GalleryResolver {
return &galleryResolver{r}
@@ -79,69 +80,84 @@ type scrapedSceneMovieResolver struct{ *Resolver }
type scrapedScenePerformerResolver struct{ *Resolver }
type scrapedSceneStudioResolver struct{ *Resolver }
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) ([]*models.SceneMarker, error) {
qb := models.NewSceneMarkerQueryBuilder()
return qb.Wall(q)
func (r *Resolver) withTxn(ctx context.Context, fn func(r models.Repository) error) error {
return r.txnManager.WithTxn(ctx, fn)
}
func (r *queryResolver) SceneWall(ctx context.Context, q *string) ([]*models.Scene, error) {
qb := models.NewSceneQueryBuilder()
return qb.Wall(q)
func (r *Resolver) withReadTxn(ctx context.Context, fn func(r models.ReaderRepository) error) error {
return r.txnManager.WithReadTxn(ctx, fn)
}
func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *string) ([]*models.MarkerStringsResultType, error) {
qb := models.NewSceneMarkerQueryBuilder()
return qb.GetMarkerStrings(q, sort)
}
func (r *queryResolver) ValidGalleriesForScene(ctx context.Context, scene_id *string) ([]*models.Gallery, error) {
if scene_id == nil {
panic("nil scene id") // TODO make scene_id mandatory
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.SceneMarker().Wall(q)
return err
}); err != nil {
return nil, err
}
sceneID, _ := strconv.Atoi(*scene_id)
sqb := models.NewSceneQueryBuilder()
scene, err := sqb.Find(sceneID)
if err != nil {
return ret, nil
}
func (r *queryResolver) SceneWall(ctx context.Context, q *string) (ret []*models.Scene, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Scene().Wall(q)
return err
}); err != nil {
return nil, err
}
qb := models.NewGalleryQueryBuilder()
validGalleries, err := qb.ValidGalleriesForScenePath(scene.Path)
sceneGallery, _ := qb.FindBySceneID(sceneID, nil)
if sceneGallery != nil {
validGalleries = append(validGalleries, sceneGallery)
return ret, nil
}
func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *string) (ret []*models.MarkerStringsResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.SceneMarker().GetMarkerStrings(q, sort)
return err
}); err != nil {
return nil, err
}
return validGalleries, nil
return ret, nil
}
func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) {
scenesQB := models.NewSceneQueryBuilder()
scenesCount, _ := scenesQB.Count()
scenesSize, _ := scenesQB.Size()
imageQB := models.NewImageQueryBuilder()
imageCount, _ := imageQB.Count()
imageSize, _ := imageQB.Size()
galleryQB := models.NewGalleryQueryBuilder()
galleryCount, _ := galleryQB.Count()
performersQB := models.NewPerformerQueryBuilder()
performersCount, _ := performersQB.Count()
studiosQB := models.NewStudioQueryBuilder()
studiosCount, _ := studiosQB.Count()
moviesQB := models.NewMovieQueryBuilder()
moviesCount, _ := moviesQB.Count()
tagsQB := models.NewTagQueryBuilder()
tagsCount, _ := tagsQB.Count()
return &models.StatsResultType{
SceneCount: scenesCount,
ScenesSize: int(scenesSize),
ImageCount: imageCount,
ImagesSize: int(imageSize),
GalleryCount: galleryCount,
PerformerCount: performersCount,
StudioCount: studiosCount,
MovieCount: moviesCount,
TagCount: tagsCount,
}, nil
var ret models.StatsResultType
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
scenesQB := repo.Scene()
imageQB := repo.Image()
galleryQB := repo.Gallery()
studiosQB := repo.Studio()
performersQB := repo.Performer()
moviesQB := repo.Movie()
tagsQB := repo.Tag()
scenesCount, _ := scenesQB.Count()
scenesSize, _ := scenesQB.Size()
imageCount, _ := imageQB.Count()
imageSize, _ := imageQB.Size()
galleryCount, _ := galleryQB.Count()
performersCount, _ := performersQB.Count()
studiosCount, _ := studiosQB.Count()
moviesCount, _ := moviesQB.Count()
tagsCount, _ := tagsQB.Count()
ret = models.StatsResultType{
SceneCount: scenesCount,
ScenesSize: scenesSize,
ImageCount: imageCount,
ImagesSize: imageSize,
GalleryCount: galleryCount,
PerformerCount: performersCount,
StudioCount: studiosCount,
MovieCount: moviesCount,
TagCount: tagsCount,
}
return nil
}); err != nil {
return nil, err
}
return &ret, nil
}
func (r *queryResolver) Version(ctx context.Context) (*models.Version, error) {
@@ -171,31 +187,41 @@ func (r *queryResolver) Latestversion(ctx context.Context) (*models.ShortVersion
// Get scene marker tags which show up under the video.
func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([]*models.SceneMarkerTag, error) {
sceneID, _ := strconv.Atoi(scene_id)
sqb := models.NewSceneMarkerQueryBuilder()
sceneMarkers, err := sqb.FindBySceneID(sceneID, nil)
sceneID, err := strconv.Atoi(scene_id)
if err != nil {
return nil, err
}
tags := make(map[int]*models.SceneMarkerTag)
var keys []int
tqb := models.NewTagQueryBuilder()
for _, sceneMarker := range sceneMarkers {
markerPrimaryTag, err := tqb.Find(sceneMarker.PrimaryTagID, nil)
tags := make(map[int]*models.SceneMarkerTag)
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
sceneMarkers, err := repo.SceneMarker().FindBySceneID(sceneID)
if err != nil {
return nil, err
return err
}
_, hasKey := tags[markerPrimaryTag.ID]
var sceneMarkerTag *models.SceneMarkerTag
if !hasKey {
sceneMarkerTag = &models.SceneMarkerTag{Tag: markerPrimaryTag}
tags[markerPrimaryTag.ID] = sceneMarkerTag
keys = append(keys, markerPrimaryTag.ID)
} else {
sceneMarkerTag = tags[markerPrimaryTag.ID]
tqb := repo.Tag()
for _, sceneMarker := range sceneMarkers {
markerPrimaryTag, err := tqb.Find(sceneMarker.PrimaryTagID)
if err != nil {
return err
}
_, hasKey := tags[markerPrimaryTag.ID]
var sceneMarkerTag *models.SceneMarkerTag
if !hasKey {
sceneMarkerTag = &models.SceneMarkerTag{Tag: markerPrimaryTag}
tags[markerPrimaryTag.ID] = sceneMarkerTag
keys = append(keys, markerPrimaryTag.ID)
} else {
sceneMarkerTag = tags[markerPrimaryTag.ID]
}
tags[markerPrimaryTag.ID].SceneMarkers = append(tags[markerPrimaryTag.ID].SceneMarkers, sceneMarker)
}
tags[markerPrimaryTag.ID].SceneMarkers = append(tags[markerPrimaryTag.ID].SceneMarkers, sceneMarker)
return nil
}); err != nil {
return nil, err
}
// Sort so that primary tags that show up earlier in the video are first.
@@ -212,13 +238,3 @@ func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([
return result, nil
}
// wasFieldIncluded returns true if the given field was included in the request.
// Slices are unmarshalled to empty slices even if the field was omitted. This
// method determines if it was omitted altogether.
func wasFieldIncluded(ctx context.Context, field string) bool {
rctx := graphql.GetRequestContext(ctx)
_, ret := rctx.Variables[field]
return ret
}

View File

@@ -22,30 +22,39 @@ func (r *galleryResolver) Title(ctx context.Context, obj *models.Gallery) (*stri
return nil, nil
}
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) ([]*models.Image, error) {
qb := models.NewImageQueryBuilder()
return qb.FindByGalleryID(obj.ID)
}
func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (*models.Image, error) {
qb := models.NewImageQueryBuilder()
imgs, err := qb.FindByGalleryID(obj.ID)
if err != nil {
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) (ret []*models.Image, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Image().FindByGalleryID(obj.ID)
return err
}); err != nil {
return nil, err
}
var ret *models.Image
if len(imgs) > 0 {
ret = imgs[0]
}
return ret, nil
}
for _, img := range imgs {
if image.IsCover(img) {
ret = img
break
func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (ret *models.Image, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
imgs, err := repo.Image().FindByGalleryID(obj.ID)
if err != nil {
return err
}
if len(imgs) > 0 {
ret = imgs[0]
}
for _, img := range imgs {
if image.IsCover(img) {
ret = img
break
}
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
@@ -81,35 +90,66 @@ func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int
return nil, nil
}
func (r *galleryResolver) Scene(ctx context.Context, obj *models.Gallery) (*models.Scene, error) {
if !obj.SceneID.Valid {
return nil, nil
func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Scene().FindByGalleryID(obj.ID)
return err
}); err != nil {
return nil, err
}
qb := models.NewSceneQueryBuilder()
return qb.Find(int(obj.SceneID.Int64))
return ret, nil
}
func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (*models.Studio, error) {
func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret *models.Studio, err error) {
if !obj.StudioID.Valid {
return nil, nil
}
qb := models.NewStudioQueryBuilder()
return qb.Find(int(obj.StudioID.Int64), nil)
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) ([]*models.Tag, error) {
qb := models.NewTagQueryBuilder()
return qb.FindByGalleryID(obj.ID, nil)
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Tag().FindByGalleryID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) ([]*models.Performer, error) {
qb := models.NewPerformerQueryBuilder()
return qb.FindByGalleryID(obj.ID, nil)
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Performer().FindByGalleryID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (int, error) {
qb := models.NewImageQueryBuilder()
return qb.CountByGalleryID(obj.ID)
func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (ret int, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Image().CountByGalleryID(obj.ID)
return err
}); err != nil {
return 0, err
}
return ret, nil
}

View File

@@ -34,7 +34,7 @@ func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*models.Im
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*models.ImagePathsType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewImageURLBuilder(baseURL, obj.ID)
builder := urlbuilders.NewImageURLBuilder(baseURL, obj)
thumbnailPath := builder.GetThumbnailURL()
imagePath := builder.GetImageURL()
return &models.ImagePathsType{
@@ -43,26 +43,51 @@ func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*models.I
}, nil
}
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) ([]*models.Gallery, error) {
qb := models.NewGalleryQueryBuilder()
return qb.FindByImageID(obj.ID, nil)
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret []*models.Gallery, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Gallery().FindByImageID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (*models.Studio, error) {
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *models.Studio, err error) {
if !obj.StudioID.Valid {
return nil, nil
}
qb := models.NewStudioQueryBuilder()
return qb.Find(int(obj.StudioID.Int64), nil)
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) ([]*models.Tag, error) {
qb := models.NewTagQueryBuilder()
return qb.FindByImageID(obj.ID, nil)
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindByImageID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) ([]*models.Performer, error) {
qb := models.NewPerformerQueryBuilder()
return qb.FindByImageID(obj.ID, nil)
func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret []*models.Performer, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().FindByImageID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -53,10 +53,16 @@ func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, er
return nil, nil
}
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (*models.Studio, error) {
qb := models.NewStudioQueryBuilder()
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *models.Studio, err error) {
if obj.StudioID.Valid {
return qb.Find(int(obj.StudioID.Int64), nil)
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
}
return nil, nil
@@ -78,18 +84,42 @@ func (r *movieResolver) Synopsis(ctx context.Context, obj *models.Movie) (*strin
func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
frontimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj.ID).GetMovieFrontImageURL()
frontimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieFrontImageURL()
return &frontimagePath, nil
}
func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
// don't return any thing if there is no back image
var img []byte
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
img, err = repo.Movie().GetBackImage(obj.ID)
if err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
if img == nil {
return nil, nil
}
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
backimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj.ID).GetMovieBackImageURL()
backimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieBackImageURL()
return &backimagePath, nil
}
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (*int, error) {
qb := models.NewSceneQueryBuilder()
res, err := qb.CountByMovieID(obj.ID)
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = repo.Scene().CountByMovieID(obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, err
}

View File

@@ -4,6 +4,8 @@ import (
"context"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
)
@@ -134,22 +136,112 @@ func (r *performerResolver) Favorite(ctx context.Context, obj *models.Performer)
func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer) (*string, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
imagePath := urlbuilders.NewPerformerURLBuilder(baseURL, obj.ID).GetPerformerImageURL()
imagePath := urlbuilders.NewPerformerURLBuilder(baseURL, obj).GetPerformerImageURL()
return &imagePath, nil
}
func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (*int, error) {
qb := models.NewSceneQueryBuilder()
res, err := qb.CountByPerformerID(obj.ID)
return &res, err
func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindByPerformerID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) ([]*models.Scene, error) {
qb := models.NewSceneQueryBuilder()
return qb.FindByPerformerID(obj.ID)
func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = repo.Scene().CountByPerformerID(obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) {
qb := models.NewJoinsQueryBuilder()
return qb.GetPerformerStashIDs(obj.ID)
func (r *performerResolver) ImageCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = image.CountByPerformerID(repo.Image(), obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}
func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = gallery.CountByPerformerID(repo.Gallery(), obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}
func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (ret []*models.Scene, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Scene().FindByPerformerID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) (ret []*models.StashID, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().GetStashIDs(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
}
return nil, nil
}
func (r *performerResolver) Details(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Details.Valid {
return &obj.Details.String, nil
}
return nil, nil
}
func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.DeathDate.Valid {
return &obj.DeathDate.String, nil
}
return nil, nil
}
func (r *performerResolver) HairColor(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.HairColor.Valid {
return &obj.HairColor.String, nil
}
return nil, nil
}
func (r *performerResolver) Weight(ctx context.Context, obj *models.Performer) (*int, error) {
if obj.Weight.Valid {
weight := int(obj.Weight.Int64)
return &weight, nil
}
return nil, nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
@@ -78,11 +79,13 @@ func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.Sc
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.ScenePathsType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
builder.APIKey = config.GetInstance().GetAPIKey()
screenshotPath := builder.GetScreenshotURL(obj.UpdatedAt.Timestamp)
previewPath := builder.GetStreamPreviewURL()
streamPath := builder.GetStreamURL()
webpPath := builder.GetStreamPreviewImageURL()
vttPath := builder.GetSpriteVTTURL()
spritePath := builder.GetSpriteURL()
chaptersVttPath := builder.GetChaptersVTTURL()
return &models.ScenePathsType{
Screenshot: &screenshotPath,
@@ -91,67 +94,121 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.S
Webp: &webpPath,
Vtt: &vttPath,
ChaptersVtt: &chaptersVttPath,
Sprite: &spritePath,
}, nil
}
func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) ([]*models.SceneMarker, error) {
qb := models.NewSceneMarkerQueryBuilder()
return qb.FindBySceneID(obj.ID, nil)
}
func (r *sceneResolver) Gallery(ctx context.Context, obj *models.Scene) (*models.Gallery, error) {
qb := models.NewGalleryQueryBuilder()
return qb.FindBySceneID(obj.ID, nil)
}
func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (*models.Studio, error) {
qb := models.NewStudioQueryBuilder()
return qb.FindBySceneID(obj.ID)
}
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) ([]*models.SceneMovie, error) {
joinQB := models.NewJoinsQueryBuilder()
qb := models.NewMovieQueryBuilder()
sceneMovies, err := joinQB.GetSceneMovies(obj.ID, nil)
if err != nil {
func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (ret []*models.SceneMarker, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.SceneMarker().FindBySceneID(obj.ID)
return err
}); err != nil {
return nil, err
}
var ret []*models.SceneMovie
for _, sm := range sceneMovies {
movie, err := qb.Find(sm.MovieID, nil)
return ret, nil
}
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Gallery().FindBySceneID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *models.Studio, err error) {
if !obj.StudioID.Valid {
return nil, nil
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().Find(int(obj.StudioID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*models.SceneMovie, err error) {
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
mqb := repo.Movie()
sceneMovies, err := qb.GetMovies(obj.ID)
if err != nil {
return nil, err
return err
}
sceneIdx := sm.SceneIndex
sceneMovie := &models.SceneMovie{
Movie: movie,
for _, sm := range sceneMovies {
movie, err := mqb.Find(sm.MovieID)
if err != nil {
return err
}
sceneIdx := sm.SceneIndex
sceneMovie := &models.SceneMovie{
Movie: movie,
}
if sceneIdx.Valid {
var idx int
idx = int(sceneIdx.Int64)
sceneMovie.SceneIndex = &idx
}
ret = append(ret, sceneMovie)
}
if sceneIdx.Valid {
var idx int
idx = int(sceneIdx.Int64)
sceneMovie.SceneIndex = &idx
}
ret = append(ret, sceneMovie)
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) ([]*models.Tag, error) {
qb := models.NewTagQueryBuilder()
return qb.FindBySceneID(obj.ID, nil)
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindBySceneID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) ([]*models.Performer, error) {
qb := models.NewPerformerQueryBuilder()
return qb.FindBySceneID(obj.ID, nil)
func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().FindBySceneID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) ([]*models.StashID, error) {
qb := models.NewJoinsQueryBuilder()
return qb.GetSceneStashIDs(obj.ID)
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Scene().GetStashIDs(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *sceneResolver) Phash(ctx context.Context, obj *models.Scene) (*string, error) {
if obj.Phash.Valid {
hexval := utils.PhashToString(obj.Phash.Int64)
return &hexval, nil
}
return nil, nil
}

View File

@@ -2,29 +2,47 @@ package api
import (
"context"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/models"
)
func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker) (*models.Scene, error) {
func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker) (ret *models.Scene, err error) {
if !obj.SceneID.Valid {
panic("Invalid scene id")
}
qb := models.NewSceneQueryBuilder()
sceneID := int(obj.SceneID.Int64)
scene, err := qb.Find(sceneID)
return scene, err
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
sceneID := int(obj.SceneID.Int64)
ret, err = repo.Scene().Find(sceneID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneMarker) (*models.Tag, error) {
qb := models.NewTagQueryBuilder()
tag, err := qb.Find(obj.PrimaryTagID, nil)
return tag, err
func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneMarker) (ret *models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().Find(obj.PrimaryTagID)
return err
}); err != nil {
return nil, err
}
return ret, err
}
func (r *sceneMarkerResolver) Tags(ctx context.Context, obj *models.SceneMarker) ([]*models.Tag, error) {
qb := models.NewTagQueryBuilder()
return qb.FindBySceneMarkerID(obj.ID, nil)
func (r *sceneMarkerResolver) Tags(ctx context.Context, obj *models.SceneMarker) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindBySceneMarkerID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, err
}
func (r *sceneMarkerResolver) Stream(ctx context.Context, obj *models.SceneMarker) (string, error) {

View File

@@ -4,6 +4,8 @@ import (
"context"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
)
@@ -23,31 +25,109 @@ func (r *studioResolver) URL(ctx context.Context, obj *models.Studio) (*string,
func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*string, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj.ID).GetStudioImageURL()
imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj).GetStudioImageURL()
var hasImage bool
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
hasImage, err = repo.Studio().HasImage(obj.ID)
return err
}); err != nil {
return nil, err
}
// indicate that image is missing by setting default query param to true
if !hasImage {
imagePath = imagePath + "?default=true"
}
return &imagePath, nil
}
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (*int, error) {
qb := models.NewSceneQueryBuilder()
res, err := qb.CountByStudioID(obj.ID)
func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = repo.Scene().CountByStudioID(obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, err
}
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (*models.Studio, error) {
func (r *studioResolver) ImageCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = image.CountByStudioID(repo.Image(), obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}
func (r *studioResolver) GalleryCount(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = gallery.CountByStudioID(repo.Gallery(), obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
if !obj.ParentID.Valid {
return nil, nil
}
qb := models.NewStudioQueryBuilder()
return qb.Find(int(obj.ParentID.Int64), nil)
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().Find(int(obj.ParentID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) ([]*models.Studio, error) {
qb := models.NewStudioQueryBuilder()
return qb.FindChildren(obj.ID, nil)
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (ret []*models.Studio, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().FindChildren(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*models.StashID, error) {
qb := models.NewJoinsQueryBuilder()
return qb.GetStudioStashIDs(obj.ID)
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) (ret []*models.StashID, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().GetStashIDs(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
}
return nil, nil
}
func (r *studioResolver) Details(ctx context.Context, obj *models.Studio) (*string, error) {
if obj.Details.Valid {
return &obj.Details.String, nil
}
return nil, nil
}

View File

@@ -4,29 +4,73 @@ import (
"context"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
)
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (*int, error) {
qb := models.NewSceneQueryBuilder()
if obj == nil {
return nil, nil
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
var count int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
count, err = repo.Scene().CountByTagID(obj.ID)
return err
}); err != nil {
return nil, err
}
count, err := qb.CountByTagID(obj.ID)
return &count, err
}
func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (*int, error) {
qb := models.NewSceneMarkerQueryBuilder()
if obj == nil {
return nil, nil
func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
var count int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
count, err = repo.SceneMarker().CountByTagID(obj.ID)
return err
}); err != nil {
return nil, err
}
count, err := qb.CountByTagID(obj.ID)
return &count, err
}
func (r *tagResolver) ImageCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = image.CountByTagID(repo.Image(), obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}
func (r *tagResolver) GalleryCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
var res int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
res, err = gallery.CountByTagID(repo.Gallery(), obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}
func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
var count int
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
count, err = repo.Performer().CountByTagID(obj.ID)
return err
}); err != nil {
return nil, err
}
return &count, err
}
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj.ID).GetTagImageURL()
imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj).GetTagImageURL()
return &imagePath, nil
}

View File

@@ -13,15 +13,37 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
func (r *mutationResolver) Setup(ctx context.Context, input models.SetupInput) (bool, error) {
err := manager.GetInstance().Setup(input)
return err == nil, err
}
func (r *mutationResolver) Migrate(ctx context.Context, input models.MigrateInput) (bool, error) {
err := manager.GetInstance().Migrate(input)
return err == nil, err
}
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.ConfigGeneralInput) (*models.ConfigGeneralResult, error) {
c := config.GetInstance()
existingPaths := c.GetStashPaths()
if len(input.Stashes) > 0 {
for _, s := range input.Stashes {
exists, err := utils.DirExists(s.Path)
if !exists {
return makeConfigGeneralResult(), err
// Only validate existence of new paths
isNew := true
for _, path := range existingPaths {
if path.Path == s.Path {
isNew = false
break
}
}
if isNew {
exists, err := utils.DirExists(s.Path)
if !exists {
return makeConfigGeneralResult(), err
}
}
}
config.Set(config.Stash, input.Stashes)
c.Set(config.Stash, input.Stashes)
}
if input.DatabasePath != nil {
@@ -29,133 +51,140 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
if ext != ".db" && ext != ".sqlite" && ext != ".sqlite3" {
return makeConfigGeneralResult(), fmt.Errorf("invalid database path, use extension db, sqlite, or sqlite3")
}
config.Set(config.Database, input.DatabasePath)
c.Set(config.Database, input.DatabasePath)
}
if input.GeneratedPath != nil {
if err := utils.EnsureDir(*input.GeneratedPath); err != nil {
return makeConfigGeneralResult(), err
}
config.Set(config.Generated, input.GeneratedPath)
c.Set(config.Generated, input.GeneratedPath)
}
if input.CachePath != nil {
if err := utils.EnsureDir(*input.CachePath); err != nil {
return makeConfigGeneralResult(), err
if *input.CachePath != "" {
if err := utils.EnsureDir(*input.CachePath); err != nil {
return makeConfigGeneralResult(), err
}
}
config.Set(config.Cache, input.CachePath)
c.Set(config.Cache, input.CachePath)
}
if !input.CalculateMd5 && input.VideoFileNamingAlgorithm == models.HashAlgorithmMd5 {
return makeConfigGeneralResult(), errors.New("calculateMD5 must be true if using MD5")
}
if input.VideoFileNamingAlgorithm != config.GetVideoFileNamingAlgorithm() {
if input.VideoFileNamingAlgorithm != c.GetVideoFileNamingAlgorithm() {
// validate changing VideoFileNamingAlgorithm
if err := manager.ValidateVideoFileNamingAlgorithm(input.VideoFileNamingAlgorithm); err != nil {
if err := manager.ValidateVideoFileNamingAlgorithm(r.txnManager, input.VideoFileNamingAlgorithm); err != nil {
return makeConfigGeneralResult(), err
}
config.Set(config.VideoFileNamingAlgorithm, input.VideoFileNamingAlgorithm)
c.Set(config.VideoFileNamingAlgorithm, input.VideoFileNamingAlgorithm)
}
config.Set(config.CalculateMD5, input.CalculateMd5)
c.Set(config.CalculateMD5, input.CalculateMd5)
if input.ParallelTasks != nil {
c.Set(config.ParallelTasks, *input.ParallelTasks)
}
if input.PreviewSegments != nil {
config.Set(config.PreviewSegments, *input.PreviewSegments)
c.Set(config.PreviewSegments, *input.PreviewSegments)
}
if input.PreviewSegmentDuration != nil {
config.Set(config.PreviewSegmentDuration, *input.PreviewSegmentDuration)
c.Set(config.PreviewSegmentDuration, *input.PreviewSegmentDuration)
}
if input.PreviewExcludeStart != nil {
config.Set(config.PreviewExcludeStart, *input.PreviewExcludeStart)
c.Set(config.PreviewExcludeStart, *input.PreviewExcludeStart)
}
if input.PreviewExcludeEnd != nil {
config.Set(config.PreviewExcludeEnd, *input.PreviewExcludeEnd)
c.Set(config.PreviewExcludeEnd, *input.PreviewExcludeEnd)
}
if input.PreviewPreset != nil {
config.Set(config.PreviewPreset, input.PreviewPreset.String())
c.Set(config.PreviewPreset, input.PreviewPreset.String())
}
if input.MaxTranscodeSize != nil {
config.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
c.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
}
if input.MaxStreamingTranscodeSize != nil {
config.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
c.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
}
if input.Username != nil {
config.Set(config.Username, input.Username)
c.Set(config.Username, input.Username)
}
if input.Password != nil {
// bit of a hack - check if the passed in password is the same as the stored hash
// and only set if they are different
currentPWHash := config.GetPasswordHash()
currentPWHash := c.GetPasswordHash()
if *input.Password != currentPWHash {
config.SetPassword(*input.Password)
c.SetPassword(*input.Password)
}
}
if input.MaxSessionAge != nil {
config.Set(config.MaxSessionAge, *input.MaxSessionAge)
c.Set(config.MaxSessionAge, *input.MaxSessionAge)
}
if input.LogFile != nil {
config.Set(config.LogFile, input.LogFile)
c.Set(config.LogFile, input.LogFile)
}
config.Set(config.LogOut, input.LogOut)
config.Set(config.LogAccess, input.LogAccess)
c.Set(config.LogOut, input.LogOut)
c.Set(config.LogAccess, input.LogAccess)
if input.LogLevel != config.GetLogLevel() {
config.Set(config.LogLevel, input.LogLevel)
if input.LogLevel != c.GetLogLevel() {
c.Set(config.LogLevel, input.LogLevel)
logger.SetLogLevel(input.LogLevel)
}
if input.Excludes != nil {
config.Set(config.Exclude, input.Excludes)
c.Set(config.Exclude, input.Excludes)
}
if input.ImageExcludes != nil {
config.Set(config.ImageExclude, input.ImageExcludes)
c.Set(config.ImageExclude, input.ImageExcludes)
}
if input.VideoExtensions != nil {
config.Set(config.VideoExtensions, input.VideoExtensions)
c.Set(config.VideoExtensions, input.VideoExtensions)
}
if input.ImageExtensions != nil {
config.Set(config.ImageExtensions, input.ImageExtensions)
c.Set(config.ImageExtensions, input.ImageExtensions)
}
if input.GalleryExtensions != nil {
config.Set(config.GalleryExtensions, input.GalleryExtensions)
c.Set(config.GalleryExtensions, input.GalleryExtensions)
}
config.Set(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
c.Set(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
refreshScraperCache := false
if input.ScraperUserAgent != nil {
config.Set(config.ScraperUserAgent, input.ScraperUserAgent)
c.Set(config.ScraperUserAgent, input.ScraperUserAgent)
refreshScraperCache = true
}
if input.ScraperCDPPath != nil {
config.Set(config.ScraperCDPPath, input.ScraperCDPPath)
c.Set(config.ScraperCDPPath, input.ScraperCDPPath)
refreshScraperCache = true
}
c.Set(config.ScraperCertCheck, input.ScraperCertCheck)
if input.StashBoxes != nil {
if err := config.ValidateStashBoxes(input.StashBoxes); err != nil {
if err := c.ValidateStashBoxes(input.StashBoxes); err != nil {
return nil, err
}
config.Set(config.StashBoxes, input.StashBoxes)
c.Set(config.StashBoxes, input.StashBoxes)
}
if err := config.Write(); err != nil {
if err := c.Write(); err != nil {
return makeConfigGeneralResult(), err
}
@@ -168,32 +197,41 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
}
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) {
c := config.GetInstance()
if input.MenuItems != nil {
c.Set(config.MenuItems, input.MenuItems)
}
if input.SoundOnPreview != nil {
config.Set(config.SoundOnPreview, *input.SoundOnPreview)
c.Set(config.SoundOnPreview, *input.SoundOnPreview)
}
if input.WallShowTitle != nil {
config.Set(config.WallShowTitle, *input.WallShowTitle)
c.Set(config.WallShowTitle, *input.WallShowTitle)
}
if input.WallPlayback != nil {
config.Set(config.WallPlayback, *input.WallPlayback)
c.Set(config.WallPlayback, *input.WallPlayback)
}
if input.MaximumLoopDuration != nil {
config.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
c.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
}
if input.AutostartVideo != nil {
config.Set(config.AutostartVideo, *input.AutostartVideo)
c.Set(config.AutostartVideo, *input.AutostartVideo)
}
if input.ShowStudioAsText != nil {
config.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
c.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
}
if input.Language != nil {
config.Set(config.Language, *input.Language)
c.Set(config.Language, *input.Language)
}
if input.SlideshowDelay != nil {
c.Set(config.SlideshowDelay, *input.SlideshowDelay)
}
css := ""
@@ -202,15 +240,38 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
css = *input.CSS
}
config.SetCSS(css)
c.SetCSS(css)
if input.CSSEnabled != nil {
config.Set(config.CSSEnabled, *input.CSSEnabled)
c.Set(config.CSSEnabled, *input.CSSEnabled)
}
if err := config.Write(); err != nil {
if err := c.Write(); err != nil {
return makeConfigInterfaceResult(), err
}
return makeConfigInterfaceResult(), nil
}
func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input models.GenerateAPIKeyInput) (string, error) {
c := config.GetInstance()
var newAPIKey string
if input.Clear == nil || !*input.Clear {
username := c.GetUsername()
if username != "" {
var err error
newAPIKey, err = manager.GenerateAPIKey(username)
if err != nil {
return "", err
}
}
}
c.Set(config.ApiKey, newAPIKey)
if err := c.Write(); err != nil {
return newAPIKey, err
}
return newAPIKey, nil
}

View File

@@ -4,11 +4,10 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"time"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
@@ -61,108 +60,115 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.Galle
newGallery.StudioID = sql.NullInt64{Valid: false}
}
if input.SceneID != nil {
sceneID, _ := strconv.ParseInt(*input.SceneID, 10, 64)
newGallery.SceneID = sql.NullInt64{Int64: sceneID, Valid: true}
} else {
// studio must be nullable
newGallery.SceneID = sql.NullInt64{Valid: false}
}
// Start the transaction and save the performer
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewGalleryQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
gallery, err := qb.Create(newGallery, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
// Save the performers
var performerJoins []models.PerformersGalleries
for _, pid := range input.PerformerIds {
performerID, _ := strconv.Atoi(pid)
performerJoin := models.PerformersGalleries{
PerformerID: performerID,
GalleryID: gallery.ID,
// Start the transaction and save the gallery
var gallery *models.Gallery
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Gallery()
var err error
gallery, err = qb.Create(newGallery)
if err != nil {
return err
}
performerJoins = append(performerJoins, performerJoin)
}
if err := jqb.UpdatePerformersGalleries(gallery.ID, performerJoins, tx); err != nil {
return nil, err
}
// Save the tags
var tagJoins []models.GalleriesTags
for _, tid := range input.TagIds {
tagID, _ := strconv.Atoi(tid)
tagJoin := models.GalleriesTags{
GalleryID: gallery.ID,
TagID: tagID,
// Save the performers
if err := r.updateGalleryPerformers(qb, gallery.ID, input.PerformerIds); err != nil {
return err
}
tagJoins = append(tagJoins, tagJoin)
}
if err := jqb.UpdateGalleriesTags(gallery.ID, tagJoins, tx); err != nil {
return nil, err
}
// Commit
if err := tx.Commit(); err != nil {
// Save the tags
if err := r.updateGalleryTags(qb, gallery.ID, input.TagIds); err != nil {
return err
}
// Save the scenes
if err := r.updateGalleryScenes(qb, gallery.ID, input.SceneIds); err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
return gallery, nil
}
func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.GalleryUpdateInput) (*models.Gallery, error) {
// Start the transaction and save the gallery
tx := database.DB.MustBeginTx(ctx, nil)
ret, err := r.galleryUpdate(input, tx)
func (r *mutationResolver) updateGalleryPerformers(qb models.GalleryReaderWriter, galleryID int, performerIDs []string) error {
ids, err := utils.StringSliceToIntSlice(performerIDs)
if err != nil {
_ = tx.Rollback()
return nil, err
return err
}
// Commit
if err := tx.Commit(); err != nil {
return nil, err
}
return ret, nil
return qb.UpdatePerformers(galleryID, ids)
}
func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.GalleryUpdateInput) ([]*models.Gallery, error) {
func (r *mutationResolver) updateGalleryTags(qb models.GalleryReaderWriter, galleryID int, tagIDs []string) error {
ids, err := utils.StringSliceToIntSlice(tagIDs)
if err != nil {
return err
}
return qb.UpdateTags(galleryID, ids)
}
func (r *mutationResolver) updateGalleryScenes(qb models.GalleryReaderWriter, galleryID int, sceneIDs []string) error {
ids, err := utils.StringSliceToIntSlice(sceneIDs)
if err != nil {
return err
}
return qb.UpdateScenes(galleryID, ids)
}
func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.GalleryUpdateInput) (ret *models.Gallery, err error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Start the transaction and save the gallery
tx := database.DB.MustBeginTx(ctx, nil)
var ret []*models.Gallery
for _, gallery := range input {
thisGallery, err := r.galleryUpdate(*gallery, tx)
ret = append(ret, thisGallery)
if err != nil {
_ = tx.Rollback()
return nil, err
}
}
// Commit
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
ret, err = r.galleryUpdate(input, translator, repo)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, tx *sqlx.Tx) (*models.Gallery, error) {
qb := models.NewGalleryQueryBuilder()
func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.GalleryUpdateInput) (ret []*models.Gallery, err error) {
inputMaps := getUpdateInputMaps(ctx)
// Start the transaction and save the gallery
if err := r.withTxn(ctx, func(repo models.Repository) error {
for i, gallery := range input {
translator := changesetTranslator{
inputMap: inputMaps[i],
}
thisGallery, err := r.galleryUpdate(*gallery, translator, repo)
if err != nil {
return err
}
ret = append(ret, thisGallery)
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Gallery, error) {
qb := repo.Gallery()
// Populate gallery from the input
galleryID, _ := strconv.Atoi(input.ID)
originalGallery, err := qb.Find(galleryID, nil)
galleryID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
originalGallery, err := qb.Find(galleryID)
if err != nil {
return nil, err
}
@@ -176,6 +182,7 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, tx *sq
ID: galleryID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
if input.Title != nil {
// ensure title is not empty
if *input.Title == "" {
@@ -190,65 +197,40 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, tx *sq
updatedGallery.Title = &sql.NullString{String: *input.Title, Valid: true}
}
if input.Details != nil {
updatedGallery.Details = &sql.NullString{String: *input.Details, Valid: true}
}
if input.URL != nil {
updatedGallery.URL = &sql.NullString{String: *input.URL, Valid: true}
}
if input.Date != nil {
updatedGallery.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
}
if input.Rating != nil {
updatedGallery.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
// rating must be nullable
updatedGallery.Rating = &sql.NullInt64{Valid: false}
}
if input.StudioID != nil {
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
updatedGallery.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
} else {
// studio must be nullable
updatedGallery.StudioID = &sql.NullInt64{Valid: false}
}
updatedGallery.Details = translator.nullString(input.Details, "details")
updatedGallery.URL = translator.nullString(input.URL, "url")
updatedGallery.Date = translator.sqliteDate(input.Date, "date")
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedGallery.Organized = input.Organized
// gallery scene is set from the scene only
jqb := models.NewJoinsQueryBuilder()
gallery, err := qb.UpdatePartial(updatedGallery, tx)
gallery, err := qb.UpdatePartial(updatedGallery)
if err != nil {
return nil, err
}
// Save the performers
var performerJoins []models.PerformersGalleries
for _, pid := range input.PerformerIds {
performerID, _ := strconv.Atoi(pid)
performerJoin := models.PerformersGalleries{
PerformerID: performerID,
GalleryID: galleryID,
if translator.hasField("performer_ids") {
if err := r.updateGalleryPerformers(qb, galleryID, input.PerformerIds); err != nil {
return nil, err
}
performerJoins = append(performerJoins, performerJoin)
}
if err := jqb.UpdatePerformersGalleries(galleryID, performerJoins, tx); err != nil {
return nil, err
}
// Save the tags
var tagJoins []models.GalleriesTags
for _, tid := range input.TagIds {
tagID, _ := strconv.Atoi(tid)
tagJoin := models.GalleriesTags{
GalleryID: galleryID,
TagID: tagID,
if translator.hasField("tag_ids") {
if err := r.updateGalleryTags(qb, galleryID, input.TagIds); err != nil {
return nil, err
}
tagJoins = append(tagJoins, tagJoin)
}
if err := jqb.UpdateGalleriesTags(galleryID, tagJoins, tx); err != nil {
return nil, err
// Save the scenes
if translator.hasField("scene_ids") {
if err := r.updateGalleryScenes(qb, galleryID, input.SceneIds); err != nil {
return nil, err
}
}
return gallery, nil
@@ -258,227 +240,181 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.B
// Populate gallery from the input
updatedTime := time.Now()
// Start the transaction and save the gallery marker
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewGalleryQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedGallery := models.GalleryPartial{
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
if input.Details != nil {
updatedGallery.Details = &sql.NullString{String: *input.Details, Valid: true}
}
if input.URL != nil {
updatedGallery.URL = &sql.NullString{String: *input.URL, Valid: true}
}
if input.Date != nil {
updatedGallery.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
}
if input.Rating != nil {
// a rating of 0 means unset the rating
if *input.Rating == 0 {
updatedGallery.Rating = &sql.NullInt64{Int64: 0, Valid: false}
} else {
updatedGallery.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
}
}
if input.StudioID != nil {
// empty string means unset the studio
if *input.StudioID == "" {
updatedGallery.StudioID = &sql.NullInt64{Int64: 0, Valid: false}
} else {
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
updatedGallery.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
}
}
if input.SceneID != nil {
// empty string means unset the studio
if *input.SceneID == "" {
updatedGallery.SceneID = &sql.NullInt64{Int64: 0, Valid: false}
} else {
sceneID, _ := strconv.ParseInt(*input.SceneID, 10, 64)
updatedGallery.SceneID = &sql.NullInt64{Int64: sceneID, Valid: true}
}
}
updatedGallery.Details = translator.nullString(input.Details, "details")
updatedGallery.URL = translator.nullString(input.URL, "url")
updatedGallery.Date = translator.sqliteDate(input.Date, "date")
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedGallery.Organized = input.Organized
ret := []*models.Gallery{}
for _, galleryIDStr := range input.Ids {
galleryID, _ := strconv.Atoi(galleryIDStr)
updatedGallery.ID = galleryID
// Start the transaction and save the galleries
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Gallery()
gallery, err := qb.UpdatePartial(updatedGallery, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
for _, galleryIDStr := range input.Ids {
galleryID, _ := strconv.Atoi(galleryIDStr)
updatedGallery.ID = galleryID
ret = append(ret, gallery)
// Save the performers
if wasFieldIncluded(ctx, "performer_ids") {
performerIDs, err := adjustGalleryPerformerIDs(tx, galleryID, *input.PerformerIds)
gallery, err := qb.UpdatePartial(updatedGallery)
if err != nil {
_ = tx.Rollback()
return nil, err
return err
}
var performerJoins []models.PerformersGalleries
for _, performerID := range performerIDs {
performerJoin := models.PerformersGalleries{
PerformerID: performerID,
GalleryID: galleryID,
ret = append(ret, gallery)
// Save the performers
if translator.hasField("performer_ids") {
performerIDs, err := adjustGalleryPerformerIDs(qb, galleryID, *input.PerformerIds)
if err != nil {
return err
}
if err := qb.UpdatePerformers(galleryID, performerIDs); err != nil {
return err
}
performerJoins = append(performerJoins, performerJoin)
}
if err := jqb.UpdatePerformersGalleries(galleryID, performerJoins, tx); err != nil {
_ = tx.Rollback()
return nil, err
// Save the tags
if translator.hasField("tag_ids") {
tagIDs, err := adjustGalleryTagIDs(qb, galleryID, *input.TagIds)
if err != nil {
return err
}
if err := qb.UpdateTags(galleryID, tagIDs); err != nil {
return err
}
}
// Save the scenes
if translator.hasField("scene_ids") {
sceneIDs, err := adjustGallerySceneIDs(qb, galleryID, *input.SceneIds)
if err != nil {
return err
}
if err := qb.UpdateScenes(galleryID, sceneIDs); err != nil {
return err
}
}
}
// Save the tags
if wasFieldIncluded(ctx, "tag_ids") {
tagIDs, err := adjustGalleryTagIDs(tx, galleryID, *input.TagIds)
if err != nil {
_ = tx.Rollback()
return nil, err
}
var tagJoins []models.GalleriesTags
for _, tagID := range tagIDs {
tagJoin := models.GalleriesTags{
GalleryID: galleryID,
TagID: tagID,
}
tagJoins = append(tagJoins, tagJoin)
}
if err := jqb.UpdateGalleriesTags(galleryID, tagJoins, tx); err != nil {
_ = tx.Rollback()
return nil, err
}
}
}
// Commit
if err := tx.Commit(); err != nil {
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func adjustGalleryPerformerIDs(tx *sqlx.Tx, galleryID int, ids models.BulkUpdateIds) ([]int, error) {
var ret []int
jqb := models.NewJoinsQueryBuilder()
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
// adding to the joins
performerJoins, err := jqb.GetGalleryPerformers(galleryID, tx)
if err != nil {
return nil, err
}
for _, join := range performerJoins {
ret = append(ret, join.PerformerID)
}
func adjustGalleryPerformerIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetPerformerIDs(galleryID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func adjustGalleryTagIDs(tx *sqlx.Tx, galleryID int, ids models.BulkUpdateIds) ([]int, error) {
var ret []int
func adjustGalleryTagIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetTagIDs(galleryID)
if err != nil {
return nil, err
}
jqb := models.NewJoinsQueryBuilder()
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
// adding to the joins
tagJoins, err := jqb.GetGalleryTags(galleryID, tx)
return adjustIDs(ret, ids), nil
}
if err != nil {
return nil, err
}
for _, join := range tagJoins {
ret = append(ret, join.TagID)
}
func adjustGallerySceneIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetSceneIDs(galleryID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.GalleryDestroyInput) (bool, error) {
qb := models.NewGalleryQueryBuilder()
iqb := models.NewImageQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
galleryIDs, err := utils.StringSliceToIntSlice(input.Ids)
if err != nil {
return false, err
}
var galleries []*models.Gallery
var imgsToPostProcess []*models.Image
var imgsToDelete []*models.Image
for _, id := range input.Ids {
galleryID, _ := strconv.Atoi(id)
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Gallery()
iqb := repo.Image()
for _, id := range galleryIDs {
gallery, err := qb.Find(id)
if err != nil {
return err
}
if gallery == nil {
return fmt.Errorf("gallery with id %d not found", id)
}
gallery, err := qb.Find(galleryID, tx)
if gallery != nil {
galleries = append(galleries, gallery)
}
err = qb.Destroy(galleryID, tx)
if err != nil {
tx.Rollback()
return false, err
}
// if this is a zip-based gallery, delete the images as well
if gallery.Zip {
imgs, err := iqb.FindByGalleryID(galleryID)
if err != nil {
tx.Rollback()
return false, err
}
for _, img := range imgs {
err = iqb.Destroy(img.ID, tx)
// if this is a zip-based gallery, delete the images as well first
if gallery.Zip {
imgs, err := iqb.FindByGalleryID(id)
if err != nil {
tx.Rollback()
return false, err
return err
}
imgsToPostProcess = append(imgsToPostProcess, img)
}
} else if input.DeleteFile != nil && *input.DeleteFile {
// Delete image if it is only attached to this gallery
imgs, err := iqb.FindByGalleryID(galleryID)
if err != nil {
tx.Rollback()
return false, err
}
for _, img := range imgs {
imgGalleries, err := qb.FindByImageID(img.ID, tx)
if err != nil {
tx.Rollback()
return false, err
}
if len(imgGalleries) == 0 {
err = iqb.Destroy(img.ID, tx)
if err != nil {
tx.Rollback()
return false, err
for _, img := range imgs {
if err := iqb.Destroy(img.ID); err != nil {
return err
}
imgsToDelete = append(imgsToDelete, img)
imgsToPostProcess = append(imgsToPostProcess, img)
}
} else if input.DeleteFile != nil && *input.DeleteFile {
// Delete image if it is only attached to this gallery
imgs, err := iqb.FindByGalleryID(id)
if err != nil {
return err
}
for _, img := range imgs {
imgGalleries, err := qb.FindByImageID(img.ID)
if err != nil {
return err
}
if len(imgGalleries) == 0 {
if err := iqb.Destroy(img.ID); err != nil {
return err
}
imgsToDelete = append(imgsToDelete, img)
imgsToPostProcess = append(imgsToPostProcess, img)
}
}
}
if err := qb.Destroy(id); err != nil {
return err
}
}
}
if err := tx.Commit(); err != nil {
return nil
}); err != nil {
return false, err
}
@@ -506,34 +442,39 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
}
func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.GalleryAddInput) (bool, error) {
galleryID, _ := strconv.Atoi(input.GalleryID)
qb := models.NewGalleryQueryBuilder()
gallery, err := qb.Find(galleryID, nil)
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return false, err
}
if gallery == nil {
return false, errors.New("gallery not found")
imageIDs, err := utils.StringSliceToIntSlice(input.ImageIds)
if err != nil {
return false, err
}
if gallery.Zip {
return false, errors.New("cannot modify zip gallery images")
}
jqb := models.NewJoinsQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
for _, id := range input.ImageIds {
imageID, _ := strconv.Atoi(id)
_, err := jqb.AddImageGallery(imageID, galleryID, tx)
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Gallery()
gallery, err := qb.Find(galleryID)
if err != nil {
tx.Rollback()
return false, err
return err
}
}
if err := tx.Commit(); err != nil {
if gallery == nil {
return errors.New("gallery not found")
}
if gallery.Zip {
return errors.New("cannot modify zip gallery images")
}
newIDs, err := qb.GetImageIDs(galleryID)
if err != nil {
return err
}
newIDs = utils.IntAppendUniques(newIDs, imageIDs)
return qb.UpdateImages(galleryID, newIDs)
}); err != nil {
return false, err
}
@@ -541,34 +482,39 @@ func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.Ga
}
func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input models.GalleryRemoveInput) (bool, error) {
galleryID, _ := strconv.Atoi(input.GalleryID)
qb := models.NewGalleryQueryBuilder()
gallery, err := qb.Find(galleryID, nil)
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return false, err
}
if gallery == nil {
return false, errors.New("gallery not found")
imageIDs, err := utils.StringSliceToIntSlice(input.ImageIds)
if err != nil {
return false, err
}
if gallery.Zip {
return false, errors.New("cannot modify zip gallery images")
}
jqb := models.NewJoinsQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
for _, id := range input.ImageIds {
imageID, _ := strconv.Atoi(id)
_, err := jqb.RemoveImageGallery(imageID, galleryID, tx)
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Gallery()
gallery, err := qb.Find(galleryID)
if err != nil {
tx.Rollback()
return false, err
return err
}
}
if err := tx.Commit(); err != nil {
if gallery == nil {
return errors.New("gallery not found")
}
if gallery.Zip {
return errors.New("cannot modify zip gallery images")
}
newIDs, err := qb.GetImageIDs(galleryID)
if err != nil {
return err
}
newIDs = utils.IntExclude(newIDs, imageIDs)
return qb.UpdateImages(galleryID, newIDs)
}); err != nil {
return false, err
}

View File

@@ -2,323 +2,257 @@ package api
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *mutationResolver) ImageUpdate(ctx context.Context, input models.ImageUpdateInput) (*models.Image, error) {
// Start the transaction and save the image
tx := database.DB.MustBeginTx(ctx, nil)
ret, err := r.imageUpdate(input, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
func (r *mutationResolver) ImageUpdate(ctx context.Context, input models.ImageUpdateInput) (ret *models.Image, err error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Commit
if err := tx.Commit(); err != nil {
// Start the transaction and save the image
if err := r.withTxn(ctx, func(repo models.Repository) error {
ret, err = r.imageUpdate(input, translator, repo)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*models.ImageUpdateInput) ([]*models.Image, 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
tx := database.DB.MustBeginTx(ctx, nil)
if err := r.withTxn(ctx, func(repo models.Repository) error {
for i, image := range input {
translator := changesetTranslator{
inputMap: inputMaps[i],
}
var ret []*models.Image
thisImage, err := r.imageUpdate(*image, translator, repo)
if err != nil {
return err
}
for _, image := range input {
thisImage, err := r.imageUpdate(*image, tx)
ret = append(ret, thisImage)
if err != nil {
_ = tx.Rollback()
return nil, err
ret = append(ret, thisImage)
}
}
// Commit
if err := tx.Commit(); err != nil {
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) imageUpdate(input models.ImageUpdateInput, tx *sqlx.Tx) (*models.Image, error) {
func (r *mutationResolver) imageUpdate(input models.ImageUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Image, error) {
// Populate image from the input
imageID, _ := strconv.Atoi(input.ID)
imageID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
updatedTime := time.Now()
updatedImage := models.ImagePartial{
ID: imageID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
if input.Title != nil {
updatedImage.Title = &sql.NullString{String: *input.Title, Valid: true}
}
if input.Rating != nil {
updatedImage.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
// rating must be nullable
updatedImage.Rating = &sql.NullInt64{Valid: false}
}
updatedImage.Title = translator.nullString(input.Title, "title")
updatedImage.Rating = translator.nullInt64(input.Rating, "rating")
updatedImage.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedImage.Organized = input.Organized
if input.StudioID != nil {
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
updatedImage.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
} else {
// studio must be nullable
updatedImage.StudioID = &sql.NullInt64{Valid: false}
}
qb := models.NewImageQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
image, err := qb.Update(updatedImage, tx)
qb := repo.Image()
image, err := qb.Update(updatedImage)
if err != nil {
return nil, err
}
// don't set the galleries directly. Use add/remove gallery images interface instead
if translator.hasField("gallery_ids") {
if err := r.updateImageGalleries(qb, imageID, input.GalleryIds); err != nil {
return nil, err
}
}
// Save the performers
var performerJoins []models.PerformersImages
for _, pid := range input.PerformerIds {
performerID, _ := strconv.Atoi(pid)
performerJoin := models.PerformersImages{
PerformerID: performerID,
ImageID: imageID,
if translator.hasField("performer_ids") {
if err := r.updateImagePerformers(qb, imageID, input.PerformerIds); err != nil {
return nil, err
}
performerJoins = append(performerJoins, performerJoin)
}
if err := jqb.UpdatePerformersImages(imageID, performerJoins, tx); err != nil {
return nil, err
}
// Save the tags
var tagJoins []models.ImagesTags
for _, tid := range input.TagIds {
tagID, _ := strconv.Atoi(tid)
tagJoin := models.ImagesTags{
ImageID: imageID,
TagID: tagID,
if translator.hasField("tag_ids") {
if err := r.updateImageTags(qb, imageID, input.TagIds); err != nil {
return nil, err
}
tagJoins = append(tagJoins, tagJoin)
}
if err := jqb.UpdateImagesTags(imageID, tagJoins, tx); err != nil {
return nil, err
}
return image, nil
}
func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input models.BulkImageUpdateInput) ([]*models.Image, error) {
func (r *mutationResolver) updateImageGalleries(qb models.ImageReaderWriter, imageID int, galleryIDs []string) error {
ids, err := utils.StringSliceToIntSlice(galleryIDs)
if err != nil {
return err
}
return qb.UpdateGalleries(imageID, ids)
}
func (r *mutationResolver) updateImagePerformers(qb models.ImageReaderWriter, imageID int, performerIDs []string) error {
ids, err := utils.StringSliceToIntSlice(performerIDs)
if err != nil {
return err
}
return qb.UpdatePerformers(imageID, ids)
}
func (r *mutationResolver) updateImageTags(qb models.ImageReaderWriter, imageID int, tagsIDs []string) error {
ids, err := utils.StringSliceToIntSlice(tagsIDs)
if err != nil {
return err
}
return qb.UpdateTags(imageID, ids)
}
func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input models.BulkImageUpdateInput) (ret []*models.Image, err error) {
imageIDs, err := utils.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, err
}
// Populate image from the input
updatedTime := time.Now()
// Start the transaction and save the image marker
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewImageQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
updatedImage := models.ImagePartial{
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
if input.Title != nil {
updatedImage.Title = &sql.NullString{String: *input.Title, Valid: true}
}
if input.Rating != nil {
// a rating of 0 means unset the rating
if *input.Rating == 0 {
updatedImage.Rating = &sql.NullInt64{Int64: 0, Valid: false}
} else {
updatedImage.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
}
}
if input.StudioID != nil {
// empty string means unset the studio
if *input.StudioID == "" {
updatedImage.StudioID = &sql.NullInt64{Int64: 0, Valid: false}
} else {
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
updatedImage.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
ret := []*models.Image{}
updatedImage.Title = translator.nullString(input.Title, "title")
updatedImage.Rating = translator.nullInt64(input.Rating, "rating")
updatedImage.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedImage.Organized = input.Organized
for _, imageIDStr := range input.Ids {
imageID, _ := strconv.Atoi(imageIDStr)
updatedImage.ID = imageID
// Start the transaction and save the image marker
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
image, err := qb.Update(updatedImage, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
for _, imageID := range imageIDs {
updatedImage.ID = imageID
ret = append(ret, image)
// Save the galleries
if wasFieldIncluded(ctx, "gallery_ids") {
galleryIDs, err := adjustImageGalleryIDs(tx, imageID, *input.GalleryIds)
image, err := qb.Update(updatedImage)
if err != nil {
_ = tx.Rollback()
return nil, err
return err
}
var galleryJoins []models.GalleriesImages
for _, gid := range galleryIDs {
galleryJoin := models.GalleriesImages{
GalleryID: gid,
ImageID: imageID,
ret = append(ret, image)
// Save the galleries
if translator.hasField("gallery_ids") {
galleryIDs, err := adjustImageGalleryIDs(qb, imageID, *input.GalleryIds)
if err != nil {
return err
}
if err := qb.UpdateGalleries(imageID, galleryIDs); err != nil {
return err
}
galleryJoins = append(galleryJoins, galleryJoin)
}
if err := jqb.UpdateGalleriesImages(imageID, galleryJoins, tx); err != nil {
return nil, err
// Save the performers
if translator.hasField("performer_ids") {
performerIDs, err := adjustImagePerformerIDs(qb, imageID, *input.PerformerIds)
if err != nil {
return err
}
if err := qb.UpdatePerformers(imageID, performerIDs); err != nil {
return err
}
}
// Save the tags
if translator.hasField("tag_ids") {
tagIDs, err := adjustImageTagIDs(qb, imageID, *input.TagIds)
if err != nil {
return err
}
if err := qb.UpdateTags(imageID, tagIDs); err != nil {
return err
}
}
}
// Save the performers
if wasFieldIncluded(ctx, "performer_ids") {
performerIDs, err := adjustImagePerformerIDs(tx, imageID, *input.PerformerIds)
if err != nil {
_ = tx.Rollback()
return nil, err
}
var performerJoins []models.PerformersImages
for _, performerID := range performerIDs {
performerJoin := models.PerformersImages{
PerformerID: performerID,
ImageID: imageID,
}
performerJoins = append(performerJoins, performerJoin)
}
if err := jqb.UpdatePerformersImages(imageID, performerJoins, tx); err != nil {
_ = tx.Rollback()
return nil, err
}
}
// Save the tags
if wasFieldIncluded(ctx, "tag_ids") {
tagIDs, err := adjustImageTagIDs(tx, imageID, *input.TagIds)
if err != nil {
_ = tx.Rollback()
return nil, err
}
var tagJoins []models.ImagesTags
for _, tagID := range tagIDs {
tagJoin := models.ImagesTags{
ImageID: imageID,
TagID: tagID,
}
tagJoins = append(tagJoins, tagJoin)
}
if err := jqb.UpdateImagesTags(imageID, tagJoins, tx); err != nil {
_ = tx.Rollback()
return nil, err
}
}
}
// Commit
if err := tx.Commit(); err != nil {
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func adjustImageGalleryIDs(tx *sqlx.Tx, imageID int, ids models.BulkUpdateIds) ([]int, error) {
var ret []int
jqb := models.NewJoinsQueryBuilder()
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
// adding to the joins
galleryJoins, err := jqb.GetImageGalleries(imageID, tx)
if err != nil {
return nil, err
}
for _, join := range galleryJoins {
ret = append(ret, join.GalleryID)
}
}
return adjustIDs(ret, ids), nil
}
func adjustImagePerformerIDs(tx *sqlx.Tx, imageID int, ids models.BulkUpdateIds) ([]int, error) {
var ret []int
jqb := models.NewJoinsQueryBuilder()
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
// adding to the joins
performerJoins, err := jqb.GetImagePerformers(imageID, tx)
if err != nil {
return nil, err
}
for _, join := range performerJoins {
ret = append(ret, join.PerformerID)
}
}
return adjustIDs(ret, ids), nil
}
func adjustImageTagIDs(tx *sqlx.Tx, imageID int, ids models.BulkUpdateIds) ([]int, error) {
var ret []int
jqb := models.NewJoinsQueryBuilder()
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
// adding to the joins
tagJoins, err := jqb.GetImageTags(imageID, tx)
if err != nil {
return nil, err
}
for _, join := range tagJoins {
ret = append(ret, join.TagID)
}
}
return adjustIDs(ret, ids), nil
}
func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageDestroyInput) (bool, error) {
qb := models.NewImageQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
imageID, _ := strconv.Atoi(input.ID)
image, err := qb.Find(imageID)
err = qb.Destroy(imageID, tx)
func adjustImageGalleryIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetGalleryIDs(imageID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func adjustImagePerformerIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetPerformerIDs(imageID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func adjustImageTagIDs(qb models.ImageReader, imageID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetTagIDs(imageID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageDestroyInput) (ret bool, err error) {
imageID, err := strconv.Atoi(input.ID)
if err != nil {
tx.Rollback()
return false, err
}
if err := tx.Commit(); err != nil {
var image *models.Image
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
image, err = qb.Find(imageID)
if err != nil {
return err
}
if image == nil {
return fmt.Errorf("image with id %d not found", imageID)
}
return qb.Destroy(imageID)
}); err != nil {
return false, err
}
@@ -337,27 +271,35 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD
return true, nil
}
func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.ImagesDestroyInput) (bool, error) {
qb := models.NewImageQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
var images []*models.Image
for _, id := range input.Ids {
imageID, _ := strconv.Atoi(id)
image, err := qb.Find(imageID)
if image != nil {
images = append(images, image)
}
err = qb.Destroy(imageID, tx)
if err != nil {
tx.Rollback()
return false, err
}
func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.ImagesDestroyInput) (ret bool, err error) {
imageIDs, err := utils.StringSliceToIntSlice(input.Ids)
if err != nil {
return false, err
}
if err := tx.Commit(); err != nil {
var images []*models.Image
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
for _, imageID := range imageIDs {
image, err := qb.Find(imageID)
if err != nil {
return err
}
if image == nil {
return fmt.Errorf("image with id %d not found", imageID)
}
images = append(images, image)
if err := qb.Destroy(imageID); err != nil {
return err
}
}
return nil
}); err != nil {
return false, err
}
@@ -378,62 +320,56 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image
return true, nil
}
func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (int, error) {
imageID, _ := strconv.Atoi(id)
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewImageQueryBuilder()
newVal, err := qb.IncrementOCounter(imageID, tx)
func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (ret int, err error) {
imageID, err := strconv.Atoi(id)
if err != nil {
_ = tx.Rollback()
return 0, err
}
// Commit
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
ret, err = qb.IncrementOCounter(imageID)
return err
}); err != nil {
return 0, err
}
return newVal, nil
return ret, nil
}
func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (int, error) {
imageID, _ := strconv.Atoi(id)
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewImageQueryBuilder()
newVal, err := qb.DecrementOCounter(imageID, tx)
func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (ret int, err error) {
imageID, err := strconv.Atoi(id)
if err != nil {
_ = tx.Rollback()
return 0, err
}
// Commit
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
ret, err = qb.DecrementOCounter(imageID)
return err
}); err != nil {
return 0, err
}
return newVal, nil
return ret, nil
}
func (r *mutationResolver) ImageResetO(ctx context.Context, id string) (int, error) {
imageID, _ := strconv.Atoi(id)
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewImageQueryBuilder()
newVal, err := qb.ResetOCounter(imageID, tx)
func (r *mutationResolver) ImageResetO(ctx context.Context, id string) (ret int, err error) {
imageID, err := strconv.Atoi(id)
if err != nil {
_ = tx.Rollback()
return 0, err
}
// Commit
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Image()
ret, err = qb.ResetOCounter(imageID)
return err
}); err != nil {
return 0, err
}
return newVal, nil
return ret, nil
}

View File

@@ -2,11 +2,16 @@ package api
import (
"context"
"io/ioutil"
"path/filepath"
"time"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
@@ -15,13 +20,20 @@ func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMe
}
func (r *mutationResolver) MetadataImport(ctx context.Context) (string, error) {
manager.GetInstance().Import()
if err := manager.GetInstance().Import(); err != nil {
return "", err
}
return "todo", nil
}
func (r *mutationResolver) ImportObjects(ctx context.Context, input models.ImportObjectsInput) (string, error) {
t := manager.CreateImportTask(config.GetVideoFileNamingAlgorithm(), input)
_, err := manager.GetInstance().RunSingleTask(t)
t, err := manager.CreateImportTask(config.GetInstance().GetVideoFileNamingAlgorithm(), input)
if err != nil {
return "", err
}
_, err = manager.GetInstance().RunSingleTask(t)
if err != nil {
return "", err
}
@@ -30,12 +42,15 @@ func (r *mutationResolver) ImportObjects(ctx context.Context, input models.Impor
}
func (r *mutationResolver) MetadataExport(ctx context.Context) (string, error) {
manager.GetInstance().Export()
if err := manager.GetInstance().Export(); err != nil {
return "", err
}
return "todo", nil
}
func (r *mutationResolver) ExportObjects(ctx context.Context, input models.ExportObjectsInput) (*string, error) {
t := manager.CreateExportTask(config.GetVideoFileNamingAlgorithm(), input)
t := manager.CreateExportTask(config.GetInstance().GetVideoFileNamingAlgorithm(), input)
wg, err := manager.GetInstance().RunSingleTask(t)
if err != nil {
return nil, err
@@ -61,12 +76,12 @@ func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.Ge
}
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
manager.GetInstance().AutoTag(input.Performers, input.Studios, input.Tags)
manager.GetInstance().AutoTag(input)
return "todo", nil
}
func (r *mutationResolver) MetadataClean(ctx context.Context) (string, error) {
manager.GetInstance().Clean()
func (r *mutationResolver) MetadataClean(ctx context.Context, input models.CleanMetadataInput) (string, error) {
manager.GetInstance().Clean(input)
return "todo", nil
}
@@ -89,3 +104,42 @@ func (r *mutationResolver) JobStatus(ctx context.Context) (*models.MetadataUpdat
func (r *mutationResolver) StopJob(ctx context.Context) (bool, error) {
return manager.GetInstance().Status.Stop(), nil
}
func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.BackupDatabaseInput) (*string, error) {
// if download is true, then backup to temporary file and return a link
download := input.Download != nil && *input.Download
mgr := manager.GetInstance()
var backupPath string
if download {
utils.EnsureDir(mgr.Paths.Generated.Downloads)
f, err := ioutil.TempFile(mgr.Paths.Generated.Downloads, "backup*.sqlite")
if err != nil {
return nil, err
}
backupPath = f.Name()
f.Close()
} else {
backupPath = database.DatabaseBackupPath()
}
err := database.Backup(database.DB, backupPath)
if err != nil {
return nil, err
}
if download {
downloadHash := mgr.DownloadStore.RegisterFile(backupPath, "", false)
logger.Debugf("Generated backup file %s with hash %s", backupPath, downloadHash)
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
fn := filepath.Base(database.DatabaseBackupPath())
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
return &ret, nil
} else {
logger.Infof("Successfully backed up database to: %s", backupPath)
}
return nil, nil
}

View File

@@ -6,7 +6,6 @@ import (
"strconv"
"time"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
@@ -27,7 +26,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
// Process the base 64 encoded image string
if input.FrontImage != nil {
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
frontimageData, err = utils.ProcessImageInput(*input.FrontImage)
if err != nil {
return nil, err
}
@@ -35,7 +34,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
// Process the base 64 encoded image string
if input.BackImage != nil {
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
backimageData, err = utils.ProcessImageInput(*input.BackImage)
if err != nil {
return nil, err
}
@@ -85,24 +84,23 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
}
// Start the transaction and save the movie
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewMovieQueryBuilder()
movie, err := qb.Create(newMovie, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
// update image table
if len(frontimageData) > 0 {
if err := qb.UpdateMovieImages(movie.ID, frontimageData, backimageData, tx); err != nil {
_ = tx.Rollback()
return nil, err
var movie *models.Movie
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Movie()
movie, err = qb.Create(newMovie)
if err != nil {
return err
}
}
// Commit
if err := tx.Commit(); err != nil {
// update image table
if len(frontimageData) > 0 {
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
@@ -111,25 +109,32 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUpdateInput) (*models.Movie, error) {
// Populate movie from the input
movieID, _ := strconv.Atoi(input.ID)
movieID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
updatedMovie := models.MoviePartial{
ID: movieID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
var frontimageData []byte
var err error
frontImageIncluded := wasFieldIncluded(ctx, "front_image")
frontImageIncluded := translator.hasField("front_image")
if input.FrontImage != nil {
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
frontimageData, err = utils.ProcessImageInput(*input.FrontImage)
if err != nil {
return nil, err
}
}
backImageIncluded := wasFieldIncluded(ctx, "back_image")
backImageIncluded := translator.hasField("back_image")
var backimageData []byte
if input.BackImage != nil {
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
backimageData, err = utils.ProcessImageInput(*input.BackImage)
if err != nil {
return nil, err
}
@@ -142,94 +147,59 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
updatedMovie.Checksum = &checksum
}
if input.Aliases != nil {
updatedMovie.Aliases = &sql.NullString{String: *input.Aliases, Valid: true}
}
if input.Duration != nil {
duration := int64(*input.Duration)
updatedMovie.Duration = &sql.NullInt64{Int64: duration, Valid: true}
}
if input.Date != nil {
updatedMovie.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
}
if input.Rating != nil {
rating := int64(*input.Rating)
updatedMovie.Rating = &sql.NullInt64{Int64: rating, Valid: true}
} else {
// rating must be nullable
updatedMovie.Rating = &sql.NullInt64{Valid: false}
}
if input.StudioID != nil {
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
updatedMovie.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
} else {
// studio must be nullable
updatedMovie.StudioID = &sql.NullInt64{Valid: false}
}
if input.Director != nil {
updatedMovie.Director = &sql.NullString{String: *input.Director, Valid: true}
}
if input.Synopsis != nil {
updatedMovie.Synopsis = &sql.NullString{String: *input.Synopsis, Valid: true}
}
if input.URL != nil {
updatedMovie.URL = &sql.NullString{String: *input.URL, Valid: true}
}
updatedMovie.Aliases = translator.nullString(input.Aliases, "aliases")
updatedMovie.Duration = translator.nullInt64(input.Duration, "duration")
updatedMovie.Date = translator.sqliteDate(input.Date, "date")
updatedMovie.Rating = translator.nullInt64(input.Rating, "rating")
updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedMovie.Director = translator.nullString(input.Director, "director")
updatedMovie.Synopsis = translator.nullString(input.Synopsis, "synopsis")
updatedMovie.URL = translator.nullString(input.URL, "url")
// Start the transaction and save the movie
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewMovieQueryBuilder()
movie, err := qb.Update(updatedMovie, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
// update image table
if frontImageIncluded || backImageIncluded {
if !frontImageIncluded {
frontimageData, err = qb.GetFrontImage(updatedMovie.ID, tx)
if err != nil {
tx.Rollback()
return nil, err
}
var movie *models.Movie
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Movie()
movie, err = qb.Update(updatedMovie)
if err != nil {
return err
}
if !backImageIncluded {
backimageData, err = qb.GetBackImage(updatedMovie.ID, tx)
if err != nil {
tx.Rollback()
return nil, err
// update image table
if frontImageIncluded || backImageIncluded {
if !frontImageIncluded {
frontimageData, err = qb.GetFrontImage(updatedMovie.ID)
if err != nil {
return err
}
}
if !backImageIncluded {
backimageData, err = qb.GetBackImage(updatedMovie.ID)
if err != nil {
return err
}
}
if len(frontimageData) == 0 && len(backimageData) == 0 {
// both images are being nulled. Destroy them.
if err := qb.DestroyImages(movie.ID); err != nil {
return err
}
} else {
// HACK - if front image is null and back image is not null, then set the front image
// to the default image since we can't have a null front image and a non-null back image
if frontimageData == nil && backimageData != nil {
frontimageData, _ = utils.ProcessImageInput(models.DefaultMovieImage)
}
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {
return err
}
}
}
if len(frontimageData) == 0 && len(backimageData) == 0 {
// both images are being nulled. Destroy them.
if err := qb.DestroyMovieImages(movie.ID, tx); err != nil {
tx.Rollback()
return nil, err
}
} else {
// HACK - if front image is null and back image is not null, then set the front image
// to the default image since we can't have a null front image and a non-null back image
if frontimageData == nil && backimageData != nil {
_, frontimageData, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
}
if err := qb.UpdateMovieImages(movie.ID, frontimageData, backimageData, tx); err != nil {
_ = tx.Rollback()
return nil, err
}
}
}
// Commit
if err := tx.Commit(); err != nil {
return nil
}); err != nil {
return nil, err
}
@@ -237,13 +207,35 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
}
func (r *mutationResolver) MovieDestroy(ctx context.Context, input models.MovieDestroyInput) (bool, error) {
qb := models.NewMovieQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
if err := qb.Destroy(input.ID, tx); err != nil {
_ = tx.Rollback()
id, err := strconv.Atoi(input.ID)
if err != nil {
return false, err
}
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
return repo.Movie().Destroy(id)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string) (bool, error) {
ids, err := utils.StringSliceToIntSlice(movieIDs)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Movie()
for _, id := range ids {
if err := qb.Destroy(id); err != nil {
return err
}
}
return nil
}); err != nil {
return false, err
}
return true, nil

View File

@@ -3,11 +3,12 @@ package api
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/performer"
"github.com/stashapp/stash/pkg/utils"
)
@@ -19,7 +20,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
var err error
if input.Image != nil {
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
}
if err != nil {
@@ -84,43 +85,64 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
} else {
newPerformer.Favorite = sql.NullBool{Bool: false, Valid: true}
}
if input.Rating != nil {
newPerformer.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
newPerformer.Rating = sql.NullInt64{Valid: false}
}
if input.Details != nil {
newPerformer.Details = sql.NullString{String: *input.Details, Valid: true}
}
if input.DeathDate != nil {
newPerformer.DeathDate = models.SQLiteDate{String: *input.DeathDate, Valid: true}
}
if input.HairColor != nil {
newPerformer.HairColor = sql.NullString{String: *input.HairColor, Valid: true}
}
if input.Weight != nil {
weight := int64(*input.Weight)
newPerformer.Weight = sql.NullInt64{Int64: weight, Valid: true}
}
if err := performer.ValidateDeathDate(nil, input.Birthdate, input.DeathDate); err != nil {
if err != nil {
return nil, err
}
}
// Start the transaction and save the performer
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewPerformerQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
var performer *models.Performer
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Performer()
performer, err := qb.Create(newPerformer, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdatePerformerImage(performer.ID, imageData, tx); err != nil {
_ = tx.Rollback()
return nil, err
performer, err = qb.Create(newPerformer)
if err != nil {
return err
}
}
// Save the stash_ids
if input.StashIds != nil {
var stashIDJoins []models.StashID
for _, stashID := range input.StashIds {
newJoin := models.StashID{
StashID: stashID.StashID,
Endpoint: stashID.Endpoint,
if len(input.TagIds) > 0 {
if err := r.updatePerformerTags(qb, performer.ID, input.TagIds); err != nil {
return err
}
stashIDJoins = append(stashIDJoins, newJoin)
}
if err := jqb.UpdatePerformerStashIDs(performer.ID, stashIDJoins, tx); err != nil {
return nil, err
}
}
// Commit
if err := tx.Commit(); err != nil {
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(performer.ID, imageData); err != nil {
return err
}
}
// Save the stash_ids
if input.StashIds != nil {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(performer.ID, stashIDJoins); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
@@ -130,133 +152,259 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) {
// Populate performer from the input
performerID, _ := strconv.Atoi(input.ID)
updatedPerformer := models.Performer{
updatedPerformer := models.PerformerPartial{
ID: performerID,
UpdatedAt: models.SQLiteTimestamp{Timestamp: time.Now()},
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
var imageData []byte
var err error
imageIncluded := wasFieldIncluded(ctx, "image")
imageIncluded := translator.hasField("image")
if input.Image != nil {
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
if err != nil {
return nil, err
}
}
if input.Name != nil {
// generate checksum from performer name rather than image
checksum := utils.MD5FromString(*input.Name)
updatedPerformer.Name = sql.NullString{String: *input.Name, Valid: true}
updatedPerformer.Checksum = checksum
}
if input.URL != nil {
updatedPerformer.URL = sql.NullString{String: *input.URL, Valid: true}
}
if input.Gender != nil {
updatedPerformer.Gender = sql.NullString{String: input.Gender.String(), Valid: true}
}
if input.Birthdate != nil {
updatedPerformer.Birthdate = models.SQLiteDate{String: *input.Birthdate, Valid: true}
}
if input.Ethnicity != nil {
updatedPerformer.Ethnicity = sql.NullString{String: *input.Ethnicity, Valid: true}
}
if input.Country != nil {
updatedPerformer.Country = sql.NullString{String: *input.Country, Valid: true}
}
if input.EyeColor != nil {
updatedPerformer.EyeColor = sql.NullString{String: *input.EyeColor, Valid: true}
}
if input.Height != nil {
updatedPerformer.Height = sql.NullString{String: *input.Height, Valid: true}
}
if input.Measurements != nil {
updatedPerformer.Measurements = sql.NullString{String: *input.Measurements, Valid: true}
}
if input.FakeTits != nil {
updatedPerformer.FakeTits = sql.NullString{String: *input.FakeTits, Valid: true}
}
if input.CareerLength != nil {
updatedPerformer.CareerLength = sql.NullString{String: *input.CareerLength, Valid: true}
}
if input.Tattoos != nil {
updatedPerformer.Tattoos = sql.NullString{String: *input.Tattoos, Valid: true}
}
if input.Piercings != nil {
updatedPerformer.Piercings = sql.NullString{String: *input.Piercings, Valid: true}
}
if input.Aliases != nil {
updatedPerformer.Aliases = sql.NullString{String: *input.Aliases, Valid: true}
}
if input.Twitter != nil {
updatedPerformer.Twitter = sql.NullString{String: *input.Twitter, Valid: true}
}
if input.Instagram != nil {
updatedPerformer.Instagram = sql.NullString{String: *input.Instagram, Valid: true}
}
if input.Favorite != nil {
updatedPerformer.Favorite = sql.NullBool{Bool: *input.Favorite, Valid: true}
} else {
updatedPerformer.Favorite = sql.NullBool{Bool: false, Valid: true}
updatedPerformer.Name = &sql.NullString{String: *input.Name, Valid: true}
updatedPerformer.Checksum = &checksum
}
// Start the transaction and save the performer
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewPerformerQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
updatedPerformer.URL = translator.nullString(input.URL, "url")
performer, err := qb.Update(updatedPerformer, tx)
if err != nil {
tx.Rollback()
return nil, err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdatePerformerImage(performer.ID, imageData, tx); err != nil {
tx.Rollback()
return nil, err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyPerformerImage(performer.ID, tx); err != nil {
tx.Rollback()
return nil, err
if translator.hasField("gender") {
if input.Gender != nil {
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
} else {
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
}
}
// Save the stash_ids
if input.StashIds != nil {
var stashIDJoins []models.StashID
for _, stashID := range input.StashIds {
newJoin := models.StashID{
StashID: stashID.StashID,
Endpoint: stashID.Endpoint,
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
updatedPerformer.Country = translator.nullString(input.Country, "country")
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
updatedPerformer.Height = translator.nullString(input.Height, "height")
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
updatedPerformer.Details = translator.nullString(input.Details, "details")
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
// Start the transaction and save the p
var p *models.Performer
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Performer()
// need to get existing performer
existing, err := qb.Find(updatedPerformer.ID)
if err != nil {
return err
}
if existing == nil {
return fmt.Errorf("performer with id %d not found", updatedPerformer.ID)
}
if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil {
if err != nil {
return err
}
stashIDJoins = append(stashIDJoins, newJoin)
}
if err := jqb.UpdatePerformerStashIDs(performerID, stashIDJoins, tx); err != nil {
return nil, err
}
}
// Commit
if err := tx.Commit(); err != nil {
p, err = qb.Update(updatedPerformer)
if err != nil {
return err
}
// Save the tags
if translator.hasField("tag_ids") {
if err := r.updatePerformerTags(qb, p.ID, input.TagIds); err != nil {
return err
}
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(p.ID, imageData); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyImage(p.ID); err != nil {
return err
}
}
// Save the stash_ids
if translator.hasField("stash_ids") {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(performerID, stashIDJoins); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
return performer, nil
return p, nil
}
func (r *mutationResolver) updatePerformerTags(qb models.PerformerReaderWriter, performerID int, tagsIDs []string) error {
ids, err := utils.StringSliceToIntSlice(tagsIDs)
if err != nil {
return err
}
return qb.UpdateTags(performerID, ids)
}
func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models.BulkPerformerUpdateInput) ([]*models.Performer, error) {
performerIDs, err := utils.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, err
}
// Populate performer from the input
updatedTime := time.Now()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedPerformer := models.PerformerPartial{
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
updatedPerformer.URL = translator.nullString(input.URL, "url")
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.nullString(input.Country, "country")
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
updatedPerformer.Height = translator.nullString(input.Height, "height")
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.nullInt64(input.Rating, "rating")
updatedPerformer.Details = translator.nullString(input.Details, "details")
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
if translator.hasField("gender") {
if input.Gender != nil {
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
} else {
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
}
}
ret := []*models.Performer{}
// Start the transaction and save the scene marker
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Performer()
for _, performerID := range performerIDs {
updatedPerformer.ID = performerID
// need to get existing performer
existing, err := qb.Find(performerID)
if err != nil {
return err
}
if existing == nil {
return fmt.Errorf("performer with id %d not found", performerID)
}
if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil {
return err
}
performer, err := qb.Update(updatedPerformer)
if err != nil {
return err
}
ret = append(ret, performer)
// Save the tags
if translator.hasField("tag_ids") {
tagIDs, err := adjustTagIDs(qb, performerID, *input.TagIds)
if err != nil {
return err
}
if err := qb.UpdateTags(performerID, tagIDs); err != nil {
return err
}
}
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) PerformerDestroy(ctx context.Context, input models.PerformerDestroyInput) (bool, error) {
qb := models.NewPerformerQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
if err := qb.Destroy(input.ID, tx); err != nil {
_ = tx.Rollback()
id, err := strconv.Atoi(input.ID)
if err != nil {
return false, err
}
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
return repo.Performer().Destroy(id)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) PerformersDestroy(ctx context.Context, performerIDs []string) (bool, error) {
ids, err := utils.StringSliceToIntSlice(performerIDs)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Performer()
for _, id := range ids {
if err := qb.Destroy(id); err != nil {
return err
}
}
return nil
}); err != nil {
return false, err
}
return true, nil

View File

@@ -23,6 +23,7 @@ func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, t
}
}
config := config.GetInstance()
serverConnection := common.StashServerConnection{
Scheme: "http",
Port: config.GetPort(),

View File

@@ -3,64 +3,64 @@ package api
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (*models.Scene, error) {
// Start the transaction and save the scene
tx := database.DB.MustBeginTx(ctx, nil)
ret, err := r.sceneUpdate(input, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (ret *models.Scene, err error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
// Commit
if err := tx.Commit(); err != nil {
// Start the transaction and save the scene
if err := r.withTxn(ctx, func(repo models.Repository) error {
ret, err = r.sceneUpdate(input, translator, repo)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.SceneUpdateInput) ([]*models.Scene, error) {
func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.SceneUpdateInput) (ret []*models.Scene, err error) {
inputMaps := getUpdateInputMaps(ctx)
// Start the transaction and save the scene
tx := database.DB.MustBeginTx(ctx, nil)
if err := r.withTxn(ctx, func(repo models.Repository) error {
for i, scene := range input {
translator := changesetTranslator{
inputMap: inputMaps[i],
}
var ret []*models.Scene
thisScene, err := r.sceneUpdate(*scene, translator, repo)
ret = append(ret, thisScene)
for _, scene := range input {
thisScene, err := r.sceneUpdate(*scene, tx)
ret = append(ret, thisScene)
if err != nil {
_ = tx.Rollback()
return nil, err
if err != nil {
return err
}
}
}
// Commit
if err := tx.Commit(); err != nil {
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.Tx) (*models.Scene, error) {
func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Scene, error) {
// Populate scene from the input
sceneID, _ := strconv.Atoi(input.ID)
sceneID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
var coverImageData []byte
@@ -69,22 +69,18 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.T
ID: sceneID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
if input.Title != nil {
updatedScene.Title = &sql.NullString{String: *input.Title, Valid: true}
}
if input.Details != nil {
updatedScene.Details = &sql.NullString{String: *input.Details, Valid: true}
}
if input.URL != nil {
updatedScene.URL = &sql.NullString{String: *input.URL, Valid: true}
}
if input.Date != nil {
updatedScene.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
}
updatedScene.Title = translator.nullString(input.Title, "title")
updatedScene.Details = translator.nullString(input.Details, "details")
updatedScene.URL = translator.nullString(input.URL, "url")
updatedScene.Date = translator.sqliteDate(input.Date, "date")
updatedScene.Rating = translator.nullInt64(input.Rating, "rating")
updatedScene.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedScene.Organized = input.Organized
if input.CoverImage != nil && *input.CoverImage != "" {
var err error
_, coverImageData, err = utils.ProcessBase64Image(*input.CoverImage)
coverImageData, err = utils.ProcessImageInput(*input.CoverImage)
if err != nil {
return nil, err
}
@@ -92,81 +88,85 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.T
// update the cover after updating the scene
}
if input.Rating != nil {
updatedScene.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
// rating must be nullable
updatedScene.Rating = &sql.NullInt64{Valid: false}
}
if input.StudioID != nil {
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
updatedScene.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
} else {
// studio must be nullable
updatedScene.StudioID = &sql.NullInt64{Valid: false}
}
qb := models.NewSceneQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
scene, err := qb.Update(updatedScene, tx)
qb := repo.Scene()
scene, err := qb.Update(updatedScene)
if err != nil {
return nil, err
}
// update cover table
if len(coverImageData) > 0 {
if err := qb.UpdateSceneCover(sceneID, coverImageData, tx); err != nil {
return nil, err
}
}
// Clear the existing gallery value
gqb := models.NewGalleryQueryBuilder()
err = gqb.ClearGalleryId(sceneID, tx)
if err != nil {
return nil, err
}
if input.GalleryID != nil {
// Save the gallery
galleryID, _ := strconv.Atoi(*input.GalleryID)
updatedGallery := models.Gallery{
ID: galleryID,
SceneID: sql.NullInt64{Int64: int64(sceneID), Valid: true},
UpdatedAt: models.SQLiteTimestamp{Timestamp: updatedTime},
}
gqb := models.NewGalleryQueryBuilder()
_, err := gqb.Update(updatedGallery, tx)
if err != nil {
if err := qb.UpdateCover(sceneID, coverImageData); err != nil {
return nil, err
}
}
// Save the performers
var performerJoins []models.PerformersScenes
for _, pid := range input.PerformerIds {
performerID, _ := strconv.Atoi(pid)
performerJoin := models.PerformersScenes{
PerformerID: performerID,
SceneID: sceneID,
if translator.hasField("performer_ids") {
if err := r.updateScenePerformers(qb, sceneID, input.PerformerIds); err != nil {
return nil, err
}
performerJoins = append(performerJoins, performerJoin)
}
if err := jqb.UpdatePerformersScenes(sceneID, performerJoins, tx); err != nil {
return nil, err
}
// Save the movies
if translator.hasField("movies") {
if err := r.updateSceneMovies(qb, sceneID, input.Movies); err != nil {
return nil, err
}
}
// Save the tags
if translator.hasField("tag_ids") {
if err := r.updateSceneTags(qb, sceneID, input.TagIds); err != nil {
return nil, err
}
}
// Save the galleries
if translator.hasField("gallery_ids") {
if err := r.updateSceneGalleries(qb, sceneID, input.GalleryIds); err != nil {
return nil, err
}
}
// Save the stash_ids
if translator.hasField("stash_ids") {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(sceneID, stashIDJoins); err != nil {
return nil, err
}
}
// only update the cover image if provided and everything else was successful
if coverImageData != nil {
err = manager.SetSceneScreenshot(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
if err != nil {
return nil, err
}
}
return scene, nil
}
func (r *mutationResolver) updateScenePerformers(qb models.SceneReaderWriter, sceneID int, performerIDs []string) error {
ids, err := utils.StringSliceToIntSlice(performerIDs)
if err != nil {
return err
}
return qb.UpdatePerformers(sceneID, ids)
}
func (r *mutationResolver) updateSceneMovies(qb models.SceneReaderWriter, sceneID int, movies []*models.SceneMovieInput) error {
var movieJoins []models.MoviesScenes
for _, movie := range input.Movies {
movieID, _ := strconv.Atoi(movie.MovieID)
for _, movie := range movies {
movieID, err := strconv.Atoi(movie.MovieID)
if err != nil {
return err
}
movieJoin := models.MoviesScenes{
MovieID: movieID,
SceneID: sceneID,
}
if movie.SceneIndex != nil {
@@ -178,169 +178,106 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.T
movieJoins = append(movieJoins, movieJoin)
}
if err := jqb.UpdateMoviesScenes(sceneID, movieJoins, tx); err != nil {
return nil, err
}
// Save the tags
var tagJoins []models.ScenesTags
for _, tid := range input.TagIds {
tagID, _ := strconv.Atoi(tid)
tagJoin := models.ScenesTags{
SceneID: sceneID,
TagID: tagID,
}
tagJoins = append(tagJoins, tagJoin)
}
if err := jqb.UpdateScenesTags(sceneID, tagJoins, tx); err != nil {
return nil, err
}
return qb.UpdateMovies(sceneID, movieJoins)
}
// only update the cover image if provided and everything else was successful
if coverImageData != nil {
err = manager.SetSceneScreenshot(scene.GetHash(config.GetVideoFileNamingAlgorithm()), coverImageData)
if err != nil {
return nil, err
}
func (r *mutationResolver) updateSceneTags(qb models.SceneReaderWriter, sceneID int, tagsIDs []string) error {
ids, err := utils.StringSliceToIntSlice(tagsIDs)
if err != nil {
return err
}
return qb.UpdateTags(sceneID, ids)
}
// Save the stash_ids
if input.StashIds != nil {
var stashIDJoins []models.StashID
for _, stashID := range input.StashIds {
newJoin := models.StashID{
StashID: stashID.StashID,
Endpoint: stashID.Endpoint,
}
stashIDJoins = append(stashIDJoins, newJoin)
}
if err := jqb.UpdateSceneStashIDs(sceneID, stashIDJoins, tx); err != nil {
return nil, err
}
func (r *mutationResolver) updateSceneGalleries(qb models.SceneReaderWriter, sceneID int, galleryIDs []string) error {
ids, err := utils.StringSliceToIntSlice(galleryIDs)
if err != nil {
return err
}
return scene, nil
return qb.UpdateGalleries(sceneID, ids)
}
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.BulkSceneUpdateInput) ([]*models.Scene, error) {
sceneIDs, err := utils.StringSliceToIntSlice(input.Ids)
if err != nil {
return nil, err
}
// Populate scene from the input
updatedTime := time.Now()
// Start the transaction and save the scene marker
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewSceneQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedScene := models.ScenePartial{
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
if input.Title != nil {
updatedScene.Title = &sql.NullString{String: *input.Title, Valid: true}
}
if input.Details != nil {
updatedScene.Details = &sql.NullString{String: *input.Details, Valid: true}
}
if input.URL != nil {
updatedScene.URL = &sql.NullString{String: *input.URL, Valid: true}
}
if input.Date != nil {
updatedScene.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
}
if input.Rating != nil {
// a rating of 0 means unset the rating
if *input.Rating == 0 {
updatedScene.Rating = &sql.NullInt64{Int64: 0, Valid: false}
} else {
updatedScene.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
}
}
if input.StudioID != nil {
// empty string means unset the studio
if *input.StudioID == "" {
updatedScene.StudioID = &sql.NullInt64{Int64: 0, Valid: false}
} else {
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
updatedScene.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
}
}
updatedScene.Title = translator.nullString(input.Title, "title")
updatedScene.Details = translator.nullString(input.Details, "details")
updatedScene.URL = translator.nullString(input.URL, "url")
updatedScene.Date = translator.sqliteDate(input.Date, "date")
updatedScene.Rating = translator.nullInt64(input.Rating, "rating")
updatedScene.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedScene.Organized = input.Organized
ret := []*models.Scene{}
for _, sceneIDStr := range input.Ids {
sceneID, _ := strconv.Atoi(sceneIDStr)
updatedScene.ID = sceneID
// Start the transaction and save the scene marker
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
scene, err := qb.Update(updatedScene, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
for _, sceneID := range sceneIDs {
updatedScene.ID = sceneID
ret = append(ret, scene)
if input.GalleryID != nil {
// Save the gallery
galleryID, _ := strconv.Atoi(*input.GalleryID)
updatedGallery := models.Gallery{
ID: galleryID,
SceneID: sql.NullInt64{Int64: int64(sceneID), Valid: true},
UpdatedAt: models.SQLiteTimestamp{Timestamp: updatedTime},
}
gqb := models.NewGalleryQueryBuilder()
_, err := gqb.Update(updatedGallery, tx)
scene, err := qb.Update(updatedScene)
if err != nil {
_ = tx.Rollback()
return nil, err
}
}
// Save the performers
if wasFieldIncluded(ctx, "performer_ids") {
performerIDs, err := adjustScenePerformerIDs(tx, sceneID, *input.PerformerIds)
if err != nil {
_ = tx.Rollback()
return nil, err
return err
}
var performerJoins []models.PerformersScenes
for _, performerID := range performerIDs {
performerJoin := models.PerformersScenes{
PerformerID: performerID,
SceneID: sceneID,
ret = append(ret, scene)
// Save the performers
if translator.hasField("performer_ids") {
performerIDs, err := adjustScenePerformerIDs(qb, sceneID, *input.PerformerIds)
if err != nil {
return err
}
performerJoins = append(performerJoins, performerJoin)
}
if err := jqb.UpdatePerformersScenes(sceneID, performerJoins, tx); err != nil {
_ = tx.Rollback()
return nil, err
}
}
// Save the tags
if wasFieldIncluded(ctx, "tag_ids") {
tagIDs, err := adjustSceneTagIDs(tx, sceneID, *input.TagIds)
if err != nil {
_ = tx.Rollback()
return nil, err
}
var tagJoins []models.ScenesTags
for _, tagID := range tagIDs {
tagJoin := models.ScenesTags{
SceneID: sceneID,
TagID: tagID,
if err := qb.UpdatePerformers(sceneID, performerIDs); err != nil {
return err
}
tagJoins = append(tagJoins, tagJoin)
}
if err := jqb.UpdateScenesTags(sceneID, tagJoins, tx); err != nil {
_ = tx.Rollback()
return nil, err
// Save the tags
if translator.hasField("tag_ids") {
tagIDs, err := adjustTagIDs(qb, sceneID, *input.TagIds)
if err != nil {
return err
}
if err := qb.UpdateTags(sceneID, tagIDs); err != nil {
return err
}
}
// Save the galleries
if translator.hasField("gallery_ids") {
galleryIDs, err := adjustSceneGalleryIDs(qb, sceneID, *input.GalleryIds)
if err != nil {
return err
}
if err := qb.UpdateGalleries(sceneID, galleryIDs); err != nil {
return err
}
}
}
}
// Commit
if err := tx.Commit(); err != nil {
return nil
}); err != nil {
return nil, err
}
@@ -348,6 +285,17 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
}
func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
// if we are setting the ids, just return the ids
if updateIDs.Mode == models.BulkUpdateIDModeSet {
existingIDs = []int{}
for _, idStr := range updateIDs.Ids {
id, _ := strconv.Atoi(idStr)
existingIDs = append(existingIDs, id)
}
return existingIDs
}
for _, idStr := range updateIDs.Ids {
id, _ := strconv.Atoi(idStr)
@@ -373,67 +321,70 @@ func adjustIDs(existingIDs []int, updateIDs models.BulkUpdateIds) []int {
return existingIDs
}
func adjustScenePerformerIDs(tx *sqlx.Tx, sceneID int, ids models.BulkUpdateIds) ([]int, error) {
var ret []int
jqb := models.NewJoinsQueryBuilder()
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
// adding to the joins
performerJoins, err := jqb.GetScenePerformers(sceneID, tx)
if err != nil {
return nil, err
}
for _, join := range performerJoins {
ret = append(ret, join.PerformerID)
}
func adjustScenePerformerIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetPerformerIDs(sceneID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func adjustSceneTagIDs(tx *sqlx.Tx, sceneID int, ids models.BulkUpdateIds) ([]int, error) {
var ret []int
type tagIDsGetter interface {
GetTagIDs(id int) ([]int, error)
}
jqb := models.NewJoinsQueryBuilder()
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
// adding to the joins
tagJoins, err := jqb.GetSceneTags(sceneID, tx)
func adjustTagIDs(qb tagIDsGetter, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetTagIDs(sceneID)
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
for _, join := range tagJoins {
ret = append(ret, join.TagID)
}
func adjustSceneGalleryIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetGalleryIDs(sceneID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
qb := models.NewSceneQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
sceneID, _ := strconv.Atoi(input.ID)
scene, err := qb.Find(sceneID)
err = manager.DestroyScene(sceneID, tx)
sceneID, err := strconv.Atoi(input.ID)
if err != nil {
tx.Rollback()
return false, err
}
if err := tx.Commit(); err != nil {
var scene *models.Scene
var postCommitFunc func()
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
var err error
scene, err = qb.Find(sceneID)
if err != nil {
return err
}
if scene == nil {
return fmt.Errorf("scene with id %d not found", sceneID)
}
postCommitFunc, err = manager.DestroyScene(scene, repo)
return err
}); err != nil {
return false, err
}
// perform the post-commit actions
postCommitFunc()
// if delete generated is true, then delete the generated files
// for the scene
if input.DeleteGenerated != nil && *input.DeleteGenerated {
manager.DeleteGeneratedSceneFiles(scene, config.GetVideoFileNamingAlgorithm())
manager.DeleteGeneratedSceneFiles(scene, config.GetInstance().GetVideoFileNamingAlgorithm())
}
// if delete file is true, then delete the file as well
@@ -446,30 +397,36 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD
}
func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.ScenesDestroyInput) (bool, error) {
qb := models.NewSceneQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
var scenes []*models.Scene
for _, id := range input.Ids {
sceneID, _ := strconv.Atoi(id)
var postCommitFuncs []func()
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
scene, err := qb.Find(sceneID)
if scene != nil {
scenes = append(scenes, scene)
for _, id := range input.Ids {
sceneID, _ := strconv.Atoi(id)
scene, err := qb.Find(sceneID)
if scene != nil {
scenes = append(scenes, scene)
}
f, err := manager.DestroyScene(scene, repo)
if err != nil {
return err
}
postCommitFuncs = append(postCommitFuncs, f)
}
err = manager.DestroyScene(sceneID, tx)
if err != nil {
tx.Rollback()
return false, err
}
}
if err := tx.Commit(); err != nil {
return nil
}); err != nil {
return false, err
}
fileNamingAlgo := config.GetVideoFileNamingAlgorithm()
for _, f := range postCommitFuncs {
f()
}
fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm()
for _, scene := range scenes {
// if delete generated is true, then delete the generated files
// for the scene
@@ -488,8 +445,16 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene
}
func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.SceneMarkerCreateInput) (*models.SceneMarker, error) {
primaryTagID, _ := strconv.Atoi(input.PrimaryTagID)
sceneID, _ := strconv.Atoi(input.SceneID)
primaryTagID, err := strconv.Atoi(input.PrimaryTagID)
if err != nil {
return nil, err
}
sceneID, err := strconv.Atoi(input.SceneID)
if err != nil {
return nil, err
}
currentTime := time.Now()
newSceneMarker := models.SceneMarker{
Title: input.Title,
@@ -500,14 +465,31 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input models.S
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
}
return changeMarker(ctx, create, newSceneMarker, input.TagIds)
tagIDs, err := utils.StringSliceToIntSlice(input.TagIds)
if err != nil {
return nil, err
}
return r.changeMarker(ctx, create, newSceneMarker, tagIDs)
}
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input models.SceneMarkerUpdateInput) (*models.SceneMarker, error) {
// Populate scene marker from the input
sceneMarkerID, _ := strconv.Atoi(input.ID)
sceneID, _ := strconv.Atoi(input.SceneID)
primaryTagID, _ := strconv.Atoi(input.PrimaryTagID)
sceneMarkerID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
primaryTagID, err := strconv.Atoi(input.PrimaryTagID)
if err != nil {
return nil, err
}
sceneID, err := strconv.Atoi(input.SceneID)
if err != nil {
return nil, err
}
updatedSceneMarker := models.SceneMarker{
ID: sceneMarkerID,
Title: input.Title,
@@ -517,168 +499,151 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input models.S
UpdatedAt: models.SQLiteTimestamp{Timestamp: time.Now()},
}
return changeMarker(ctx, update, updatedSceneMarker, input.TagIds)
tagIDs, err := utils.StringSliceToIntSlice(input.TagIds)
if err != nil {
return nil, err
}
return r.changeMarker(ctx, update, updatedSceneMarker, tagIDs)
}
func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) {
qb := models.NewSceneMarkerQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
markerID, _ := strconv.Atoi(id)
marker, err := qb.Find(markerID)
markerID, err := strconv.Atoi(id)
if err != nil {
return false, err
}
if err := qb.Destroy(id, tx); err != nil {
_ = tx.Rollback()
return false, err
}
if err := tx.Commit(); err != nil {
var postCommitFunc func()
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.SceneMarker()
sqb := repo.Scene()
marker, err := qb.Find(markerID)
if err != nil {
return err
}
if marker == nil {
return fmt.Errorf("scene marker with id %d not found", markerID)
}
scene, err := sqb.Find(int(marker.SceneID.Int64))
if err != nil {
return err
}
postCommitFunc, err = manager.DestroySceneMarker(scene, marker, qb)
return err
}); err != nil {
return false, err
}
// delete the preview for the marker
sqb := models.NewSceneQueryBuilder()
scene, _ := sqb.Find(int(marker.SceneID.Int64))
if scene != nil {
seconds := int(marker.Seconds)
manager.DeleteSceneMarkerFiles(scene, seconds, config.GetVideoFileNamingAlgorithm())
}
postCommitFunc()
return true, nil
}
func changeMarker(ctx context.Context, changeType int, changedMarker models.SceneMarker, tagIds []string) (*models.SceneMarker, error) {
// Start the transaction and save the scene marker
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewSceneMarkerQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
func (r *mutationResolver) changeMarker(ctx context.Context, changeType int, changedMarker models.SceneMarker, tagIDs []int) (*models.SceneMarker, error) {
var existingMarker *models.SceneMarker
var sceneMarker *models.SceneMarker
var err error
switch changeType {
case create:
sceneMarker, err = qb.Create(changedMarker, tx)
case update:
// check to see if timestamp was changed
existingMarker, err = qb.Find(changedMarker.ID)
if err == nil {
sceneMarker, err = qb.Update(changedMarker, tx)
}
}
if err != nil {
_ = tx.Rollback()
return nil, err
}
var scene *models.Scene
// Save the marker tags
var markerTagJoins []models.SceneMarkersTags
for _, tid := range tagIds {
tagID, _ := strconv.Atoi(tid)
if tagID == changedMarker.PrimaryTagID {
continue // If this tag is the primary tag, then let's not add it.
}
markerTag := models.SceneMarkersTags{
SceneMarkerID: sceneMarker.ID,
TagID: tagID,
}
markerTagJoins = append(markerTagJoins, markerTag)
}
switch changeType {
case create:
if err := jqb.CreateSceneMarkersTags(markerTagJoins, tx); err != nil {
_ = tx.Rollback()
return nil, err
}
case update:
if err := jqb.UpdateSceneMarkersTags(changedMarker.ID, markerTagJoins, tx); err != nil {
_ = tx.Rollback()
return nil, err
}
}
// Start the transaction and save the scene marker
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.SceneMarker()
sqb := repo.Scene()
// Commit
if err := tx.Commit(); err != nil {
var err error
switch changeType {
case create:
sceneMarker, err = qb.Create(changedMarker)
case update:
// check to see if timestamp was changed
existingMarker, err = qb.Find(changedMarker.ID)
if err != nil {
return err
}
sceneMarker, err = qb.Update(changedMarker)
if err != nil {
return err
}
scene, err = sqb.Find(int(existingMarker.SceneID.Int64))
}
if err != nil {
return err
}
// Save the marker tags
// If this tag is the primary tag, then let's not add it.
tagIDs = utils.IntExclude(tagIDs, []int{changedMarker.PrimaryTagID})
return qb.UpdateTags(sceneMarker.ID, tagIDs)
}); err != nil {
return nil, err
}
// remove the marker preview if the timestamp was changed
if existingMarker != nil && existingMarker.Seconds != changedMarker.Seconds {
sqb := models.NewSceneQueryBuilder()
scene, _ := sqb.Find(int(existingMarker.SceneID.Int64))
if scene != nil {
seconds := int(existingMarker.Seconds)
manager.DeleteSceneMarkerFiles(scene, seconds, config.GetVideoFileNamingAlgorithm())
}
if scene != nil && existingMarker != nil && existingMarker.Seconds != changedMarker.Seconds {
seconds := int(existingMarker.Seconds)
manager.DeleteSceneMarkerFiles(scene, seconds, config.GetInstance().GetVideoFileNamingAlgorithm())
}
return sceneMarker, nil
}
func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (int, error) {
sceneID, _ := strconv.Atoi(id)
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewSceneQueryBuilder()
newVal, err := qb.IncrementOCounter(sceneID, tx)
func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (ret int, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
_ = tx.Rollback()
return 0, err
}
// Commit
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
ret, err = qb.IncrementOCounter(sceneID)
return err
}); err != nil {
return 0, err
}
return newVal, nil
return ret, nil
}
func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (int, error) {
sceneID, _ := strconv.Atoi(id)
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewSceneQueryBuilder()
newVal, err := qb.DecrementOCounter(sceneID, tx)
func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (ret int, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
_ = tx.Rollback()
return 0, err
}
// Commit
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
ret, err = qb.DecrementOCounter(sceneID)
return err
}); err != nil {
return 0, err
}
return newVal, nil
return ret, nil
}
func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (int, error) {
sceneID, _ := strconv.Atoi(id)
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewSceneQueryBuilder()
newVal, err := qb.ResetOCounter(sceneID, tx)
func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int, err error) {
sceneID, err := strconv.Atoi(id)
if err != nil {
_ = tx.Rollback()
return 0, err
}
// Commit
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
ret, err = qb.ResetOCounter(sceneID)
return err
}); err != nil {
return 0, err
}
return newVal, nil
return ret, nil
}
func (r *mutationResolver) SceneGenerateScreenshot(ctx context.Context, id string, at *float64) (string, error) {

View File

@@ -4,19 +4,25 @@ import (
"context"
"fmt"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scraper/stashbox"
)
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input models.StashBoxFingerprintSubmissionInput) (bool, error) {
boxes := config.GetStashBoxes()
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return false, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex])
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
return client.SubmitStashBoxFingerprints(input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
}
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input models.StashBoxBatchPerformerTagInput) (string, error) {
manager.GetInstance().StashBoxBatchPerformerTag(input)
return "todo", nil
}

View File

@@ -6,7 +6,6 @@ import (
"strconv"
"time"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
@@ -21,7 +20,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
// Process the base 64 encoded image string
if input.Image != nil {
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
if err != nil {
return nil, err
}
@@ -43,42 +42,43 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true}
}
if input.Rating != nil {
newStudio.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
newStudio.Rating = sql.NullInt64{Valid: false}
}
if input.Details != nil {
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
}
// Start the transaction and save the studio
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewStudioQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
var studio *models.Studio
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Studio()
studio, err := qb.Create(newStudio, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateStudioImage(studio.ID, imageData, tx); err != nil {
_ = tx.Rollback()
return nil, err
var err error
studio, err = qb.Create(newStudio)
if err != nil {
return err
}
}
// Save the stash_ids
if input.StashIds != nil {
var stashIDJoins []models.StashID
for _, stashID := range input.StashIds {
newJoin := models.StashID{
StashID: stashID.StashID,
Endpoint: stashID.Endpoint,
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(studio.ID, imageData); err != nil {
return err
}
stashIDJoins = append(stashIDJoins, newJoin)
}
if err := jqb.UpdateStudioStashIDs(studio.ID, stashIDJoins, tx); err != nil {
return nil, err
}
}
// Commit
if err := tx.Commit(); err != nil {
// Save the stash_ids
if input.StashIds != nil {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(studio.ID, stashIDJoins); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
@@ -87,7 +87,14 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.StudioUpdateInput) (*models.Studio, error) {
// Populate studio from the input
studioID, _ := strconv.Atoi(input.ID)
studioID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
updatedStudio := models.StudioPartial{
ID: studioID,
@@ -95,10 +102,10 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
}
var imageData []byte
imageIncluded := wasFieldIncluded(ctx, "image")
imageIncluded := translator.hasField("image")
if input.Image != nil {
var err error
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
if err != nil {
return nil, err
}
@@ -109,65 +116,49 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
updatedStudio.Name = &sql.NullString{String: *input.Name, Valid: true}
updatedStudio.Checksum = &checksum
}
if input.URL != nil {
updatedStudio.URL = &sql.NullString{String: *input.URL, Valid: true}
}
if input.ParentID != nil {
parentID, _ := strconv.ParseInt(*input.ParentID, 10, 64)
updatedStudio.ParentID = &sql.NullInt64{Int64: parentID, Valid: true}
} else {
// parent studio must be nullable
updatedStudio.ParentID = &sql.NullInt64{Valid: false}
}
updatedStudio.URL = translator.nullString(input.URL, "url")
updatedStudio.Details = translator.nullString(input.Details, "details")
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
// Start the transaction and save the studio
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewStudioQueryBuilder()
jqb := models.NewJoinsQueryBuilder()
var studio *models.Studio
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Studio()
if err := manager.ValidateModifyStudio(updatedStudio, tx); err != nil {
tx.Rollback()
return nil, err
}
studio, err := qb.Update(updatedStudio, tx)
if err != nil {
tx.Rollback()
return nil, err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateStudioImage(studio.ID, imageData, tx); err != nil {
tx.Rollback()
return nil, err
if err := manager.ValidateModifyStudio(updatedStudio, qb); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyStudioImage(studio.ID, tx); err != nil {
tx.Rollback()
return nil, err
}
}
// Save the stash_ids
if input.StashIds != nil {
var stashIDJoins []models.StashID
for _, stashID := range input.StashIds {
newJoin := models.StashID{
StashID: stashID.StashID,
Endpoint: stashID.Endpoint,
var err error
studio, err = qb.Update(updatedStudio)
if err != nil {
return err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(studio.ID, imageData); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyImage(studio.ID); err != nil {
return err
}
stashIDJoins = append(stashIDJoins, newJoin)
}
if err := jqb.UpdateStudioStashIDs(studioID, stashIDJoins, tx); err != nil {
return nil, err
}
}
// Commit
if err := tx.Commit(); err != nil {
// Save the stash_ids
if translator.hasField("stash_ids") {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(studioID, stashIDJoins); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
@@ -175,13 +166,35 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
}
func (r *mutationResolver) StudioDestroy(ctx context.Context, input models.StudioDestroyInput) (bool, error) {
qb := models.NewStudioQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
if err := qb.Destroy(input.ID, tx); err != nil {
_ = tx.Rollback()
id, err := strconv.Atoi(input.ID)
if err != nil {
return false, err
}
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
return repo.Studio().Destroy(id)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) StudiosDestroy(ctx context.Context, studioIDs []string) (bool, error) {
ids, err := utils.StringSliceToIntSlice(studioIDs)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Studio()
for _, id := range ids {
if err := qb.Destroy(id); err != nil {
return err
}
}
return nil
}); err != nil {
return false, err
}
return true, nil

View File

@@ -6,7 +6,6 @@ import (
"strconv"
"time"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
@@ -25,7 +24,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
var err error
if input.Image != nil {
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
if err != nil {
return nil, err
@@ -33,31 +32,29 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
}
// Start the transaction and save the tag
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewTagQueryBuilder()
var tag *models.Tag
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Tag()
// ensure name is unique
if err := manager.EnsureTagNameUnique(newTag, tx); err != nil {
tx.Rollback()
return nil, err
}
tag, err := qb.Create(newTag, tx)
if err != nil {
tx.Rollback()
return nil, err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateTagImage(tag.ID, imageData, tx); err != nil {
_ = tx.Rollback()
return nil, err
// ensure name is unique
if err := manager.EnsureTagNameUnique(newTag, qb); err != nil {
return err
}
}
// Commit
if err := tx.Commit(); err != nil {
tag, err = qb.Create(newTag)
if err != nil {
return err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(tag.ID, imageData); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
@@ -66,7 +63,11 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdateInput) (*models.Tag, error) {
// Populate tag from the input
tagID, _ := strconv.Atoi(input.ID)
tagID, err := strconv.Atoi(input.ID)
if err != nil {
return nil, err
}
updatedTag := models.Tag{
ID: tagID,
Name: input.Name,
@@ -74,11 +75,14 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
}
var imageData []byte
var err error
imageIncluded := wasFieldIncluded(ctx, "image")
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
}
imageIncluded := translator.hasField("image")
if input.Image != nil {
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
if err != nil {
return nil, err
@@ -86,50 +90,45 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
}
// Start the transaction and save the tag
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewTagQueryBuilder()
var tag *models.Tag
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Tag()
// ensure name is unique
existing, err := qb.Find(tagID, tx)
if err != nil {
tx.Rollback()
return nil, err
}
if existing == nil {
tx.Rollback()
return nil, fmt.Errorf("Tag with ID %d not found", tagID)
}
if existing.Name != updatedTag.Name {
if err := manager.EnsureTagNameUnique(updatedTag, tx); err != nil {
tx.Rollback()
return nil, err
// ensure name is unique
existing, err := qb.Find(tagID)
if err != nil {
return err
}
}
tag, err := qb.Update(updatedTag, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateTagImage(tag.ID, imageData, tx); err != nil {
tx.Rollback()
return nil, err
if existing == nil {
return fmt.Errorf("Tag with ID %d not found", tagID)
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyTagImage(tag.ID, tx); err != nil {
tx.Rollback()
return nil, err
}
}
// Commit
if err := tx.Commit(); err != nil {
if existing.Name != updatedTag.Name {
if err := manager.EnsureTagNameUnique(updatedTag, qb); err != nil {
return err
}
}
tag, err = qb.Update(updatedTag)
if err != nil {
return err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(tag.ID, imageData); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyImage(tag.ID); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
@@ -137,13 +136,35 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
}
func (r *mutationResolver) TagDestroy(ctx context.Context, input models.TagDestroyInput) (bool, error) {
qb := models.NewTagQueryBuilder()
tx := database.DB.MustBeginTx(ctx, nil)
if err := qb.Destroy(input.ID, tx); err != nil {
_ = tx.Rollback()
tagID, err := strconv.Atoi(input.ID)
if err != nil {
return false, err
}
if err := tx.Commit(); err != nil {
if err := r.withTxn(ctx, func(repo models.Repository) error {
return repo.Tag().Destroy(tagID)
}); err != nil {
return false, err
}
return true, nil
}
func (r *mutationResolver) TagsDestroy(ctx context.Context, tagIDs []string) (bool, error) {
ids, err := utils.StringSliceToIntSlice(tagIDs)
if err != nil {
return false, err
}
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Tag()
for _, id := range ids {
if err := qb.Destroy(id); err != nil {
return err
}
}
return nil
}); err != nil {
return false, err
}
return true, nil

View File

@@ -0,0 +1,70 @@
package api
import (
"context"
"errors"
"testing"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// TODO - move this into a common area
func newResolver() *Resolver {
return &Resolver{
txnManager: mocks.NewTransactionManager(),
}
}
const tagName = "tagName"
const errTagName = "errTagName"
const existingTagID = 1
const existingTagName = "existingTagName"
const newTagID = 2
func TestTagCreate(t *testing.T) {
r := newResolver()
tagRW := r.txnManager.(*mocks.TransactionManager).Tag().(*mocks.TagReaderWriter)
tagRW.On("FindByName", existingTagName, true).Return(&models.Tag{
ID: existingTagID,
Name: existingTagName,
}, nil).Once()
tagRW.On("FindByName", errTagName, true).Return(nil, nil).Once()
expectedErr := errors.New("TagCreate error")
tagRW.On("Create", mock.AnythingOfType("models.Tag")).Return(nil, expectedErr)
_, err := r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
Name: existingTagName,
})
assert.NotNil(t, err)
_, err = r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
Name: errTagName,
})
assert.Equal(t, expectedErr, err)
tagRW.AssertExpectations(t)
r = newResolver()
tagRW = r.txnManager.(*mocks.TransactionManager).Tag().(*mocks.TagReaderWriter)
tagRW.On("FindByName", tagName, true).Return(nil, nil).Once()
tagRW.On("Create", mock.AnythingOfType("models.Tag")).Return(&models.Tag{
ID: newTagID,
Name: tagName,
}, nil)
tag, err := r.Mutation().TagCreate(context.TODO(), models.TagCreateInput{
Name: tagName,
})
assert.Nil(t, err)
assert.NotNil(t, tag)
}

View File

@@ -34,6 +34,7 @@ func makeConfigResult() *models.ConfigResult {
}
func makeConfigGeneralResult() *models.ConfigGeneralResult {
config := config.GetInstance()
logFile := config.GetLogFile()
maxTranscodeSize := config.GetMaxTranscodeSize()
@@ -46,9 +47,12 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
Stashes: config.GetStashPaths(),
DatabasePath: config.GetDatabasePath(),
GeneratedPath: config.GetGeneratedPath(),
ConfigFilePath: config.GetConfigFilePath(),
ScrapersPath: config.GetScrapersPath(),
CachePath: config.GetCachePath(),
CalculateMd5: config.IsCalculateMD5(),
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
ParallelTasks: config.GetParallelTasks(),
PreviewSegments: config.GetPreviewSegments(),
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
PreviewExcludeStart: config.GetPreviewExcludeStart(),
@@ -56,6 +60,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
PreviewPreset: config.GetPreviewPreset(),
MaxTranscodeSize: &maxTranscodeSize,
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
APIKey: config.GetAPIKey(),
Username: config.GetUsername(),
Password: config.GetPasswordHash(),
MaxSessionAge: config.GetMaxSessionAge(),
@@ -70,12 +75,15 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
Excludes: config.GetExcludes(),
ImageExcludes: config.GetImageExcludes(),
ScraperUserAgent: &scraperUserAgent,
ScraperCertCheck: config.GetScraperCertCheck(),
ScraperCDPPath: &scraperCDPPath,
StashBoxes: config.GetStashBoxes(),
}
}
func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
config := config.GetInstance()
menuItems := config.GetMenuItems()
soundOnPreview := config.GetSoundOnPreview()
wallShowTitle := config.GetWallShowTitle()
wallPlayback := config.GetWallPlayback()
@@ -85,8 +93,10 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
css := config.GetCSS()
cssEnabled := config.GetCSSEnabled()
language := config.GetLanguage()
slideshowDelay := config.GetSlideshowDelay()
return &models.ConfigInterfaceResult{
MenuItems: menuItems,
SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle,
WallPlayback: &wallPlayback,
@@ -96,5 +106,6 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
CSS: &css,
CSSEnabled: &cssEnabled,
Language: &language,
SlideshowDelay: &slideshowDelay,
}
}

View File

@@ -7,17 +7,37 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) FindGallery(ctx context.Context, id string) (*models.Gallery, error) {
qb := models.NewGalleryQueryBuilder()
idInt, _ := strconv.Atoi(id)
return qb.Find(idInt, nil)
func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models.Gallery, err error) {
idInt, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Gallery().Find(idInt)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (*models.FindGalleriesResultType, error) {
qb := models.NewGalleryQueryBuilder()
galleries, total := qb.Query(galleryFilter, filter)
return &models.FindGalleriesResultType{
Count: total,
Galleries: galleries,
}, nil
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (ret *models.FindGalleriesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
galleries, total, err := repo.Gallery().Query(galleryFilter, filter)
if err != nil {
return err
}
ret = &models.FindGalleriesResultType{
Count: total,
Galleries: galleries,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -8,23 +8,48 @@ import (
)
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
qb := models.NewImageQueryBuilder()
var image *models.Image
var err error
if id != nil {
idInt, _ := strconv.Atoi(*id)
image, err = qb.Find(idInt)
} else if checksum != nil {
image, err = qb.FindByChecksum(*checksum)
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Image()
var err error
if id != nil {
idInt, err := strconv.Atoi(*id)
if err != nil {
return err
}
image, err = qb.Find(idInt)
} else if checksum != nil {
image, err = qb.FindByChecksum(*checksum)
}
return err
}); err != nil {
return nil, err
}
return image, err
return image, nil
}
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (*models.FindImagesResultType, error) {
qb := models.NewImageQueryBuilder()
images, total := qb.Query(imageFilter, filter)
return &models.FindImagesResultType{
Count: total,
Images: images,
}, nil
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *models.FindImagesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Image()
images, total, err := qb.Query(imageFilter, filter)
if err != nil {
return err
}
ret = &models.FindImagesResultType{
Count: total,
Images: images,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -7,27 +7,49 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) FindMovie(ctx context.Context, id string) (*models.Movie, error) {
qb := models.NewMovieQueryBuilder()
idInt, _ := strconv.Atoi(id)
return qb.Find(idInt, nil)
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Movie, err error) {
idInt, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Movie().Find(idInt)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (*models.FindMoviesResultType, error) {
qb := models.NewMovieQueryBuilder()
movies, total := qb.Query(movieFilter, filter)
return &models.FindMoviesResultType{
Count: total,
Movies: movies,
}, nil
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *models.FindMoviesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
movies, total, err := repo.Movie().Query(movieFilter, filter)
if err != nil {
return err
}
ret = &models.FindMoviesResultType{
Count: total,
Movies: movies,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) AllMovies(ctx context.Context) ([]*models.Movie, error) {
qb := models.NewMovieQueryBuilder()
return qb.All()
}
func (r *queryResolver) AllMovies(ctx context.Context) (ret []*models.Movie, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Movie().All()
return err
}); err != nil {
return nil, err
}
func (r *queryResolver) AllMoviesSlim(ctx context.Context) ([]*models.Movie, error) {
qb := models.NewMovieQueryBuilder()
return qb.AllSlim()
return ret, nil
}

View File

@@ -2,31 +2,53 @@ package api
import (
"context"
"github.com/stashapp/stash/pkg/models"
"strconv"
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) FindPerformer(ctx context.Context, id string) (*models.Performer, error) {
qb := models.NewPerformerQueryBuilder()
idInt, _ := strconv.Atoi(id)
return qb.Find(idInt)
func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *models.Performer, err error) {
idInt, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().Find(idInt)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (*models.FindPerformersResultType, error) {
qb := models.NewPerformerQueryBuilder()
performers, total := qb.Query(performerFilter, filter)
return &models.FindPerformersResultType{
Count: total,
Performers: performers,
}, nil
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (ret *models.FindPerformersResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
performers, total, err := repo.Performer().Query(performerFilter, filter)
if err != nil {
return err
}
ret = &models.FindPerformersResultType{
Count: total,
Performers: performers,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) AllPerformers(ctx context.Context) ([]*models.Performer, error) {
qb := models.NewPerformerQueryBuilder()
return qb.All()
}
func (r *queryResolver) AllPerformers(ctx context.Context) (ret []*models.Performer, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Performer().All()
return err
}); err != nil {
return nil, err
}
func (r *queryResolver) AllPerformersSlim(ctx context.Context) ([]*models.Performer, error) {
qb := models.NewPerformerQueryBuilder()
return qb.AllSlim()
return ret, nil
}

View File

@@ -9,70 +9,160 @@ import (
)
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
qb := models.NewSceneQueryBuilder()
var scene *models.Scene
var err error
if id != nil {
idInt, _ := strconv.Atoi(*id)
scene, err = qb.Find(idInt)
} else if checksum != nil {
scene, err = qb.FindByChecksum(*checksum)
}
return scene, err
}
func (r *queryResolver) FindSceneByHash(ctx context.Context, input models.SceneHashInput) (*models.Scene, error) {
qb := models.NewSceneQueryBuilder()
var scene *models.Scene
var err error
if input.Checksum != nil {
scene, err = qb.FindByChecksum(*input.Checksum)
if err != nil {
return nil, err
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Scene()
var err error
if id != nil {
idInt, err := strconv.Atoi(*id)
if err != nil {
return err
}
scene, err = qb.Find(idInt)
} else if checksum != nil {
scene, err = qb.FindByChecksum(*checksum)
}
}
if scene == nil && input.Oshash != nil {
scene, err = qb.FindByOSHash(*input.Oshash)
if err != nil {
return nil, err
}
}
return scene, err
}
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIds []int, filter *models.FindFilterType) (*models.FindScenesResultType, error) {
qb := models.NewSceneQueryBuilder()
scenes, total := qb.Query(sceneFilter, filter)
return &models.FindScenesResultType{
Count: total,
Scenes: scenes,
}, nil
}
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (*models.FindScenesResultType, error) {
qb := models.NewSceneQueryBuilder()
scenes, total := qb.QueryByPathRegex(filter)
return &models.FindScenesResultType{
Count: total,
Scenes: scenes,
}, nil
}
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config models.SceneParserInput) (*models.SceneParserResultType, error) {
parser := manager.NewSceneFilenameParser(filter, config)
result, count, err := parser.Parse()
if err != nil {
return err
}); err != nil {
return nil, err
}
return &models.SceneParserResultType{
Count: count,
Results: result,
}, nil
return scene, nil
}
func (r *queryResolver) FindSceneByHash(ctx context.Context, input models.SceneHashInput) (*models.Scene, error) {
var scene *models.Scene
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Scene()
var err error
if input.Checksum != nil {
scene, err = qb.FindByChecksum(*input.Checksum)
if err != nil {
return err
}
}
if scene == nil && input.Oshash != nil {
scene, err = qb.FindByOSHash(*input.Oshash)
if err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
return scene, nil
}
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var scenes []*models.Scene
var total int
var err error
if len(sceneIDs) > 0 {
scenes, err = repo.Scene().FindMany(sceneIDs)
if err == nil {
total = len(scenes)
}
} else {
scenes, total, err = repo.Scene().Query(sceneFilter, filter)
}
if err != nil {
return err
}
ret = &models.FindScenesResultType{
Count: total,
Scenes: scenes,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
sceneFilter := &models.SceneFilterType{}
if filter != nil && filter.Q != nil {
sceneFilter.Path = &models.StringCriterionInput{
Modifier: models.CriterionModifierMatchesRegex,
Value: "(?i)" + *filter.Q,
}
}
// make a copy of the filter if provided, nilling out Q
var queryFilter *models.FindFilterType
if filter != nil {
f := *filter
queryFilter = &f
queryFilter.Q = nil
}
scenes, total, err := repo.Scene().Query(sceneFilter, queryFilter)
if err != nil {
return err
}
ret = &models.FindScenesResultType{
Count: total,
Scenes: scenes,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) ParseSceneFilenames(ctx context.Context, filter *models.FindFilterType, config models.SceneParserInput) (ret *models.SceneParserResultType, err error) {
parser := manager.NewSceneFilenameParser(filter, config)
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
result, count, err := parser.Parse(repo)
if err != nil {
return err
}
ret = &models.SceneParserResultType{
Count: count,
Results: result,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) FindDuplicateScenes(ctx context.Context, distance *int) (ret [][]*models.Scene, err error) {
dist := 0
if distance != nil {
dist = *distance
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Scene().FindDuplicates(dist)
return err
}); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -2,14 +2,25 @@ package api
import (
"context"
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (*models.FindSceneMarkersResultType, error) {
qb := models.NewSceneMarkerQueryBuilder()
sceneMarkers, total := qb.Query(sceneMarkerFilter, filter)
return &models.FindSceneMarkersResultType{
Count: total,
SceneMarkers: sceneMarkers,
}, nil
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (ret *models.FindSceneMarkersResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
sceneMarkers, total, err := repo.SceneMarker().Query(sceneMarkerFilter, filter)
if err != nil {
return err
}
ret = &models.FindSceneMarkersResultType{
Count: total,
SceneMarkers: sceneMarkers,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -2,31 +2,55 @@ package api
import (
"context"
"github.com/stashapp/stash/pkg/models"
"strconv"
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) FindStudio(ctx context.Context, id string) (*models.Studio, error) {
qb := models.NewStudioQueryBuilder()
idInt, _ := strconv.Atoi(id)
return qb.Find(idInt, nil)
func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.Studio, err error) {
idInt, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Studio().Find(idInt)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (*models.FindStudiosResultType, error) {
qb := models.NewStudioQueryBuilder()
studios, total := qb.Query(studioFilter, filter)
return &models.FindStudiosResultType{
Count: total,
Studios: studios,
}, nil
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (ret *models.FindStudiosResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
studios, total, err := repo.Studio().Query(studioFilter, filter)
if err != nil {
return err
}
ret = &models.FindStudiosResultType{
Count: total,
Studios: studios,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) AllStudios(ctx context.Context) ([]*models.Studio, error) {
qb := models.NewStudioQueryBuilder()
return qb.All()
}
func (r *queryResolver) AllStudios(ctx context.Context) (ret []*models.Studio, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().All()
return err
}); err != nil {
return nil, err
}
func (r *queryResolver) AllStudiosSlim(ctx context.Context) ([]*models.Studio, error) {
qb := models.NewStudioQueryBuilder()
return qb.AllSlim()
return ret, nil
}

View File

@@ -7,27 +7,49 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) FindTag(ctx context.Context, id string) (*models.Tag, error) {
qb := models.NewTagQueryBuilder()
idInt, _ := strconv.Atoi(id)
return qb.Find(idInt, nil)
func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag, err error) {
idInt, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().Find(idInt)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType) (*models.FindTagsResultType, error) {
qb := models.NewTagQueryBuilder()
tags, total := qb.Query(tagFilter, filter)
return &models.FindTagsResultType{
Count: total,
Tags: tags,
}, nil
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType) (ret *models.FindTagsResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
tags, total, err := repo.Tag().Query(tagFilter, filter)
if err != nil {
return err
}
ret = &models.FindTagsResultType{
Count: total,
Tags: tags,
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *queryResolver) AllTags(ctx context.Context) ([]*models.Tag, error) {
qb := models.NewTagQueryBuilder()
return qb.All()
}
func (r *queryResolver) AllTags(ctx context.Context) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().All()
return err
}); err != nil {
return nil, err
}
func (r *queryResolver) AllTagsSlim(ctx context.Context) ([]*models.Tag, error) {
qb := models.NewTagQueryBuilder()
return qb.AllSlim()
return ret, nil
}

View File

@@ -17,3 +17,7 @@ func (r *queryResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateSt
return &ret, nil
}
func (r *queryResolver) SystemStatus(ctx context.Context) (*models.SystemStatus, error) {
return manager.GetInstance().GetSystemStatus(), nil
}

View File

@@ -7,16 +7,19 @@ import (
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*models.SceneStreamEndpoint, error) {
// find the scene
qb := models.NewSceneQueryBuilder()
idInt, _ := strconv.Atoi(*id)
scene, err := qb.Find(idInt)
if err != nil {
var scene *models.Scene
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
idInt, _ := strconv.Atoi(*id)
var err error
scene, err = repo.Scene().Find(idInt)
return err
}); err != nil {
return nil, err
}
@@ -27,5 +30,5 @@ func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*models
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewSceneURLBuilder(baseURL, scene.ID)
return manager.GetSceneStreamPaths(scene, builder.GetStreamURL())
return manager.GetSceneStreamPaths(scene, builder.GetStreamURL(), config.GetInstance().GetMaxStreamingTranscodeSize())
}

View File

@@ -88,14 +88,14 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models
return manager.GetInstance().ScraperCache.ScrapeMovieURL(url)
}
func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.StashBoxQueryInput) ([]*models.ScrapedScene, error) {
boxes := config.GetStashBoxes()
func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.StashBoxSceneQueryInput) ([]*models.ScrapedScene, error) {
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex])
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
if len(input.SceneIds) > 0 {
return client.FindStashBoxScenesByFingerprints(input.SceneIds)
@@ -107,3 +107,23 @@ func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.Sta
return nil, nil
}
func (r *queryResolver) QueryStashBoxPerformer(ctx context.Context, input models.StashBoxPerformerQueryInput) ([]*models.StashBoxPerformerQueryResult, error) {
boxes := config.GetInstance().GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
if len(input.PerformerIds) > 0 {
return client.FindStashBoxPerformersByNames(input.PerformerIds)
}
if input.Q != nil {
return client.QueryStashBoxPerformer(*input.Q)
}
return nil, nil
}

View File

@@ -12,7 +12,9 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
type imageRoutes struct{}
type imageRoutes struct {
txnManager models.TransactionManager
}
func (rs imageRoutes) Routes() chi.Router {
r := chi.NewRouter()
@@ -57,12 +59,16 @@ func ImageCtx(next http.Handler) http.Handler {
imageID, _ := strconv.Atoi(imageIdentifierQueryParam)
var image *models.Image
qb := models.NewImageQueryBuilder()
if imageID == 0 {
image, _ = qb.FindByChecksum(imageIdentifierQueryParam)
} else {
image, _ = qb.Find(imageID)
}
manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
qb := repo.Image()
if imageID == 0 {
image, _ = qb.FindByChecksum(imageIdentifierQueryParam)
} else {
image, _ = qb.Find(imageID)
}
return nil
})
if image == nil {
http.Error(w, http.StatusText(404), 404)

View File

@@ -6,11 +6,14 @@ import (
"strconv"
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
type movieRoutes struct{}
type movieRoutes struct {
txnManager models.TransactionManager
}
func (rs movieRoutes) Routes() chi.Router {
r := chi.NewRouter()
@@ -26,11 +29,16 @@ func (rs movieRoutes) Routes() chi.Router {
func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
movie := r.Context().Value(movieKey).(*models.Movie)
qb := models.NewMovieQueryBuilder()
image, _ := qb.GetFrontImage(movie.ID, nil)
defaultParam := r.URL.Query().Get("default")
if len(image) == 0 || defaultParam == "true" {
var image []byte
if defaultParam != "true" {
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
image, _ = repo.Movie().GetFrontImage(movie.ID)
return nil
})
}
if len(image) == 0 {
_, image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
}
@@ -39,11 +47,16 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
movie := r.Context().Value(movieKey).(*models.Movie)
qb := models.NewMovieQueryBuilder()
image, _ := qb.GetBackImage(movie.ID, nil)
defaultParam := r.URL.Query().Get("default")
if len(image) == 0 || defaultParam == "true" {
var image []byte
if defaultParam != "true" {
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
image, _ = repo.Movie().GetBackImage(movie.ID)
return nil
})
}
if len(image) == 0 {
_, image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
}
@@ -58,9 +71,12 @@ func MovieCtx(next http.Handler) http.Handler {
return
}
qb := models.NewMovieQueryBuilder()
movie, err := qb.Find(movieID, nil)
if err != nil {
var movie *models.Movie
if err := manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
var err error
movie, err = repo.Movie().Find(movieID)
return err
}); err != nil {
http.Error(w, http.StatusText(404), 404)
return
}

View File

@@ -6,11 +6,14 @@ import (
"strconv"
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
type performerRoutes struct{}
type performerRoutes struct {
txnManager models.TransactionManager
}
func (rs performerRoutes) Routes() chi.Router {
r := chi.NewRouter()
@@ -25,10 +28,16 @@ func (rs performerRoutes) Routes() chi.Router {
func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
performer := r.Context().Value(performerKey).(*models.Performer)
qb := models.NewPerformerQueryBuilder()
image, _ := qb.GetPerformerImage(performer.ID, nil)
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
image, _ = repo.Performer().GetImage(performer.ID)
return nil
})
}
if len(image) == 0 || defaultParam == "true" {
image, _ = getRandomPerformerImageUsingName(performer.Name.String, performer.Gender.String)
}
@@ -44,9 +53,12 @@ func PerformerCtx(next http.Handler) http.Handler {
return
}
qb := models.NewPerformerQueryBuilder()
performer, err := qb.Find(performerID)
if err != nil {
var performer *models.Performer
if err := manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
var err error
performer, err = repo.Performer().Find(performerID)
return err
}); err != nil {
http.Error(w, http.StatusText(404), 404)
return
}

View File

@@ -15,7 +15,9 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
type sceneRoutes struct{}
type sceneRoutes struct {
txnManager models.TransactionManager
}
func (rs sceneRoutes) Routes() chi.Router {
r := chi.NewRouter()
@@ -53,7 +55,7 @@ func getSceneFileContainer(scene *models.Scene) ffmpeg.Container {
container = ffmpeg.Container(scene.Format.String)
} else { // container isn't in the DB
// shouldn't happen, fallback to ffprobe
tmpVideoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path)
tmpVideoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
if err != nil {
logger.Errorf("[transcode] error reading video file: %s", err.Error())
return ffmpeg.Container("")
@@ -67,7 +69,7 @@ func getSceneFileContainer(scene *models.Scene) ffmpeg.Container {
func (rs sceneRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
fileNamingAlgo := config.GetVideoFileNamingAlgorithm()
fileNamingAlgo := config.GetInstance().GetVideoFileNamingAlgorithm()
filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.GetHash(fileNamingAlgo))
manager.RegisterStream(filepath, &w)
@@ -100,7 +102,7 @@ func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) {
func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path)
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
if err != nil {
logger.Errorf("[stream] error reading video file: %s", err.Error())
return
@@ -136,7 +138,7 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
// needs to be transcoded
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path)
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
if err != nil {
logger.Errorf("[stream] error reading video file: %s", err.Error())
return
@@ -156,7 +158,7 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
options := ffmpeg.GetTranscodeStreamOptions(*videoFile, videoCodec, audioCodec)
options.StartTime = startTime
options.MaxTranscodeSize = config.GetMaxStreamingTranscodeSize()
options.MaxTranscodeSize = config.GetInstance().GetMaxStreamingTranscodeSize()
if requestedSize != "" {
options.MaxTranscodeSize = models.StreamingResolutionEnum(requestedSize)
}
@@ -176,64 +178,76 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
filepath := manager.GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetVideoFileNamingAlgorithm()))
filepath := manager.GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
// fall back to the scene image blob if the file isn't present
screenshotExists, _ := utils.FileExists(filepath)
if screenshotExists {
http.ServeFile(w, r, filepath)
} else {
qb := models.NewSceneQueryBuilder()
cover, _ := qb.GetSceneCover(scene.ID, nil)
var cover []byte
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
cover, _ = repo.Scene().GetCover(scene.ID)
return nil
})
utils.ServeImage(cover, w, r)
}
}
func (rs sceneRoutes) Preview(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewPath(scene.GetHash(config.GetVideoFileNamingAlgorithm()))
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
utils.ServeFileNoCache(w, r, filepath)
}
func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewImagePath(scene.GetHash(config.GetVideoFileNamingAlgorithm()))
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewImagePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
http.ServeFile(w, r, filepath)
}
func getChapterVttTitle(marker *models.SceneMarker) string {
func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.SceneMarker) string {
if marker.Title != "" {
return marker.Title
}
qb := models.NewTagQueryBuilder()
primaryTag, err := qb.Find(marker.PrimaryTagID, nil)
if err != nil {
// should not happen
var ret string
if err := rs.txnManager.WithReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Tag()
primaryTag, err := qb.Find(marker.PrimaryTagID)
if err != nil {
return err
}
ret = primaryTag.Name
tags, err := qb.FindBySceneMarkerID(marker.ID)
if err != nil {
return err
}
for _, t := range tags {
ret += ", " + t.Name
}
return nil
}); err != nil {
panic(err)
}
ret := primaryTag.Name
tags, err := qb.FindBySceneMarkerID(marker.ID, nil)
if err != nil {
// should not happen
panic(err)
}
for _, t := range tags {
ret += ", " + t.Name
}
return ret
}
func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
qb := models.NewSceneMarkerQueryBuilder()
sceneMarkers, err := qb.FindBySceneID(scene.ID, nil)
if err != nil {
panic("invalid scene markers for chapter vtt")
var sceneMarkers []*models.SceneMarker
if err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
var err error
sceneMarkers, err = repo.SceneMarker().FindBySceneID(scene.ID)
return err
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
vttLines := []string{"WEBVTT", ""}
@@ -241,7 +255,7 @@ func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
vttLines = append(vttLines, strconv.Itoa(i+1))
time := utils.GetVTTTime(marker.Seconds)
vttLines = append(vttLines, time+" --> "+time)
vttLines = append(vttLines, getChapterVttTitle(marker))
vttLines = append(vttLines, rs.getChapterVttTitle(r.Context(), marker))
vttLines = append(vttLines, "")
}
vtt := strings.Join(vttLines, "\n")
@@ -253,42 +267,48 @@ func (rs sceneRoutes) ChapterVtt(w http.ResponseWriter, r *http.Request) {
func (rs sceneRoutes) VttThumbs(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
w.Header().Set("Content-Type", "text/vtt")
filepath := manager.GetInstance().Paths.Scene.GetSpriteVttFilePath(scene.GetHash(config.GetVideoFileNamingAlgorithm()))
filepath := manager.GetInstance().Paths.Scene.GetSpriteVttFilePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) VttSprite(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
w.Header().Set("Content-Type", "image/jpeg")
filepath := manager.GetInstance().Paths.Scene.GetSpriteImageFilePath(scene.GetHash(config.GetVideoFileNamingAlgorithm()))
filepath := manager.GetInstance().Paths.Scene.GetSpriteImageFilePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
qb := models.NewSceneMarkerQueryBuilder()
sceneMarker, err := qb.Find(sceneMarkerID)
if err != nil {
logger.Warn("Error when getting scene marker for stream")
http.Error(w, http.StatusText(404), 404)
var sceneMarker *models.SceneMarker
if err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
var err error
sceneMarker, err = repo.SceneMarker().Find(sceneMarkerID)
return err
}); err != nil {
logger.Warnf("Error when getting scene marker for stream: %s", err.Error())
http.Error(w, http.StatusText(500), 500)
return
}
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPath(scene.GetHash(config.GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
qb := models.NewSceneMarkerQueryBuilder()
sceneMarker, err := qb.Find(sceneMarkerID)
if err != nil {
logger.Warn("Error when getting scene marker for stream")
http.Error(w, http.StatusText(404), 404)
var sceneMarker *models.SceneMarker
if err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
var err error
sceneMarker, err = repo.SceneMarker().Find(sceneMarkerID)
return err
}); err != nil {
logger.Warnf("Error when getting scene marker for stream: %s", err.Error())
http.Error(w, http.StatusText(500), 500)
return
}
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPreviewImagePath(scene.GetHash(config.GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPreviewImagePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
// If the image doesn't exist, send the placeholder
exists, _ := utils.FileExists(filepath)
@@ -310,17 +330,21 @@ func SceneCtx(next http.Handler) http.Handler {
sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam)
var scene *models.Scene
qb := models.NewSceneQueryBuilder()
if sceneID == 0 {
// determine checksum/os by the length of the query param
if len(sceneIdentifierQueryParam) == 32 {
scene, _ = qb.FindByChecksum(sceneIdentifierQueryParam)
manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
qb := repo.Scene()
if sceneID == 0 {
// determine checksum/os by the length of the query param
if len(sceneIdentifierQueryParam) == 32 {
scene, _ = qb.FindByChecksum(sceneIdentifierQueryParam)
} else {
scene, _ = qb.FindByOSHash(sceneIdentifierQueryParam)
}
} else {
scene, _ = qb.FindByOSHash(sceneIdentifierQueryParam)
scene, _ = qb.Find(sceneID)
}
} else {
scene, _ = qb.Find(sceneID)
}
return nil
})
if scene == nil {
http.Error(w, http.StatusText(404), 404)

View File

@@ -6,11 +6,14 @@ import (
"strconv"
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
type studioRoutes struct{}
type studioRoutes struct {
txnManager models.TransactionManager
}
func (rs studioRoutes) Routes() chi.Router {
r := chi.NewRouter()
@@ -25,11 +28,17 @@ func (rs studioRoutes) Routes() chi.Router {
func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
studio := r.Context().Value(studioKey).(*models.Studio)
qb := models.NewStudioQueryBuilder()
image, _ := qb.GetStudioImage(studio.ID, nil)
defaultParam := r.URL.Query().Get("default")
if len(image) == 0 || defaultParam == "true" {
var image []byte
if defaultParam != "true" {
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
image, _ = repo.Studio().GetImage(studio.ID)
return nil
})
}
if len(image) == 0 {
_, image, _ = utils.ProcessBase64Image(models.DefaultStudioImage)
}
@@ -44,9 +53,12 @@ func StudioCtx(next http.Handler) http.Handler {
return
}
qb := models.NewStudioQueryBuilder()
studio, err := qb.Find(studioID, nil)
if err != nil {
var studio *models.Studio
if err := manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
var err error
studio, err = repo.Studio().Find(studioID)
return err
}); err != nil {
http.Error(w, http.StatusText(404), 404)
return
}

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