Compare commits

..

405 Commits

Author SHA1 Message Date
Tweeticoats
a73c99a61d Update docker file to add mechanicalsoup python library (#1688)
Add a new python library mechanicalsoup to the docker container.
There is a pending pull request in the Community scrapers for the Ifeelmyself that uses this library so it might be worth including it in the container.
This should add ~100k to the size of the container.
2021-08-31 20:14:39 +10:00
WithoutPants
709d7ce1cc Load TLS config files from config path before stash home (#1678)
* Load tls files from config or home directory
* Update README
* Require both ssl files if either present
2021-08-31 19:37:45 +10:00
gitgiggety
1774a3600c Fix Performers Is Missing: stash ID filter (#1681)
* Fix Performers Is Missing: stash ID filter.

Fixes #1679
2021-08-30 11:46:41 +10:00
FleetingOrchard
50cb6a9c79 Add duration statistics to stats page (#1626) 2021-08-26 13:37:08 +10:00
UnluckyChemical765
45a9aabdaf Add script offset / delay to Handy support. (#1573)
* Add script offset / delay to Handy support.

Further work on  #1376.

Offsets are added to the current video position, so a positive value leads to earlier motion.  (The most common setting.)

This is needed because most script times have a consistent delay when compared to the video. (Delay from the API calls to the server should be handled by the server offset calculation.)

* Rename scriptOffset to funscriptOffset
* Correct localisation keys

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-08-26 11:50:02 +10:00
gitgiggety
50217f6318 Rebuild image edit using formik (#1669)
* Rebuild image edit using formik
* Prompt on page leave when changes are not saved
* Only enables save when changes are made
* Wrap in <Form onSubmit> (not sure if this does anything)
2021-08-26 11:24:49 +10:00
gitgiggety
2e83405841 Add "toolbar" buttons for some list actions (#1673)
* Support adding buttons to list "toolbar"
* Show add and remove images to gallery in toolbar
* Show button to play selected scenes to list toolbar
2021-08-26 10:46:07 +10:00
gitgiggety
e98302fcd0 Add image and gallery count to tag listing (#1672) 2021-08-26 10:18:13 +10:00
gitgiggety
3f888a0335 Fix error when creating scene queue of single item (#1674)
* Fix error when creating scene queue of single item
2021-08-26 10:03:56 +10:00
gitgiggety
4c144db510 Performers listing improvements (#1671)
* Add sorting on image and gallery count to performers
* Make performer table headers translatable
* Add image and gallery count to performers table
* Make sure list table container fits the table

Make the table container minimally as wide as the table. This fixes the
table "overflowing" to the left and right and the left not being
accessible.

* Remove unnecessary truncation in tables

IMO there is no need to truncate the title in the scenes table and the
name in the performers table. This because both tables also contain an
image which means that multiple lines should be possible without really
extending the height of the row. Furthermore both tables contain other
values which might be way longer and also aren't wrapped (like tags and
performers for scenes, and aliases for performers).
2021-08-26 09:41:18 +10:00
Still Hsu
177339c14e Update zh-tw strings & fix various hard strings (#1666)
* Update unlocalized strings & fix various hard strings
* Fix incorrect placement of ignore_organized in en-US
* Add missing strings
* Fix hard string in PerformerList
2021-08-26 08:51:54 +10:00
EnameEtavir
cb6dab3c5f Fix: config race conditions with RWMutex (#1645)
* Fix: config race conditions with RWMutex

Added RWMutex to config.Instance which read or write locks
all instances where viper is used.

Refactored checksum manager to only use config and not
viper directly anymore.

All stash viper operations are now "behind" the config.Instance
and thus mutex "protected".
2021-08-24 15:18:30 +10:00
gitgiggety
da8803925c Add date, duration and rating to Movie sorting (#1663)
* Add date, duration and rating to Movie sorting

Fixes: #1637
2021-08-24 15:04:17 +10:00
gitgiggety
28b092885c Add option to filename parser to skip organized scenes (#1665)
Fixes #1219
2021-08-24 12:14:37 +10:00
gitgiggety
392fa3535c Rebuild gallery edit using formik (#1654)
* Rebuild gallery edit using formik

* Prompt on page leave when changes are not saved
* Fixes missing required validation
* Only enables save when changes are made
* Wrap in <Form onSubmit> (not sure if this does anything)
2021-08-24 12:01:05 +10:00
peolic
ac72d4db2b Fix scene bitrate unit (#1650) 2021-08-24 11:33:46 +10:00
InfiniteTF
0d4ab7f6f3 Fix studio editing (#1668) 2021-08-24 11:23:25 +10:00
kermieisinthehouse
7b3b2ae9ba Order sceneDuplicateChecker results by size (#1639) 2021-08-18 13:11:57 +10:00
EnameEtavir
9803684535 UI: Add checkbox to scenes List view (#1642) 2021-08-18 12:56:52 +10:00
WithoutPants
680af72dcf Custom page size and saved zoom level (#1636)
* Allow custom page sizes
* Save zoom level in filters
2021-08-18 12:14:56 +10:00
gitgiggety
fc6cafa15f Fix hierarchical criteria performance issue (#1643)
* Fix hierarchical criteria performance issue

Don't apply recursive clause to hierarchical criteria when the depth is
set to 0 (i.e.: no recursion is needed).

This undoes the current performance penalty on for example the studios
page. This as reported in #1519, using a database of 4M images, 30K
scenes and 500 studios. Without this fix loading the studios overview,
with the default of 40 items per page, takes 6 to 7 seconds. With this
fix it only takes 0,07 seconds reverting the performance back to the
pre-hierarchical filtering performance (tested against 508f7b84 which
was the last commit before #1397 was merged).
2021-08-17 13:43:22 +10:00
WithoutPants
0fc5a06332 Add scene queue keyboard shortcuts (#1635) 2021-08-16 14:14:39 +10:00
WithoutPants
b6d15cc077 Make performer scrape button a dropdown (#1634) 2021-08-16 10:16:05 +10:00
gitgiggety
7cb3d05535 Add (not) between modifiers for number criterion (#1559)
* Add (not) between modifiers for number criterion
* Extract list filters into dedicated components

Extract the filters from the AddFiltersDialog into custom components.
This allows for further refactorring where components will be bound to
criterions.

* Add placeholders to number and duration criterions
* Add backwards compatibility for saved filters

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-08-12 10:24:16 +10:00
WithoutPants
c29d8b547d Overwrite phash during generate if flag set (#1633) 2021-08-11 16:08:10 +10:00
WithoutPants
59c6fe046d Fix scrape dialog not showing when scraping url (#1632) 2021-08-11 15:54:29 +10:00
gitgiggety
d4d45d5a06 Rebuild Studio page by splitting view and edit (#1629)
* Rebuild Studio page by splitting view and edit
* Fix parent studio id, open studio link in same tab

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-08-11 14:44:18 +10:00
Still Hsu
53489106a6 Update zh-tw localization (#1631)
* Fill missing zh-tw fields
* Replace "Merge" verb hard string with i18n impl
2021-08-11 10:26:30 +10:00
kermieisinthehouse
c5e1a3ed72 Fix dropdown menus z-index issue (#1628) 2021-08-11 10:13:17 +10:00
gitgiggety
915533b8c5 Create default config directory during setup (#1623)
* Create default config directory during setup

Instead of creating the config directory during startup, create it
during setup when needed.

Fixes #1616
2021-08-10 14:58:14 +10:00
gitgiggety
59c7dd622b Fix links and styling of File Info tabs (#1622)
* Refactor HTML of movie and performer details panels

Refactor HTML of the movie and performer details panels so that the
items are contained in a single list (`<dl/>`). This allows using a grid
layout which means that the styling is easier to get right for multiple
form factors, fixing issues where "values" would overlap the "labels"
(for instance on my phone performers "Measurements" almost overlaps the
actual value, while there is a lot of space for the value itself).

This refactor also allows reusing the `TextField` and `URLField`
components as they don't have any styling related classes anymore (i.e.:
the `col-` classes are gone). Which means they can be used in more dense
places, like the SceneFileInfoPanel, as well. As the width of the label
/ value doesn't rely on the viewport size anymore (as happened due to
the `col-xl` usage, but for example the scene sidebar being small, and
16% being to small).

* Rebuild SceneFileInfoPanel

Completely rebuild the SceneFileInfoPanel to make use of the `TextField`
and `URLField` components. Using these components means that the URLs
automatically get `target="_blank" rel="noopener noreferrer"`.
Furhermore this should also improve the styling a bit, as described in
the previous commit.

* Rebuild ImageFileInfoPanel

Completely rebuild the ImageFileInfoPanel to make use of `TextField` and
`URLField` components. Furthermore it should resolve some small styling
issues.

* Rebuild GalleryFileInfoPanel

Rebuild the GalleryFileInfoPanel to make use of `TextField` and
`URLField` components. Using these components means that for example the
URLs automatically get `target="blank" rel="noopener noreferrer"`.

Also adds the url property as 1. at the moment it is nowhere accessible,
and 2. scenes also has it in this panel.

* Truncate links on the file info tabs at latest opportunity

On the File Info tabs links always have the link destination as text for
the link as well. But these texts can be long and without whitespace.
This means that the default applied `word-wrap: break-word` doesn't
really work as URLs (and paths) don't contain spaces that ofter. So
apply `word-break: break-all` instead so that the text will be as long
as possible and just cut off in the middle, instead of only at
whitespace. This thus means that the fully available width will be used
to display the URL.
2021-08-10 14:39:09 +10:00
fnoopv
d31b6841d0 Add Chinese Simplified Translation (#1620) 2021-08-10 14:15:56 +10:00
gitgiggety
dfd55346b2 Scrape tag exclusions (#1617)
* Add config option for scraper tag exclusion patterns

Add a config option for exclusing tags / tag patterns from the scraper
results.

* Handle tag exclusion patterns during scraping
2021-08-10 14:07:01 +10:00
capnrowdy
404eaa32d2 Changes to Default Performer Images (for Accessibility) (#1489)
* Changes to support custom URL paths
* Refactor image resolver code
* Initialise box files at startup
* Update packr

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-08-10 13:51:31 +10:00
WithoutPants
3e78d642a2 Remove unnecessary fields from StudioData (#1618) 2021-08-06 11:41:51 +10:00
gitgiggety
9d641c64e3 Actually implement TagFilter.marker_count (#1603)
* Actually implement TagFilter.marker_count

The marker_count filter/criterion as defined in TagFilterType isn't
actually implemented. This adds an implementation for it.

Do note this implementation _might_ have performance issues because of
using OR (in the join). Another implentation would be to remove both
joins and use:
```SQL
COUNT(
    SELECT id FROM scene_markers WHERE primary_tag_id = tags.id
  UNION
    SELECT scene_marker_id FROM scene_markers_tags WHERE tag_id = tags.id
)
```
Note this doesn't require a DISTINCT as UNION already removes any
duplicate records.

* Restore marker count filter and sorting

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-08-04 13:39:24 +10:00
gitgiggety
ac41416cd7 Restructure scraping settings (#1548)
* Change scrapers overview into collapsible per type
* Move scraping configuration to (renamed) scrapers tab

Rename the Scrapers tab to Scraping and move the scraping configuration
to this tab.
2021-08-04 12:32:58 +10:00
bnkai
518be8ee70 Upgrade gjson module (#1610) 2021-08-04 09:52:16 +10:00
WithoutPants
eaa23240f7 Tagger UI improvements (#1605)
* Choose fields to tag
* Use check-circle for success icon
* Maintain fingerprint results
* Show scene details
* Maintain whitespace in TruncatedText
* Use undefine for img when not setting
2021-08-04 09:44:51 +10:00
WithoutPants
f52bfae8ac Set stash id during performer scrape (#1608) 2021-08-04 09:33:21 +10:00
WithoutPants
7287ad3a05 Remove stripes and add background colour to default performer images (#1609) 2021-08-04 09:01:34 +10:00
WithoutPants
8a7577c9bf Fix inf values causing marshal error (#1607) 2021-08-03 14:29:57 +10:00
WithoutPants
c7d2ddc5db Fix unsetting performer gender (#1606)
* Fix unset performer gender
* Fix button group appearing over select menu
2021-08-03 13:13:48 +10:00
Felipe Fernandes Leandro
8f3036b351 Portuguese translation (#1587)
Co-authored-by: kermieisinthehouse <kermie@isinthe.house>
2021-08-03 10:58:46 +10:00
Jekora
ede8cca631 [Feature] Better resolution search (#1568)
* Fix width in database test setup
* Added more filters on resolution field
* added test to verify resolution range is defined for every resolution
* Refactor UI code

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-08-02 13:22:39 +10:00
WithoutPants
723446842f Split up Tagger component (#1534) 2021-08-02 10:32:23 +10:00
Phasetime
4bdd759dae German Translation (#1578)
* initial translation pass
* New line at EOF
2021-07-17 19:00:47 +10:00
gitgiggety
a13f43c13b Always wrap filter conditions in parentheses (#1577)
* Always wrap filter conditions in parentheses

Fixes #1571
2021-07-14 18:29:59 +10:00
thatbrick
1b20fd1ad6 Fix Incorrect loading of images on iOS devices (#1562)
* Removing the height parameter seems to resolve the issue without noticeable differences in functionality
2021-07-13 10:29:47 +10:00
gitgiggety
aecab2d131 Tag stable versions with the version number on Docker (#1550)
Fixes #1546
2021-07-13 09:59:09 +10:00
gitgiggety
9591faf3d4 Mobile fixes (#1539)
* Make new tag, gallery and studio pages mobile friendly
* Enable new button on mobile
* Update movies edit HTML to be more in line with scene

Update the code of the MovieEditPanel to be more in sync with the
SceneEditPanel. Changes made are:
 * Use FormUtil.renderLabel instead of manually building
 * Always apply xs=9 breakpoint

This fixes some layout issues on mobile while still looking the same on
tablet and desktop resolution.

* Enable delete button for tags, studios and movies on mobile
* Add changelog

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-07-12 20:56:38 +10:00
WithoutPants
7b85df868d Merge pull request #1558 from stashapp/master
Merge master to develop for version tag
2021-07-06 17:35:40 +10:00
WithoutPants
0c417ad439 Merge pull request #1544 from stashapp/develop
Merge develop to master for 0.8
2021-07-02 09:34:37 +10:00
InfiniteTF
73f99f019c Show unknown duration when tagging files without a duration (#1535) 2021-06-28 10:37:58 +10:00
bnkai
4c05535a13 Fix potential race condintion in CDP (#1536) 2021-06-28 10:36:51 +10:00
WithoutPants
4c838daa12 Add sprite to SceneData fragment (#1533)
* Add sprite to SceneData fragment

Necessary so that TypePolicies object is populated correctly.
2021-06-26 14:25:01 +10:00
WithoutPants
fd3c9153d5 Fix message id issue (#1530) 2021-06-24 10:50:51 +10:00
WithoutPants
0589df51cd Fix query text field bug (#1528) 2021-06-24 09:00:14 +10:00
WithoutPants
4e08e37d74 Clear scene tagger results on new search (#1526)
* Clear search results when searching in tagger
* Fix oshash messageID
2021-06-23 13:00:58 +10:00
WithoutPants
4e26633abb UI fixes (#1525)
* Fix scene page styling
* Hide plugins with no tasks on task page
* Update query on URL change
2021-06-23 11:57:03 +10:00
WithoutPants
debf21e6b2 Saved filter bugs (#1524)
* Don't use default filter in VIEW persist state
* Reshuffle randomly sorted saved filters
2021-06-23 11:01:23 +10:00
WithoutPants
2fdf672015 Fix scene galleries panel (#1523) 2021-06-23 10:43:54 +10:00
WithoutPants
4165e7779f Add various filter criteria (#1505)
* Add various filter criteria
* Add tag name criterion
2021-06-23 09:10:20 +10:00
WithoutPants
5ecea3f69f Test database generator (#1513) 2021-06-23 08:29:10 +10:00
peolic
be2fe1de26 Update chromedp to fix console errors (#1521) 2021-06-23 08:05:58 +10:00
WithoutPants
ae3400a9b1 DLNA refactor and support browse folder objects (#1517) 2021-06-22 18:56:16 +10:00
WithoutPants
5fdfbaa7f1 Query bug fixes (#1510)
* Fix joins being dropped
* Fix missing scene stash_id criterion
* Refactor criterion handlers
* Add tag alias filter
* Remove handleCriterionFunc
2021-06-21 15:48:28 +10:00
WithoutPants
df6e06aaf6 Allow navigation by clicking lightbox image (#1516) 2021-06-21 14:55:31 +10:00
InfiniteTF
3d1b949f4c Add button to scene page to open scene in external player (#679)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-06-21 14:52:21 +10:00
WithoutPants
8e636545f7 Include server host in plugin input (#1514) 2021-06-21 14:38:44 +10:00
gitgiggety
d7439b4832 Optimize studio filter on performers (#1515) 2021-06-21 14:17:43 +10:00
peolic
86bd993b60 fix scrape dialog title (#1511) 2021-06-17 17:57:19 +10:00
WithoutPants
dc7584d77e Saved filters (#1474)
* Refactor list filter
* Filter/criterion refactor
* Rename option value to type
* Remove None from options
* Add saved filter button
* Integrate default filters
2021-06-16 14:53:32 +10:00
WithoutPants
4fe4da6c01 Merge tags functionality (#1481)
* Add API to merge tags

Add new API endpoint, `tagsMerge(source, destination)` to merge multiple
tags into a single one. The "sources" must be provided as a list of ids
and the destination as a single id. All usages of the source tags
(scenes, markers (primary and additional), images, galleries and
performers) will be updated to the destination tag, all aliases of the
source tags will be updated to the destination, and the name of the
source will be added as alias to the destination as well.

* Add merge tag UI
* Add unit tests
* Update test mocks
* Update internationalisation
* Add changelog entry

Co-authored-by: gitgiggety <gitgiggety@outlook.com>
2021-06-16 14:33:54 +10:00
WithoutPants
45f4a5ba81 Various UI fixes (#1502)
* Set/unset existing ids when moving to/from set
* Refactor rating banner
* Fix overlapping in multi set
* Prevent UI crash on bad hierarchical input value
2021-06-16 12:17:54 +10:00
bnkai
b55715775d Fix concurrency issue in audio preview option (#1500) 2021-06-15 17:12:39 +10:00
Still Hsu
3ae187e6f0 Incorporate i18n into UI elements (#1471)
* Update zh-tw string table (till 975343d2)
* Prepare localization table
* Implement i18n for Performers & Tags
* Add "add" action strings
* Use Lodash merge for deep merging language JSONs

The original implementation does not properly merge language files, causing unexpected localization string fallback behavior.

* Localize pagination strings
* Use Field name value as null id fallback

...otherwise FormattedMessage is gonna throw when the ID is null

* Use localized "Path" string for all instances
* Localize the "Interface" tab under settings
* Localize scene & performer cards
* Rename locale folder for better compatibility with i18n-ally
* Localize majority of the categories and features
2021-06-14 15:48:59 +10:00
WithoutPants
46bbede9a0 Plugin hooks (#1452)
* Refactor session and plugin code
* Add context to job tasks
* Show hooks in plugins page
* Refactor session management
2021-06-11 17:24:58 +10:00
WithoutPants
dde361f9f3 Handle case sensitive file moves (#1427) 2021-06-11 15:25:09 +10:00
bnkai
f1786ad871 Make audio stream optional for preview generation (#1454) 2021-06-11 15:01:32 +10:00
WithoutPants
f843359ba3 Handle auto-tagging where filename has no whitespace in name (#1488) 2021-06-08 10:47:22 +10:00
WithoutPants
099b4ecc56 Fix marker display mode (#1487) 2021-06-07 15:09:06 +10:00
WithoutPants
3e80dffe34 Fix setting scene gallery (#1484) 2021-06-06 15:33:06 +10:00
WithoutPants
c53799c25b Fix Performer Studio filtering (#1483)
* Fix performer studio filtering

* Fix studio filter hook
2021-06-06 15:05:05 +10:00
WithoutPants
732cc57149 Fix card click selection (#1476)
* Make other cards use generic card
2021-06-04 12:11:17 +10:00
WithoutPants
2469012008 Fix empty sort by causing UI crash (#1480) 2021-06-04 09:49:10 +10:00
bnkai
ad0a9d0707 Fix movies sorting, scene studio editing (#1478)
* Fix movies:sort_by->scenes_count, scene:edit->remove studio
2021-06-04 09:21:17 +10:00
Still Hsu
2ce4d9f0d8 Fix exception when scene preview is unavailable (#1477) 2021-06-03 21:05:43 +10:00
gitgiggety
7164bb28ac Filter studio hierarchy (#1397)
* Add basic support for hierarchical filters

Add a new `hierarchicalMultiCriterionHandlerBuilder` filter type which
can / will be used for filtering hierarchical things like the
parent/child relation of the studios.
On the frontend side a new IHierarchicalLabeledIdCriterion criterion
type has been added to accompany this new filter type.

* Refactor movieQueryBuilder to use filterBuilder

Refactor the movieQueryBuilder to use the filterBuilder just as scene,
image and gallery as well.

* Support specifying depth for studios filter

Add an optional depth field to the studios filter for scenes, images,
galleries and movies. When specified that number of included
(grant)children are shown as well. In other words: this adds support for
showing scenes set to child studios when searching on the parent studio.

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-06-03 20:52:19 +10:00
WithoutPants
508f7b84f2 Fix plugin cache initialisation (#1475) 2021-06-03 11:00:17 +10:00
InfiniteTF
c98cc73f33 Fix scene editing of scenes with stash_ids (#1467) 2021-06-03 10:48:58 +10:00
WithoutPants
1bb5de12e3 Use formik for scene edit (#1429)
* Use formik for scene edit panel
* Fix unsetting rating
* Disable save if not dirty
* Movie image fixes
2021-06-02 08:33:38 +10:00
WithoutPants
975343d2e9 Make multiset mode buttons more obvious (#1435) 2021-06-01 08:55:15 +10:00
peolic
eec071f248 Small UI filter fixes (#1456)
* Fix interactive filter name/value
* place `none` filter criterion first and disable it
* display current criterion when editing filter
2021-06-01 08:16:16 +10:00
EnameEtavir
b5a26cec8b Deprecation Fix: Updated gqlgen server handler initialization (#1415)
Only changed to the new initialization API.
To minimize risk of possible regressions, I tried to make as little changes as possible.

Resolves #1135
2021-05-31 13:58:32 +10:00
WithoutPants
3e81d45ae9 UI filter refactor (#1406)
* Refactor Criterion
* Separate filter options from filter
* Rename utils to factory
* Sort sort by options by alphabetical
* Refactor criterion options
* Simplify list filter options
* Refactor i8n
* Simplify ILabeledIdCriterion
2021-05-31 13:46:21 +10:00
bnkai
c5fed1bbdc Fix sort by movies_scene_number (#1448) 2021-05-28 09:01:03 +10:00
peolic
47ecb9f9b1 Fix scraped tag creation (#1436) 2021-05-28 09:00:13 +10:00
WithoutPants
c70faa2a53 Tag aliases (#1412)
* Add Tag Update/UpdateFull
* Tag alias implementation
* Refactor tag page
* Add aliases in UI
* Include tag aliases in q filter
* Include aliases in tag select
* Add aliases to auto-tagger
* Use aliases in scraper
* Add tag aliases for filename parser
2021-05-26 14:36:05 +10:00
WithoutPants
9b57fbbf50 Embedded javascript plugins (#1393) 2021-05-26 14:17:53 +10:00
peolic
cc5ec650ae Fix scraper date parser failing when parsing time (#1431)
* Don't mutate the original scraped date

`time.Parse` is case-sensitive for some values, `AM/pm` in particular
2021-05-26 07:29:51 +10:00
EnameEtavir
d6ada23616 Fix: unhandled errors (#1419)
As these errors where not explicitly ignored with _, I made changes to make sure they bubble up.
2021-05-25 18:40:51 +10:00
WithoutPants
65baf46c40 Revert scrubber debounce (#1428) 2021-05-25 14:57:02 +10:00
stashist
872e0b531c utils: oshash: add tests (#1285)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-05-25 11:32:59 +10:00
stashist
fc9d70f702 utils: vtt: test + rewrite (#1284)
* utils: vtt: add tests

In lieu of documentation.

* utils: vtt: rewrite for correctness and simplicity

Now handles fractional seconds and negative values correctly.
2021-05-25 11:25:26 +10:00
peolic
3d93f7f0fe Fix invalid scene queue generated link (#1425)
* Escape quotes in criterion label

JSON encoding and slicing off the encompassing JSON-string quotes seems like a safer option

* Wrap criterion parse in try/catch

to prevent the page from crashing
2021-05-25 11:18:09 +10:00
EnameEtavir
5c4351f338 Cleanup fixes (#1422)
* cleanup: remove dead code

removing some code that does nothing

* cleanup: fixing usage of deprecated gqlgen/graphql api in api/changeset_translator

* cleanup: changing to recommended comparison methods

Changing byte and case-insensitive string comparison to the recommended methods.

* cleanup: making staticcheck happy
2021-05-25 11:03:09 +10:00
peolic
d326d4380f Add CreatedAt & UpdatedAt to all object resolvers (#1421)
* add `CreatedAt` & `UpdatedAt` to all objects
* add `FileModTime` to supported objects
* Use `GQL.SlimTagDataFragment` over `GQL.Tag`
2021-05-25 10:56:34 +10:00
Tweeticoats
3981a781b9 This adds extra libraries required by scrapers: requests-toolbelt lxml cloudscraper (#1381)
This installs pip in the prep environment, uses this to install cloudscraper then copies this to the final application container.
2021-05-25 10:48:06 +10:00
peolic
4bad988373 Remove performer "is missing scenes" filter (#1414) 2021-05-25 10:45:26 +10:00
EnameEtavir
fad558a618 Fix: login form label references (#1416)
Fixes the login form label references for Username and Password by adding
the missing id attributes to each of the input fields respectively
to which the labels where referring to.
2021-05-25 08:26:21 +10:00
EnameEtavir
dc453c193d Fix: file close even if file was not opened (#1417)
Fixed a bug where in many implementations of load-file functions the file-close was still
executed even if the file-open resulted in an error.
2021-05-25 07:52:55 +10:00
peolic
0472cd9996 Fix performer filters and sort keys (#1413)
* Fix "performer is missing image" filter
* Fix `scene_count` sort
* Add unit tests

Co-authored-by: bnkai <48220860+bnkai@users.noreply.github.com>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-05-24 17:45:51 +10:00
EnameEtavir
c08e0c0f60 add REACT_APP_PLATFORM_PORT for development (#1411)
* add REACT_APP_PLATFORM_PORT for development

Add development REACT_APP_PLATFORM_PORT env variable
to createClient.getPlatformURL so that you can more easily
run more stash platform services at once during development.

If non is given, it falls back to standard port "9999".
2021-05-24 15:44:40 +10:00
WithoutPants
0e01374537 Job queueing (#1379) 2021-05-24 14:24:18 +10:00
EnameEtavir
9aa2dfd96c fix typos in performer models repo interface (#1410)
tag methods used sceneID argument name instead of performerID
2021-05-24 14:07:08 +10:00
UnluckyChemical765
547f6d79ad Add Handy / Funscript support (#1377)
* Add funscript route to scenes

Adds a /scene/:id/funscript route which serves a funscript file, if present.

Current convention is that these are files stored with the same path, but with the extension ".funscript".

* Look for funscript during scan

This is stored in the Scene record and used to drive UI changes for funscript support.

Currently, that's limited to a funscript link in the Scene's file info.

* Add filtering and sorting for interactive
* Add Handy connection key to interface config
* Add Handy client and placeholder component.

Uses defucilis/thehandy, but not thehandy-react as I had difficulty integrating the context with the existing components.

Instead, the expensive calculation for the server time offset is put in localStorage for reuse.

A debounce was added when scrubbing the video, as otherwise it spammed the Handy API with updates to the current offset.
2021-05-24 13:34:28 +10:00
WithoutPants
33999d3e93 Studio Performers page (#1405)
* Refactor performer filter
* Add performer studio criterion
* Add Studio Performers page
2021-05-22 17:07:03 +10:00
gitgiggety
586d146fdb Apply all post processors to performer (#1387)
* Apply all post processors to performer

Scraping a performer by fragment doesn't correctly work with tags.
When tags are returned to the scraper then all are recognized as new.
This is due to the post process method not being applied while it should
be, as is done when scraping a performer by URL.
2021-05-21 12:32:28 +10:00
bnkai
ab24d0f625 Add subtractDays pp action to scraper (#1399) 2021-05-21 12:20:12 +10:00
WithoutPants
76019af3e5 DLNA (#1364) 2021-05-20 16:58:43 +10:00
WithoutPants
0f579076b6 Clamp page to max pages (#1403) 2021-05-20 07:28:42 +10:00
Still Hsu
81058a7807 i18n: Add zh-TW localization strings (#1375) 2021-05-18 13:24:40 +10:00
WithoutPants
22a2fc3177 Make ffmpeg download location more portable (#1384)
* Download ffmpeg to config path
* Add setup message for ffmpeg download
* Handle missing ffmpeg in tasks
2021-05-18 09:14:25 +10:00
WithoutPants
3df7ee06eb Shortcut scan walk for excluded folders (#1382)
* Shortcut scan walk for excluded folders
* Add note to manual
* Add changelog entry
2021-05-17 15:46:00 +10:00
bnkai
aba2514534 fix DirExists (#1388) 2021-05-17 15:05:29 +10:00
InfiniteTF
ce20df343a Add option to remove studio stash ids (#1378)
* Add option to remove studio stash ids
* Add v0.8 changelog
2021-05-17 14:58:25 +10:00
WithoutPants
9d138278c2 Github build action refinement and caching (#1295)
* Run in same container
* Add cross compile targets to makefile
* Use make targets and existing container
* Cache UI build
* Update cross-compile script
2021-05-16 19:19:56 +10:00
WithoutPants
16fe21138f CPU profiling (#1371)
* Add cpuprofile flag
* Add notes to readme
2021-05-16 17:21:11 +10:00
bnkai
bc9aa02835 Discard null values from scraper results (#1374) 2021-05-16 16:40:54 +10:00
WithoutPants
c73025c86d Merge pull request #1391 from stashapp/master
Merge back to develop for tag
2021-05-15 16:58:29 +10:00
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
JoeSmithStarkers
f0ec37c343 Added screenshots/previews to tagger list (#939)
* Added screenshots/previews to tagger list
* Move errors and stashids to subcontent container, and tweak layout
* Fix search-result margin
Co-authored-by: Infinite <infinitekittens@protonmail.com>
2020-11-24 08:02:31 +11:00
WithoutPants
93068e1ded Merge pull request #955 from stashapp/develop
0.4 release
2020-11-23 09:40:54 +11:00
WithoutPants
abf0281903 Merge branch 'master' into develop 2020-11-23 08:36:07 +11:00
WithoutPants
bbf7dbe98f Changelog for 0.4 release (#951)
* 0.4 changelog
* Updated galleries manual page
2020-11-23 08:19:54 +11:00
InfiniteTF
21806decca Expand tagger manual entry and add link to tagger interface (#947) 2020-11-20 12:07:36 +11:00
bnkai
e62e74bff4 Use symwalk for scrapers (#938) 2020-11-16 09:21:26 +11:00
WithoutPants
0a098b1d63 Selective scan (#940) 2020-11-16 09:20:04 +11:00
InfiniteTF
ba8b3b29a4 Update findGalleries to only fetch imageCount instead of all images (#941) 2020-11-15 14:40:47 +11:00
WithoutPants
c74f145224 Only set stash from env if not set (#937) 2020-11-12 10:05:09 +11:00
WithoutPants
beb84b8e94 Fix image memory issue (#935)
* Return slim images from mutations
* Fix potential memory leaks
2020-11-10 20:19:13 +11:00
peolic
ceb888958e Update scraping docs (#929)
* update editorconfig to ignore trailing whitespaces in markdown
* fix incorrect code example
* add missing genders
* add gallery to scraping docs
* reorder Scraping.md
2020-11-10 13:03:44 +11:00
InfiniteTF
6bb24d1744 Prevent movie link from being wiped when tagging scene (#933) 2020-11-09 12:29:13 +11:00
WithoutPants
8097cd39d2 Lightbox keybinds (#928)
* Add mousetrap-pause
* Disable keybinds while lightbox open
2020-11-08 11:22:38 +11:00
Belley
94392c7c4d Fixing image for Freeones Scrapers (#930) 2020-11-07 09:36:26 +11:00
bnkai
ccc2df7315 Abort PR upload if no server is available (#922)
* gracefully abort PR upload
* timeout if api doesn't respond in 15secs
2020-11-06 13:15:50 +11:00
InfiniteTF
8a04e5df62 Prevent studio parent id and scene rating from being unset when tagged (#927) 2020-11-06 13:14:17 +11:00
WithoutPants
5f482b7b8a Handle zip file modification (#877)
* Rescan zip if updating mod time
* Use inequality for mod time comparison
* Add sort by file_mod_time (fixes #469)
2020-11-05 10:26:51 +11:00
InfiniteTF
9ec762ae9a Fix outstanding tagger issues (#912)
* Fix potential image errors
* Fix issue preventing favoriting of tagged performers
* Add error handling in case of network issues
* Show individual search errors
* Unset scene results if query fails
* Don't abort scene submission if scene id isn't found
2020-11-05 08:28:58 +11:00
WithoutPants
66c7af62f6 Add gallery file info (#919) 2020-11-05 08:18:57 +11:00
bnkai
cc81d0b3ee Migrate PR build storage from transfer.sh to Gofile.io (#916) 2020-11-03 11:54:59 +11:00
WithoutPants
cbfd9e82b7 Fix image clean (#913)
* Use correct regex when cleaning images
* Clarify video exclusion pattern heading
2020-11-03 09:34:53 +11:00
WithoutPants
bae82513eb Add equals/not equals string criteria (#917)
* Fix encoding of string criteria
* Add equals and includes (and not) string criteria
2020-11-03 09:26:07 +11:00
WithoutPants
8e75a8fff5 Add selection and export for all list pages (#873)
* Include studios in movie export
* Generalise cards
* Add selection and export for movies
* Refactor gallery card
* Refactor export dialogs
* Add performer selection and export
* Add selection and export for studios
* Add selection and export of tags
* Include movie scenes and gallery images
2020-10-31 09:41:12 +11:00
WithoutPants
07212dbea9 Fix image thumbnail display (#907) 2020-10-30 09:52:53 +11:00
WithoutPants
90c5a9dd4a Add page sizes up to 1000 (#904) 2020-10-29 09:27:56 +11:00
WithoutPants
01227ceb85 Fix recursive loop (#905) 2020-10-27 12:53:34 +11:00
WithoutPants
c75b5c204d Don't set default studio image (#887) 2020-10-27 09:35:50 +11:00
SpedNSFW
cfbffb3b96 Add basic i18n implementation (#879)
To be used as a reference point for any future i18n additions for either new languages or more translatable content.
2020-10-27 09:35:25 +11:00
JoeSmithStarkers
47468fe122 Cache generated regex for each path (#891) 2020-10-26 15:57:58 +11:00
dullcibus
b3906f4b97 Add languages to code blocks for syntax highlight (#896)
Added languages to code blocks to get colored syntax highlighting for easier reading.
2020-10-26 15:12:42 +11:00
InfiniteTF
77da544e98 Wire up Tagger set tags checkbox (#889) 2020-10-24 19:19:45 +11:00
InfiniteTF
3346f8dcca Stash-box tagger integration (#454) 2020-10-24 14:31:39 +11:00
WithoutPants
70f73ecf4a Update freeones scraper (#881) 2020-10-24 13:12:21 +11:00
JoeSmithStarkers
2987b7f3d2 Video filters and transforms (#826) 2020-10-22 15:45:05 +11:00
JoeSmithStarkers
71c814c116 Added streaming quality options (#790) 2020-10-22 15:02:27 +11:00
InfiniteTF
a31c8ccd33 Fix new performer scraping (#880) 2020-10-22 12:30:22 +11:00
WithoutPants
109e55a25a Query url parameters (#878) 2020-10-22 11:56:04 +11:00
WithoutPants
228a5c5537 Use temp redirects for setup (#875) 2020-10-22 08:17:15 +11:00
com1234475
3832c8505a Make performer name mandatory in graphQL (#273) (#841) 2020-10-21 11:27:16 +11:00
SpedNSFW
147d0067f5 Add gallery scraping (#862) 2020-10-21 09:24:32 +11:00
WithoutPants
872bb70f6e Fix scan issue when encountering invalid symlinks (#871)
* Implement fixed symwalk algorithm
* Remove dependency
2020-10-20 17:00:23 +11:00
WithoutPants
8eda72ad89 Image improvements (#847)
* Fix image performer filtering
* Add performer images tab
* Add studio images tab
* Rename interface
* Add tag images tab
* Add path filtering for images
* Show image stats on stats page
* Fix incorrect scan counts after timeout
* Add gallery filters
* Relax scene gallery selector
2020-10-20 10:11:15 +11:00
InfiniteTF
80199f79f3 Persist gallery list state (#864) 2020-10-19 08:21:38 +11:00
InfiniteTF
cf003a55bf Fix loading issue in galleries and redirect on gallery creation (#857)
* Fix loading issue in galleries and redirect on gallery creation
* Add error messages when image/galleries aren't found
* Clean up gallery/image/performer/scene view states
* Simplify error messages
2020-10-17 09:57:02 +11:00
InfiniteTF
bbc6c43201 Add shim for String.prototype.replaceAll (#858) 2020-10-17 09:46:24 +11:00
InfiniteTF
d9270dd7c3 Treat empty string columns as missing (#852) 2020-10-16 11:14:48 +11:00
InfiniteTF
528b32d1b7 Sort images in galleries by path (#855) 2020-10-16 10:56:24 +11:00
InfiniteTF
73eb5c7a1f Add image-count sorting, and image deletion on gallery deletion (#853) 2020-10-16 10:35:50 +11:00
WithoutPants
d0f1ad3c24 Show static image if wall video missing (#849) 2020-10-15 10:53:00 +11:00
InfiniteTF
482f8cbd92 Fix clean and scan bugs (#846) 2020-10-14 10:51:36 +11:00
WithoutPants
12cba97192 Add scraper list page (#833) 2020-10-13 15:19:54 +11:00
WithoutPants
aca2c7c5f4 Images section (#813)
* Add new configuration options
* Refactor scan/clean
* Schema changes
* Add details to galleries
* Remove redundant code
* Refine thumbnail generation
* Gallery overhaul
* Don't allow modifying zip gallery images
* Show gallery card overlays
* Hide zoom slider when not in grid mode
2020-10-13 10:12:46 +11:00
InfiniteTF
df3252e24f Fix folder selector link color (#842) 2020-10-12 08:26:20 +11:00
WithoutPants
4f9af6ba27 Example python plugin (#825)
* Add example python plugin
* Fix log incorrectly detecting as progress level
2020-10-12 08:20:20 +11:00
WithoutPants
ade109d9e4 Path filter for scenes and galleries (#834) 2020-10-12 08:19:51 +11:00
WithoutPants
08276ac616 Add collapse button to scene page (#838) 2020-10-11 18:35:34 +11:00
WithoutPants
98dda782aa Prevent invalid date tag in video file from aborting scan (#836)
* Give more context when ffprobe fails
* Suppress JSONTime unmarshal error
* Tidy scan logging
2020-10-11 12:02:41 +11:00
InfiniteTF
ca14859339 Fix filter layout on devices (#840) 2020-10-11 12:02:01 +11:00
WithoutPants
d55177c170 Parent studio link in studio page (#835) 2020-10-10 10:29:31 +11:00
WithoutPants
c3a7d30a33 Convert scraped movie create data (#832) 2020-10-09 11:43:54 +11:00
bnkai
94dc74f4a8 Fix ffmpeg/ffprobe downloads (#824) 2020-10-03 17:28:02 +10:00
JoeSmithStarkers
30e88b96ee Added ffmpeg/ffprobe permission correction code for linux/osx (#814) 2020-10-03 16:59:23 +10:00
WithoutPants
8866670e53 Add partial import functionality (#812) 2020-09-20 18:36:02 +10:00
WithoutPants
7a45943e8e Stash box client interface (#751)
* Add gql client generation files
* Update dependencies
* Add stash-box client generation to the makefile
* Move scraped scene object matchers to models
* Add stash-box to scrape with dropdown
* Add scrape scene from fingerprint in UI
2020-09-17 19:57:18 +10:00
WithoutPants
b0b5621337 Add 0.4.0 changelog, convert to md (#803)
* Turn page into shared component
* Add v0.4.0 changelog markdown page
* Rename version files
* Markdown conversion
2020-09-16 09:54:24 +10:00
WithoutPants
03d4826c85 Selective export (#770) 2020-09-15 17:28:53 +10:00
InfiniteTF
03f5e1a442 Config for stash-box instances (#748) 2020-09-14 17:13:35 +10:00
bnkai
b527a8d137 Update makefile for static build (#808) 2020-09-14 11:12:36 +10:00
caustico
5df1e0025f Add filter on Movie section "Is Missing is scenes" (#800) 2020-09-14 10:35:54 +10:00
caustico
933d6d0bd5 Add gallery icon in "scene card" and "scene list table" (#799) 2020-09-12 19:11:43 +10:00
caustico
981b2622a5 Add filter country when click the performer flag (#795) 2020-09-12 17:55:02 +10:00
InfiniteTF
b541322d0a Hotfix for theme preview bug (#804) 2020-09-12 17:53:37 +10:00
InfiniteTF
7f907fdf41 Fix date timezone bug (#807) 2020-09-12 17:52:48 +10:00
InfiniteTF
147f50de6b Update gallery view layout and switch libraries (#793)
* Update gallery view layout and switch libraries
* Tweak gallery grid layout
2020-09-11 18:18:31 +10:00
InfiniteTF
5cd7dcaeb2 Library updates (#792)
* Upgrade Typescript to 4.0
* Update i18n-iso-countries to 6.0
* Update react-intl to 5.8.0
* Update jimp to 0.16.1
* Update apollo and graphql libraries
* Update various libraries and fix linting/type errors
* Refactor cache invalidation
* Codegen refetch queries
2020-09-11 13:01:00 +10:00
InfiniteTF
5ba11e0e93 Persist studio/gallery/marker list filters (#802) 2020-09-11 11:08:59 +10:00
InfiniteTF
9095ba21dc Scene card preview refactor (#787)
* Refactor scene card preview
* Add delay to video preview
2020-09-11 10:52:36 +10:00
WithoutPants
629126df98 Update to golang 1.13 (#754)
* Update to use golang 1.13
* Update node and ubuntu versions
* Update compiler version
2020-09-11 10:43:41 +10:00
WithoutPants
19d1d30946 Change cross compiler to fixed version (#794)
* Change cross compiler to fixed version
* Update compiler version in travis
2020-09-10 16:22:20 +10:00
InfiniteTF
19dfa571da Fix erroneous pending thumbnail on wall items (#784) 2020-09-09 08:37:19 +10:00
WithoutPants
a26151c9a3 Merge master back to develop for tagging 2020-09-03 10:07:15 +10:00
WithoutPants
fb5867ba59 Fix travis script error 2020-09-02 13:23:17 +10:00
2556 changed files with 376166 additions and 124400 deletions

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

@@ -0,0 +1,185 @@
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: Cache UI build
uses: actions/cache@v2
id: cache-ui
env:
cache-name: cache-ui
with:
path: ui/v2.5/build
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('ui/v2.5/yarn.lock', 'ui/v2.5/public/**', 'ui/v2.5/src/**', 'graphql/**/*.graphql') }}
- name: Cache go build
uses: actions/cache@v2
env:
cache-name: cache-go-cache
with:
path: .go-cache
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/go.sum') }}
- name: Start build container
run: |
mkdir -p .go-cache
docker run -d --name build --mount type=bind,source="$(pwd)",target=/stash,consistency=delegated --mount type=bind,source="$(pwd)/.go-cache",target=/root/.cache/go-build,consistency=delegated -w /stash $COMPILER_IMAGE tail -f /dev/null
- name: Pre-install
run: docker exec -t build /bin/bash -c "make pre-ui"
- name: Generate
run: docker exec -t build /bin/bash -c "make generate"
- name: Validate UI
# skip UI validation for pull requests if UI is unchanged
if: ${{ github.event_name != 'pull_request' || steps.cache-ui.outputs.cache-hit != 'true' }}
run: docker exec -t build /bin/bash -c "make ui-validate"
# TODO: Replace with `make validate` once `revive` is bundled in COMPILER_IMAGE
- name: Validate
run: docker exec -t build /bin/bash -c "make fmt-check vet it"
- name: Build UI
# skip UI build for pull requests if UI is unchanged (UI was cached)
# this means that the build version/time may be incorrect if the UI is
# not changed in a pull request
if: ${{ github.event_name != 'pull_request' || steps.cache-ui.outputs.cache-hit != 'true' }}
run: docker exec -t build /bin/bash -c "make ui-only"
- name: Compile for all supported platforms
run: |
docker exec -t build /bin/bash -c "make packr"
docker exec -t build /bin/bash -c "make cross-compile-windows"
docker exec -t build /bin/bash -c "make cross-compile-osx"
docker exec -t build /bin/bash -c "make cross-compile-linux"
docker exec -t build /bin/bash -c "make cross-compile-linux-arm64v8"
docker exec -t build /bin/bash -c "make cross-compile-linux-arm32v7"
docker exec -t build /bin/bash -c "make cross-compile-pi"
- name: Cleanup build container
run: docker rm -f -v build
- 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 "${{ github.event.release.tag_name }}"

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

13
.gqlgenc.yml Normal file
View File

@@ -0,0 +1,13 @@
model:
filename: ./pkg/scraper/stashbox/graphql/generated_models.go
client:
filename: ./pkg/scraper/stashbox/graphql/generated_client.go
models:
Date:
model: github.com/99designs/gqlgen/graphql.String
endpoint:
# This points to stashdb.org currently, but can be directed at any stash-box
# instance. It is used for generation only.
url: https://stashdb.org/graphql
query:
- "./graphql/stash-box/*.graphql"

View File

@@ -5,13 +5,31 @@ git:
depth: false
language: go
go:
- 1.11.x
- 1.13.x
services:
- docker
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
@@ -23,8 +41,10 @@ script:
#- make lint
- make fmt-check vet it
after_success:
- docker pull stashapp/compiler:develop
- 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
@@ -32,7 +52,7 @@ before_deploy:
- export RELEASE_DATE=$(date +'%Y-%m-%d %H:%M:%S %Z')
- export STASH_VERSION=$(git describe --tags --exclude latest_develop)
# set TRAVIS_TAG explcitly to the version so that it doesn't pick up latest_develop
- if [ "$TRAVIS_BRANCH" = "master"]; then export TRAVIS_TAG=${STASH_VERSION}; fi
- if [ "$TRAVIS_BRANCH" = "master" ]; then export TRAVIS_TAG=${STASH_VERSION}; fi
deploy:
# latest develop release
- provider: releases
@@ -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

@@ -22,6 +22,9 @@ ifdef OUTPUT
OUTPUT := -o $(OUTPUT)
endif
export CGO_ENABLED = 1
export GO111MODULE = on
.PHONY: release pre-build install clean
release: generate ui build-release
@@ -41,13 +44,59 @@ endif
build: pre-build
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/pkg/api.version=$(STASH_VERSION)' -X 'github.com/stashapp/stash/pkg/api.buildstamp=$(BUILD_DATE)' -X 'github.com/stashapp/stash/pkg/api.githash=$(GITHASH)')
$(SET) CGO_ENABLED=1 $(SEPARATOR) go build $(OUTPUT) -mod=vendor -v -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS)"
go build $(OUTPUT) -mod=vendor -v -tags "sqlite_omit_load_extension osusergo netgo" -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS)"
# strips debug symbols from the release build
# consider -trimpath in go build if we move to go 1.13+
build-release: EXTRA_LDFLAGS := -s -w
build-release: build
build-release-static: EXTRA_LDFLAGS := -extldflags=-static -s -w
build-release-static: build
# cross-compile- targets should be run within the compiler docker container
cross-compile-windows: export GOOS := windows
cross-compile-windows: export GOARCH := amd64
cross-compile-windows: export CC := x86_64-w64-mingw32-gcc
cross-compile-windows: export CXX := x86_64-w64-mingw32-g++
cross-compile-windows: OUTPUT := -o dist/stash-win.exe
cross-compile-windows: build-release-static
cross-compile-osx: export GOOS := darwin
cross-compile-osx: export GOARCH := amd64
cross-compile-osx: export CC := o64-clang
cross-compile-osx: export CXX := o64-clang++
cross-compile-osx: OUTPUT := -o dist/stash-osx
# can't use static build for OSX
cross-compile-osx: build-release
cross-compile-linux: export GOOS := linux
cross-compile-linux: export GOARCH := amd64
cross-compile-linux: OUTPUT := -o dist/stash-linux
cross-compile-linux: build-release-static
cross-compile-linux-arm64v8: export GOOS := linux
cross-compile-linux-arm64v8: export GOARCH := arm64
cross-compile-linux-arm64v8: export CC := aarch64-linux-gnu-gcc
cross-compile-linux-arm64v8: OUTPUT := -o dist/stash-linux-arm64v8
cross-compile-linux-arm64v8: build-release-static
cross-compile-linux-arm32v7: export GOOS := linux
cross-compile-linux-arm32v7: export GOARCH := arm
cross-compile-linux-arm32v7: export GOARM := 7
cross-compile-linux-arm32v7: export CC := arm-linux-gnueabihf-gcc
cross-compile-linux-arm32v7: OUTPUT := -o dist/stash-linux-arm32v7
cross-compile-linux-arm32v7: build-release-static
cross-compile-pi: export GOOS := linux
cross-compile-pi: export GOARCH := arm
cross-compile-pi: export GOARM := 6
cross-compile-pi: export CC := arm-linux-gnueabi-gcc
cross-compile-pi: OUTPUT := -o dist/stash-pi
cross-compile-pi: build-release-static
cross-compile-all: cross-compile-windows cross-compile-osx cross-compile-linux cross-compile-linux-arm64v8 cross-compile-linux-arm32v7 cross-compile-pi
install:
packr2 install
@@ -60,6 +109,11 @@ generate:
go generate -mod=vendor
cd ui/v2.5 && yarn run gqlgen
# Regenerates stash-box client files
.PHONY: generate-stash-box-client
generate-stash-box-client:
go run -mod=vendor github.com/Yamashou/gqlgenc
# Runs gofmt -w on the project's source code, modifying any files that do not match its style.
.PHONY: fmt
fmt:
@@ -89,6 +143,11 @@ test:
it:
go test -mod=vendor -tags=integration ./...
# generates test mocks
.PHONY: generate-test-mocks
generate-test-mocks:
go run -mod=vendor github.com/vektra/mockery/v2 --dir ./pkg/models --name '.*ReaderWriter' --outpkg mocks --output ./pkg/models/mocks
# installs UI dependencies. Run when first cloning repository, or if UI
# dependencies have changed
.PHONY: pre-ui

View File

@@ -6,71 +6,94 @@
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`
This command would need customizing for your environment. [This link](https://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl) might be useful.
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.
Once you have a certificate and key file name them `stash.crt` and `stash.key` and place them in the same directory as the `config.yml` file, or 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).
@@ -108,6 +131,7 @@ NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
* `make fmt-check` - Ensure changed files are formatted correctly
* `make it` - Run the unit and integration tests
* `make validate` - Run all of the tests and checks required to submit a PR
* `make ui-start` - Runs the UI in development mode. Requires a running stash server to connect to. Stash port can be changed from the default of `9999` with environment variable `REACT_APP_PLATFORM_PORT`.
## Building a release
@@ -117,15 +141,20 @@ 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
## Profiling
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 can be profiled using the `--cpuprofile <output profile filename>` command line flag.
[Stash Plex Theme](https://github.com/stashapp/stash/wiki/Stash-Plex-Theme) is a community created theme inspired by popular Plex Interface.
The resulting file can then be used with pprof as follows:
`go tool pprof <path to binary> <path to profile filename>`
With `graphviz` installed and in the path, a call graph can be generated with:
`go tool pprof -svg <path to binary> <path to profile filename> > <output svg file>`

View File

@@ -2,10 +2,10 @@
# ie from top=level stash:
# docker build -t stash/build -f docker/build/x86_64/Dockerfile .
FROM golang:1.11.13 as compiler
FROM golang:1.13.15 as compiler
RUN apt-get update && apt-get install -y apt-transport-https
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash -
# prevent caching of the key
ADD https://dl.yarnpkg.com/debian/pubkey.gpg yarn.gpg
@@ -48,11 +48,13 @@ RUN make generate
RUN make ui
RUN make build
FROM ubuntu:19.10 as app
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,22 @@
# must be built from /dist directory
FROM ubuntu:18.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"]
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
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update && apt install -y python3 python-is-python3 python3-requests python3-requests-toolbelt python3-lxml python3-pip && pip3 install cloudscraper
FROM ubuntu:20.04 as app
run apt update && apt install -y python3 python-is-python3 python3-requests python3-requests-toolbelt python3-lxml python3-mechanicalsoup ffmpeg && rm -rf /var/lib/apt/lists/*
COPY --from=prep /stash /usr/bin/
COPY --from=prep /usr/local/lib/python3.8/dist-packages /usr/local/lib/python3.8/dist-packages
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/
FROM ubuntu:18.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
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999
CMD ["stash"]

View File

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

View File

@@ -1,4 +1,4 @@
FROM golang:1.11.5
FROM golang:1.13.15
LABEL maintainer="stashappdev@gmail.com"
@@ -9,7 +9,7 @@ ENV PACKR2_DOWNLOAD_URL=https://github.com/gobuffalo/packr/releases/download/v${
# Install tools
RUN apt-get update && apt-get install -y apt-transport-https
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash -
# prevent caching of the key
ADD https://dl.yarnpkg.com/debian/pubkey.gpg yarn.gpg
@@ -21,7 +21,7 @@ RUN apt-get update && \
apt-get install -y automake autogen \
libtool libxml2-dev uuid-dev libssl-dev bash \
patch make tar xz-utils bzip2 gzip sed cpio \
gcc-6-multilib g++-6-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
gcc-8-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
gcc-arm-linux-gnueabi libc-dev-armel-cross linux-libc-dev-armel-cross \
gcc-arm-linux-gnueabihf libc-dev-armhf-cross \
gcc-aarch64-linux-gnu libc-dev-arm64-cross \

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
FROM ubuntu:18.04 as prep
FROM ubuntu:20.04 as prep
LABEL MAINTAINER="https://discord.gg/Uz29ny"
RUN apt-get update && \
@@ -17,8 +17,11 @@ RUN curl --http1.1 -o /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/f
rm ffmpeg.tar.xz && \
mv /ffmpeg*/ /ffmpeg/
FROM ubuntu:18.04 as app
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

@@ -1,4 +1,4 @@
FROM ubuntu:18.04 as prep
FROM ubuntu:20.04 as prep
LABEL MAINTAINER="leopere [at] nixc [dot] us"
RUN apt-get update && \
@@ -17,8 +17,11 @@ RUN curl --http1.1 -o /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/f
rm ffmpeg.tar.xz && \
mv /ffmpeg*/ /ffmpeg/
FROM ubuntu:18.04 as app
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"]

48
go.mod
View File

@@ -1,39 +1,53 @@
module github.com/stashapp/stash
require (
github.com/99designs/gqlgen v0.9.0
github.com/99designs/gqlgen v0.12.2
github.com/Yamashou/gqlgenc v0.0.0-20200902035953-4dbef3551953
github.com/anacrolix/dms v1.2.2
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/chromedp/cdproto v0.0.0-20210622022015-fe1827b46b84
github.com/chromedp/chromedp v0.7.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/gobuffalo/logger v1.0.4 // indirect
github.com/gobuffalo/packr/v2 v2.8.1
github.com/golang-migrate/migrate/v4 v4.3.1
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.0
github.com/gorilla/websocket v1.4.2
github.com/h2non/filetype v1.0.8
github.com/inconshreveable/mousetrap v1.0.0 // indirect
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/karrick/godirwalk v1.16.1 // indirect
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/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
github.com/rs/cors v1.6.0
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
github.com/sirupsen/logrus v1.4.2
github.com/sirupsen/logrus v1.8.1
github.com/spf13/afero v1.2.0 // indirect
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.4.0
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.5.1
github.com/tidwall/gjson v1.6.0
github.com/vektah/gqlparser v1.1.2
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
gopkg.in/yaml.v2 v2.2.2
github.com/tidwall/gjson v1.8.1
github.com/tidwall/pretty v1.2.0 // indirect
github.com/vektah/gqlparser/v2 v2.0.1
github.com/vektra/mockery/v2 v2.2.1
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/tools v0.0.0-20200915031644-64986481280e // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.3.0
)
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
go 1.11
go 1.13

826
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,10 @@ struct_tag: gqlgen
models:
Gallery:
model: github.com/stashapp/stash/pkg/models.Gallery
Image:
model: github.com/stashapp/stash/pkg/models.Image
ImageFileType:
model: github.com/stashapp/stash/pkg/models.ImageFileType
Performer:
model: github.com/stashapp/stash/pkg/models.Performer
Scene:
@@ -48,3 +52,7 @@ models:
model: github.com/stashapp/stash/pkg/models.ScrapedMovie
ScrapedMovieStudio:
model: github.com/stashapp/stash/pkg/models.ScrapedMovieStudio
SavedFilter:
model: github.com/stashapp/stash/pkg/models.SavedFilter
StashID:
model: github.com/stashapp/stash/pkg/models.StashID

View File

@@ -1,10 +1,16 @@
fragment ConfigGeneralData on ConfigGeneralResult {
stashes
stashes {
path
excludeVideo
excludeImage
}
databasePath
generatedPath
cachePath
calculateMD5
videoFileNamingAlgorithm
parallelTasks
previewAudio
previewSegments
previewSegmentDuration
previewExcludeStart
@@ -12,6 +18,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
previewPreset
maxTranscodeSize
maxStreamingTranscodeSize
apiKey
username
password
maxSessionAge
@@ -19,12 +26,25 @@ fragment ConfigGeneralData on ConfigGeneralResult {
logOut
logLevel
logAccess
createGalleriesFromFolders
videoExtensions
imageExtensions
galleryExtensions
excludes
imageExcludes
customPerformerImageLocation
scraperUserAgent
scraperCertCheck
scraperCDPPath
stashBoxes {
name
endpoint
api_key
}
}
fragment ConfigInterfaceData on ConfigInterfaceResult {
menuItems
soundOnPreview
wallShowTitle
wallPlayback
@@ -34,6 +54,23 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
css
cssEnabled
language
slideshowDelay
handyKey
funscriptOffset
}
fragment ConfigDLNAData on ConfigDLNAResult {
serverName
enabled
whitelistedIPs
interfaces
}
fragment ConfigScrapingData on ConfigScrapingResult {
scraperUserAgent
scraperCertCheck
scraperCDPPath
excludeTagPatterns
}
fragment ConfigData on ConfigResult {
@@ -43,4 +80,10 @@ fragment ConfigData on ConfigResult {
interface {
...ConfigInterfaceData
}
dlna {
...ConfigDLNAData
}
scraping {
...ConfigScrapingData
}
}

View File

@@ -0,0 +1,6 @@
fragment SavedFilterData on SavedFilter {
id
mode
name
filter
}

View File

@@ -0,0 +1,44 @@
fragment SlimGalleryData on Gallery {
id
checksum
path
title
date
url
details
rating
organized
image_count
cover {
file {
size
width
height
}
paths {
thumbnail
}
}
studio {
id
name
image_path
}
tags {
id
name
}
performers {
id
name
gender
favorite
image_path
}
scenes {
id
title
path
}
}

View File

@@ -3,14 +3,28 @@ fragment GalleryData on Gallery {
checksum
path
title
files {
index
name
path
date
url
details
rating
organized
images {
...SlimImageData
}
scene {
id
title
path
cover {
...SlimImageData
}
studio {
...SlimStudioData
}
tags {
...SlimTagData
}
performers {
...PerformerData
}
scenes {
...SlimSceneData
}
}

View File

@@ -0,0 +1,45 @@
fragment SlimImageData on Image {
id
checksum
title
rating
organized
o_counter
path
file {
size
width
height
}
paths {
thumbnail
image
}
galleries {
id
path
title
}
studio {
id
name
image_path
}
tags {
id
name
}
performers {
id
name
gender
favorite
image_path
}
}

View File

@@ -0,0 +1,36 @@
fragment ImageData on Image {
id
checksum
title
rating
organized
o_counter
path
file {
size
width
height
}
paths {
thumbnail
image
}
galleries {
...GalleryData
}
studio {
...SlimStudioData
}
tags {
...SlimTagData
}
performers {
...PerformerData
}
}

View File

@@ -0,0 +1,10 @@
fragment JobData on Job {
id
status
subTasks
description
progress
startTime
endTime
addTime
}

View File

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

View File

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

View File

@@ -20,4 +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

@@ -12,10 +12,12 @@ fragment SceneMarkerData on SceneMarker {
primary_tag {
id
name
aliases
}
tags {
id
name
aliases
}
}

View File

@@ -8,7 +8,10 @@ fragment SlimSceneData on Scene {
date
rating
o_counter
organized
path
phash
interactive
file {
size
@@ -28,6 +31,8 @@ fragment SlimSceneData on Scene {
webp
vtt
chapters_vtt
sprite
funscript
}
scene_markers {
@@ -36,7 +41,7 @@ fragment SlimSceneData on Scene {
seconds
}
gallery {
galleries {
id
path
title
@@ -65,7 +70,13 @@ fragment SlimSceneData on Scene {
performers {
id
name
gender
favorite
image_path
}
stash_ids {
endpoint
stash_id
}
}

View File

@@ -8,7 +8,10 @@ fragment SceneData on Scene {
date
rating
o_counter
organized
path
phash
interactive
file {
size
@@ -28,18 +31,20 @@ fragment SceneData on Scene {
webp
vtt
chapters_vtt
sprite
funscript
}
scene_markers {
...SceneMarkerData
}
gallery {
...GalleryData
galleries {
...SlimGalleryData
}
studio {
...StudioData
...SlimStudioData
}
movies {
@@ -50,10 +55,15 @@ fragment SceneData on Scene {
}
tags {
...TagData
...SlimTagData
}
performers {
...PerformerData
}
stash_ids {
endpoint
stash_id
}
}

View File

@@ -15,7 +15,15 @@ fragment ScrapedPerformerData on ScrapedPerformer {
tattoos
piercings
aliases
tags {
...ScrapedSceneTagData
}
image
details
death_date
hair_color
weight
remote_site_id
}
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
@@ -36,6 +44,15 @@ fragment ScrapedScenePerformerData on ScrapedScenePerformer {
tattoos
piercings
aliases
tags {
...ScrapedSceneTagData
}
remote_site_id
images
details
death_date
hair_color
weight
}
fragment ScrapedMovieStudioData on ScrapedMovieStudio {
@@ -77,6 +94,7 @@ fragment ScrapedSceneStudioData on ScrapedSceneStudio {
stored_id
name
url
remote_site_id
}
fragment ScrapedSceneTagData on ScrapedSceneTag {
@@ -118,3 +136,72 @@ fragment ScrapedSceneData on ScrapedScene {
...ScrapedSceneMovieData
}
}
fragment ScrapedGalleryData on ScrapedGallery {
title
details
url
date
studio {
...ScrapedSceneStudioData
}
tags {
...ScrapedSceneTagData
}
performers {
...ScrapedScenePerformerData
}
}
fragment ScrapedStashBoxSceneData on ScrapedScene {
title
details
url
date
image
remote_site_id
duration
file {
size
duration
video_codec
audio_codec
width
height
framerate
bitrate
}
fingerprints {
hash
algorithm
duration
}
studio {
...ScrapedSceneStudioData
}
tags {
...ScrapedSceneTagData
}
performers {
...ScrapedScenePerformerData
}
movies {
...ScrapedSceneMovieData
}
}
fragment ScrapedStashBoxPerformerData on StashBoxPerformerQueryResult {
query
results {
...ScrapedScenePerformerData
}
}

View File

@@ -2,4 +2,13 @@ fragment SlimStudioData on Studio {
id
name
image_path
}
stash_ids {
endpoint
stash_id
}
parent_studio {
id
}
details
rating
}

View File

@@ -5,20 +5,23 @@ fragment StudioData on Studio {
url
parent_studio {
id
checksum
name
url
image_path
scene_count
}
child_studios {
id
checksum
name
url
image_path
scene_count
}
image_path
scene_count
image_count
gallery_count
stash_ids {
stash_id
endpoint
}
details
rating
}

View File

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

View File

@@ -1,7 +1,11 @@
fragment TagData on Tag {
id
name
aliases
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,20 @@ mutation ConfigureInterface($input: ConfigInterfaceInput!) {
configureInterface(input: $input) {
...ConfigInterfaceData
}
}
}
mutation ConfigureDLNA($input: ConfigDLNAInput!) {
configureDLNA(input: $input) {
...ConfigDLNAData
}
}
mutation ConfigureScraping($input: ConfigScrapingInput!) {
configureScraping(input: $input) {
...ConfigScrapingData
}
}
mutation GenerateAPIKey($input: GenerateAPIKeyInput!) {
generateAPIKey(input: $input)
}

View File

@@ -0,0 +1,15 @@
mutation EnableDLNA($input: EnableDLNAInput!) {
enableDLNA(input: $input)
}
mutation DisableDLNA($input: DisableDLNAInput!) {
disableDLNA(input: $input)
}
mutation AddTempDLNAIP($input: AddTempDLNAIPInput!) {
addTempDLNAIP(input: $input)
}
mutation RemoveTempDLNAIP($input: RemoveTempDLNAIPInput!) {
removeTempDLNAIP(input: $input)
}

View File

@@ -0,0 +1,13 @@
mutation SaveFilter($input: SaveFilterInput!) {
saveFilter(input: $input) {
...SavedFilterData
}
}
mutation DestroySavedFilter($input: DestroyFilterInput!) {
destroySavedFilter(input: $input)
}
mutation SetDefaultFilter($input: SetDefaultFilterInput!) {
setDefaultFilter(input: $input)
}

View File

@@ -0,0 +1,41 @@
mutation GalleryCreate(
$input: GalleryCreateInput!) {
galleryCreate(input: $input) {
...GalleryData
}
}
mutation GalleryUpdate(
$input: GalleryUpdateInput!) {
galleryUpdate(input: $input) {
...GalleryData
}
}
mutation BulkGalleryUpdate(
$input: BulkGalleryUpdateInput!) {
bulkGalleryUpdate(input: $input) {
...GalleryData
}
}
mutation GalleriesUpdate($input : [GalleryUpdateInput!]!) {
galleriesUpdate(input: $input) {
...GalleryData
}
}
mutation GalleryDestroy($ids: [ID!]!, $delete_file: Boolean, $delete_generated : Boolean) {
galleryDestroy(input: {ids: $ids, delete_file: $delete_file, delete_generated: $delete_generated})
}
mutation AddGalleryImages($gallery_id: ID!, $image_ids: [ID!]!) {
addGalleryImages(input: {gallery_id: $gallery_id, image_ids: $image_ids})
}
mutation RemoveGalleryImages($gallery_id: ID!, $image_ids: [ID!]!) {
removeGalleryImages(input: {gallery_id: $gallery_id, image_ids: $image_ids})
}

View File

@@ -0,0 +1,41 @@
mutation ImageUpdate(
$input: ImageUpdateInput!) {
imageUpdate(input: $input) {
...SlimImageData
}
}
mutation BulkImageUpdate(
$input: BulkImageUpdateInput!) {
bulkImageUpdate(input: $input) {
...SlimImageData
}
}
mutation ImagesUpdate($input : [ImageUpdateInput!]!) {
imagesUpdate(input: $input) {
...SlimImageData
}
}
mutation ImageIncrementO($id: ID!) {
imageIncrementO(id: $id)
}
mutation ImageDecrementO($id: ID!) {
imageDecrementO(id: $id)
}
mutation ImageResetO($id: ID!) {
imageResetO(id: $id)
}
mutation ImageDestroy($id: ID!, $delete_file: Boolean, $delete_generated : Boolean) {
imageDestroy(input: {id: $id, delete_file: $delete_file, delete_generated: $delete_generated})
}
mutation ImagesDestroy($ids: [ID!]!, $delete_file: Boolean, $delete_generated : Boolean) {
imagesDestroy(input: {ids: $ids, delete_file: $delete_file, delete_generated: $delete_generated})
}

View File

@@ -0,0 +1,7 @@
mutation StopJob($job_id: ID!) {
stopJob(job_id: $job_id)
}
mutation StopAllJobs {
stopAllJobs
}

View File

@@ -6,6 +6,14 @@ mutation MetadataExport {
metadataExport
}
mutation ExportObjects($input: ExportObjectsInput!) {
exportObjects(input: $input)
}
mutation ImportObjects($input: ImportObjectsInput!) {
importObjects(input: $input)
}
mutation MetadataScan($input: ScanMetadataInput!) {
metadataScan(input: $input)
}
@@ -18,14 +26,14 @@ mutation MetadataAutoTag($input: AutoTagMetadataInput!) {
metadataAutoTag(input: $input)
}
mutation MetadataClean {
metadataClean
mutation MetadataClean($input: CleanMetadataInput!) {
metadataClean(input: $input)
}
mutation MigrateHashNaming {
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,31 @@
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,
$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,
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,
$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,
image: $image
}) {
performerUpdate(input: $input) {
...PerformerData
}
}
mutation BulkPerformerUpdate(
$input: BulkPerformerUpdateInput!) {
bulkPerformerUpdate(input: $input) {
...PerformerData
}
}
mutation PerformerDestroy($id: ID!) {
performerDestroy(input: { id: $id })
}
}
mutation PerformersDestroy($ids: [ID!]!) {
performersDestroy(ids: $ids)
}

View File

@@ -1,60 +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!] = [],
$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,
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

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

View File

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

View File

@@ -1,5 +1,5 @@
mutation TagCreate($name: String!, $image: String) {
tagCreate(input: { name: $name, image: $image }) {
mutation TagCreate($input: TagCreateInput!) {
tagCreate(input: $input) {
...TagData
}
}
@@ -8,8 +8,18 @@ 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
}
}
}
mutation TagsMerge($source: [ID!]!, $destination: ID!) {
tagsMerge(input: { source: $source, destination: $destination }) {
...TagData
}
}

View File

@@ -0,0 +1,11 @@
query DLNAStatus {
dlnaStatus {
running
until
recentIPAddresses
allowedIPAddresses {
ipAddress
until
}
}
}

View File

@@ -0,0 +1,11 @@
query FindSavedFilters($mode: FilterMode!) {
findSavedFilters(mode: $mode) {
...SavedFilterData
}
}
query FindDefaultFilter($mode: FilterMode!) {
findDefaultFilter(mode: $mode) {
...SavedFilterData
}
}

View File

@@ -2,7 +2,7 @@ query FindGalleries($filter: FindFilterType, $gallery_filter: GalleryFilterType)
findGalleries(gallery_filter: $gallery_filter, filter: $filter) {
count
galleries {
...GalleryData
...SlimGalleryData
}
}
}
@@ -11,4 +11,4 @@ query FindGallery($id: ID!) {
findGallery(id: $id) {
...GalleryData
}
}
}

View File

@@ -0,0 +1,14 @@
query FindImages($filter: FindFilterType, $image_filter: ImageFilterType, $image_ids: [Int!]) {
findImages(filter: $filter, image_filter: $image_filter, image_ids: $image_ids) {
count
images {
...SlimImageData
}
}
}
query FindImage($id: ID!, $checksum: String) {
findImage(id: $id, checksum: $checksum) {
...ImageData
}
}

View File

@@ -0,0 +1,11 @@
query JobQueue {
jobQueue {
...JobData
}
}
query FindJob($input: FindJobInput!) {
findJob(input: $input) {
...JobData
}
}

View File

@@ -13,40 +13,37 @@ 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
aliases
}
}
query Stats {
stats {
scene_count,
scene_size_count,
scenes_size,
scenes_duration,
image_count,
images_size,
gallery_count,
performer_count,
studio_count,

View File

@@ -11,4 +11,4 @@ query FindPerformer($id: ID!) {
findPerformer(id: $id) {
...PerformerData
}
}
}

View File

@@ -10,6 +10,12 @@ query Plugins {
name
description
}
hooks {
name
description
hooks
}
}
}

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

@@ -20,6 +20,17 @@ query ListSceneScrapers {
}
}
query ListGalleryScrapers {
listGalleryScrapers {
id
name
gallery {
urls
supported_scrapes
}
}
}
query ListMovieScrapers {
listMovieScrapers {
id
@@ -61,8 +72,32 @@ query ScrapeSceneURL($url: String!) {
}
}
query ScrapeGallery($scraper_id: ID!, $gallery: GalleryUpdateInput!) {
scrapeGallery(scraper_id: $scraper_id, gallery: $gallery) {
...ScrapedGalleryData
}
}
query ScrapeGalleryURL($url: String!) {
scrapeGalleryURL(url: $url) {
...ScrapedGalleryData
}
}
query ScrapeMovieURL($url: String!) {
scrapeMovieURL(url: $url) {
...ScrapedMovieData
}
}
query QueryStashBoxScene($input: StashBoxSceneQueryInput!) {
queryStashBoxScene(input: $input) {
...ScrapedStashBoxSceneData
}
}
query QueryStashBoxPerformer($input: StashBoxPerformerQueryInput!) {
queryStashBoxPerformer(input: $input) {
...ScrapedStashBoxPerformerData
}
}

View File

@@ -1,7 +1,9 @@
query JobStatus {
jobStatus {
progress
query SystemStatus {
systemStatus {
databaseSchema
databasePath
appSchema
status
message
configPath
}
}

View File

@@ -11,4 +11,4 @@ query FindStudio($id: ID!) {
findStudio(id: $id) {
...StudioData
}
}
}

View File

@@ -1,8 +1,13 @@
subscription MetadataUpdate {
metadataUpdate {
progress
status
message
subscription JobsSubscribe {
jobsSubscribe {
type
job {
id
status
subTasks
description
progress
}
}
}
@@ -10,4 +15,8 @@ subscription LoggingSubscribe {
loggingSubscribe {
...LogEntryData
}
}
subscription ScanCompleteSubscribe {
scanCompleteSubscribe
}

View File

@@ -1,5 +1,9 @@
"""The query root for this schema"""
type Query {
# Filters
findSavedFilters(mode: FilterMode!): [SavedFilter!]!
findDefaultFilter(mode: FilterMode!): SavedFilter
"""Find a scene by ID or Checksum"""
findScene(id: ID, checksum: String): Scene
findSceneByHash(input: SceneHashInput!): Scene
@@ -9,6 +13,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!]!
@@ -17,6 +24,11 @@ type Query {
"""A function which queries SceneMarker objects"""
findSceneMarkers(scene_marker_filter: SceneMarkerFilterType filter: FindFilterType): FindSceneMarkersResultType!
findImage(id: ID, checksum: String): Image
"""A function which queries Scene objects"""
findImages(image_filter: ImageFilterType, image_ids: [Int!], filter: FindFilterType): FindImagesResultType!
"""Find a performer by ID"""
findPerformer(id: ID!): Performer
"""A function which queries Performer objects"""
@@ -45,8 +57,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"""
@@ -59,6 +69,7 @@ type Query {
"""List available scrapers"""
listPerformerScrapers: [Scraper!]!
listSceneScrapers: [Scraper!]!
listGalleryScrapers: [Scraper!]!
listMovieScrapers: [Scraper!]!
"""Scrape a list of performers based on name"""
@@ -71,6 +82,10 @@ type Query {
scrapeScene(scraper_id: ID!, scene: SceneUpdateInput!): ScrapedScene
"""Scrapes a complete performer record based on a URL"""
scrapeSceneURL(url: String!): ScrapedScene
"""Scrapes a complete gallery record based on an existing gallery"""
scrapeGallery(scraper_id: ID!, gallery: GalleryUpdateInput!): ScrapedGallery
"""Scrapes a complete gallery record based on a URL"""
scrapeGalleryURL(url: String!): ScrapedGallery
"""Scrapes a complete movie record based on a URL"""
scrapeMovieURL(url: String!): ScrapedMovie
@@ -79,22 +94,30 @@ type Query {
"""Scrape a list of performers from a query"""
scrapeFreeonesPerformerList(query: String!): [String!]!
"""Query StashBox for scenes"""
queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]!
queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]!
# Plugins
"""List loaded plugins"""
plugins: [Plugin!]
"""List available plugin operations"""
pluginTasks: [PluginTask!]
# Config
"""Returns the current, complete configuration"""
configuration: ConfigResult!
"""Returns an array of paths for the given path"""
directory(path: String): Directory!
# Metadata
# System status
systemStatus: SystemStatus!
jobStatus: MetadataUpdateStatus!
# Job status
jobQueue: [Job!]
findJob(input: FindJobInput!): Job
dlnaStatus: DLNAStatus!
# Get everything
@@ -105,11 +128,6 @@ type Query {
# Get everything with minimal metadata
allPerformersSlim: [Performer!]!
allStudiosSlim: [Studio!]!
allMoviesSlim: [Movie!]!
allTagsSlim: [Tag!]!
# Version
version: Version!
@@ -118,6 +136,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!
@@ -138,56 +159,121 @@ type Mutation {
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean!
imageUpdate(input: ImageUpdateInput!): Image
bulkImageUpdate(input: BulkImageUpdateInput!): [Image!]
imageDestroy(input: ImageDestroyInput!): Boolean!
imagesDestroy(input: ImagesDestroyInput!): Boolean!
imagesUpdate(input: [ImageUpdateInput!]!): [Image]
"""Increments the o-counter for an image. Returns the new value"""
imageIncrementO(id: ID!): Int!
"""Decrements the o-counter for an image. Returns the new value"""
imageDecrementO(id: ID!): Int!
"""Resets the o-counter for a image to 0. Returns the new value"""
imageResetO(id: ID!): Int!
galleryCreate(input: GalleryCreateInput!): Gallery
galleryUpdate(input: GalleryUpdateInput!): Gallery
bulkGalleryUpdate(input: BulkGalleryUpdateInput!): [Gallery!]
galleryDestroy(input: GalleryDestroyInput!): Boolean!
galleriesUpdate(input: [GalleryUpdateInput!]!): [Gallery]
addGalleryImages(input: GalleryAddInput!): Boolean!
removeGalleryImages(input: GalleryRemoveInput!): Boolean!
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!
tagsMerge(input: TagsMergeInput!): Tag
# Saved filters
saveFilter(input: SaveFilterInput!): SavedFilter!
destroySavedFilter(input: DestroyFilterInput!): Boolean!
setDefaultFilter(input: SetDefaultFilterInput!): Boolean!
"""Change general configuration options"""
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
configureDLNA(input: ConfigDLNAInput!): ConfigDLNAResult!
configureScraping(input: ConfigScrapingInput!): ConfigScrapingResult!
"""Start an import. Returns the job ID"""
metadataImport: String!
"""Start an export. Returns the job ID"""
metadataExport: String!
"""Generate and set (or clear) API key"""
generateAPIKey(input: GenerateAPIKeyInput!): String!
"""Returns a link to download the result"""
exportObjects(input: ExportObjectsInput!): String
"""Performs an incremental import. Returns the job ID"""
importObjects(input: ImportObjectsInput!): ID!
"""Start an full import. Completely wipes the database and imports from the metadata directory. Returns the job ID"""
metadataImport: ID!
"""Start a full export. Outputs to the metadata directory. Returns the job ID"""
metadataExport: ID!
"""Start a scan. Returns the job ID"""
metadataScan(input: ScanMetadataInput!): String!
metadataScan(input: ScanMetadataInput!): ID!
"""Start generating content. Returns the job ID"""
metadataGenerate(input: GenerateMetadataInput!): String!
metadataGenerate(input: GenerateMetadataInput!): ID!
"""Start auto-tagging. Returns the job ID"""
metadataAutoTag(input: AutoTagMetadataInput!): String!
metadataAutoTag(input: AutoTagMetadataInput!): ID!
"""Clean metadata. Returns the job ID"""
metadataClean: String!
metadataClean(input: CleanMetadataInput!): ID!
"""Migrate generated files for the current hash naming"""
migrateHashNaming: String!
migrateHashNaming: ID!
"""Reload scrapers"""
reloadScrapers: Boolean!
"""Run plugin task. Returns the job ID"""
runPluginTask(plugin_id: ID!, task_name: String!, args: [PluginArgInput!]): String!
runPluginTask(plugin_id: ID!, task_name: String!, args: [PluginArgInput!]): ID!
reloadPlugins: Boolean!
stopJob: Boolean!
stopJob(job_id: ID!): Boolean!
stopAllJobs: Boolean!
"""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!
"""Enables DLNA for an optional duration. Has no effect if DLNA is enabled by default"""
enableDLNA(input: EnableDLNAInput!): Boolean!
"""Disables DLNA for an optional duration. Has no effect if DLNA is disabled by default"""
disableDLNA(input: DisableDLNAInput!): Boolean!
"""Enables an IP address for DLNA for an optional duration"""
addTempDLNAIP(input: AddTempDLNAIPInput!): Boolean!
"""Removes an IP address from the temporary DLNA whitelist"""
removeTempDLNAIP(input: RemoveTempDLNAIPInput!): Boolean!
}
type Subscription {
"""Update from the metadata manager"""
metadataUpdate: MetadataUpdateStatus!
jobsSubscribe: JobStatusUpdate!
loggingSubscribe: [LogEntry!]!
scanCompleteSubscribe: Boolean!
}
schema {

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
@@ -24,7 +34,7 @@ enum HashAlgorithm {
input ConfigGeneralInput {
"""Array of file paths to content"""
stashes: [String!]
stashes: [StashConfigInput!]
"""Path to the SQLite database"""
databasePath: String
"""Path to generated files"""
@@ -35,6 +45,10 @@ 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
"""Include audio stream in previews"""
previewAudio: Boolean!
"""Number of segments in a preview file"""
previewSegments: Int
"""Preview segment duration, in seconds"""
@@ -63,27 +77,51 @@ input ConfigGeneralInput {
logLevel: String!
"""Whether to log http access"""
logAccess: Boolean!
"""Array of file regexp to exclude from Scan"""
"""True if galleries should be created from folders with images"""
createGalleriesFromFolders: Boolean!
"""Array of video file extensions"""
videoExtensions: [String!]
"""Array of image file extensions"""
imageExtensions: [String!]
"""Array of gallery zip file extensions"""
galleryExtensions: [String!]
"""Array of file regexp to exclude from Video Scans"""
excludes: [String!]
"""Array of file regexp to exclude from Image Scans"""
imageExcludes: [String!]
"""Custom Performer Image Location"""
customPerformerImageLocation: String
"""Scraper user agent string"""
scraperUserAgent: String
scraperUserAgent: String @deprecated(reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead")
"""Scraper CDP path. Path to chrome executable or remote address"""
scraperCDPPath: String
scraperCDPPath: String @deprecated(reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead")
"""Whether the scraper should check for invalid certificates"""
scraperCertCheck: Boolean @deprecated(reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead")
"""Stash-box instances used for tagging"""
stashBoxes: [StashBoxInput!]!
}
type ConfigGeneralResult {
"""Array of file paths to content"""
stashes: [String!]!
stashes: [StashConfig!]!
"""Path to the SQLite database"""
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!
"""Include audio stream in previews"""
previewAudio: Boolean!
"""Number of segments in a preview file"""
previewSegments: Int!
"""Preview segment duration, in seconds"""
@@ -98,6 +136,8 @@ type ConfigGeneralResult {
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""API Key"""
apiKey: String!
"""Username"""
username: String!
"""Password"""
@@ -112,15 +152,33 @@ type ConfigGeneralResult {
logLevel: String!
"""Whether to log http access"""
logAccess: Boolean!
"""Array of file regexp to exclude from Scan"""
"""Array of video file extensions"""
videoExtensions: [String!]!
"""Array of image file extensions"""
imageExtensions: [String!]!
"""Array of gallery zip file extensions"""
galleryExtensions: [String!]!
"""True if galleries should be created from folders with images"""
createGalleriesFromFolders: Boolean!
"""Array of file regexp to exclude from Video Scans"""
excludes: [String!]!
"""Array of file regexp to exclude from Image Scans"""
imageExcludes: [String!]!
"""Custom Performer Image Location"""
customPerformerImageLocation: String
"""Scraper user agent string"""
scraperUserAgent: String
scraperUserAgent: String @deprecated(reason: "use ConfigResult.scraping instead")
"""Scraper CDP path. Path to chrome executable or remote address"""
scraperCDPPath: String
scraperCDPPath: String @deprecated(reason: "use ConfigResult.scraping instead")
"""Whether the scraper should check for invalid certificates"""
scraperCertCheck: Boolean! @deprecated(reason: "use ConfigResult.scraping instead")
"""Stash-box instances used for tagging"""
stashBoxes: [StashBox!]!
}
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"""
@@ -136,10 +194,19 @@ input ConfigInterfaceInput {
"""Custom CSS"""
css: String
cssEnabled: Boolean
"""Interface language"""
language: String
"""Slideshow Delay"""
slideshowDelay: Int
"""Handy Connection Key"""
handyKey: String
"""Funscript Time Offset"""
funscriptOffset: 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"""
@@ -150,19 +217,69 @@ 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
"""Handy Connection Key"""
handyKey: String
"""Funscript Time Offset"""
funscriptOffset: Int
}
input ConfigDLNAInput {
serverName: String
"""True if DLNA service should be enabled by default"""
enabled: Boolean
"""List of IPs whitelisted for DLNA service"""
whitelistedIPs: [String!]
"""List of interfaces to run DLNA on. Empty for all"""
interfaces: [String!]
}
type ConfigDLNAResult {
serverName: String!
"""True if DLNA service should be enabled by default"""
enabled: Boolean!
"""List of IPs whitelisted for DLNA service"""
whitelistedIPs: [String!]!
"""List of interfaces to run DLNA on. Empty for all"""
interfaces: [String!]!
}
input ConfigScrapingInput {
"""Scraper user agent string"""
scraperUserAgent: String
"""Scraper CDP path. Path to chrome executable or remote address"""
scraperCDPPath: String
"""Whether the scraper should check for invalid certificates"""
scraperCertCheck: Boolean!
"""Tags blacklist during scraping"""
excludeTagPatterns: [String!]
}
type ConfigScrapingResult {
"""Scraper user agent string"""
scraperUserAgent: String
"""Scraper CDP path. Path to chrome executable or remote address"""
scraperCDPPath: String
"""Whether the scraper should check for invalid certificates"""
scraperCertCheck: Boolean!
"""Tags blacklist during scraping"""
excludeTagPatterns: [String!]!
}
"""All configuration settings"""
type ConfigResult {
general: ConfigGeneralResult!
interface: ConfigInterfaceResult!
dlna: ConfigDLNAResult!
scraping: ConfigScrapingResult!
}
"""Directory structure of a path"""
@@ -171,3 +288,20 @@ type Directory {
parent: String
directories: [String!]!
}
"""Stash configuration details"""
input StashConfigInput {
path: String!
excludeVideo: Boolean!
excludeImage: Boolean!
}
type StashConfig {
path: String!
excludeVideo: Boolean!
excludeImage: Boolean!
}
input GenerateAPIKeyInput {
clear: Boolean
}

View File

@@ -0,0 +1,35 @@
type DLNAIP {
ipAddress: String!
"""Time until IP will be no longer allowed/disallowed"""
until: Time
}
type DLNAStatus {
running: Boolean!
"""If not currently running, time until it will be started. If running, time until it will be stopped"""
until: Time
recentIPAddresses: [String!]!
allowedIPAddresses: [DLNAIP!]!
}
input EnableDLNAInput {
"""Duration to enable, in minutes. 0 or null for indefinite."""
duration: Int
}
input DisableDLNAInput {
"""Duration to enable, in minutes. 0 or null for indefinite."""
duration: Int
}
input AddTempDLNAIPInput {
address: String!
"""Duration to enable, in minutes. 0 or null for indefinite."""
duration: Int
}
input RemoveTempDLNAIPInput {
address: String!
}

View File

@@ -6,20 +6,41 @@ 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 ResolutionCriterionInput {
value: ResolutionEnum!
modifier: CriterionModifier!
}
input PerformerFilterType {
AND: PerformerFilterType
OR: PerformerFilterType
NOT: PerformerFilterType
name: StringCriterionInput
details: StringCriterionInput
"""Filter by favorite"""
filter_favorites: Boolean
"""Filter by birth year"""
@@ -38,7 +59,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,6 +71,30 @@ 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: 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
"""Filter by studios where performer appears in scene/image/gallery"""
studios: HierarchicalMultiCriterionInput
}
input SceneMarkerFilterType {
@@ -64,12 +109,29 @@ input SceneMarkerFilterType {
}
input SceneFilterType {
AND: SceneFilterType
OR: SceneFilterType
NOT: SceneFilterType
title: StringCriterionInput
details: StringCriterionInput
"""Filter by file oshash"""
oshash: StringCriterionInput
"""Filter by file checksum"""
checksum: StringCriterionInput
"""Filter by file phash"""
phash: StringCriterionInput
"""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"""
resolution: ResolutionEnum
resolution: ResolutionCriterionInput
"""Filter by duration (in seconds)"""
duration: IntCriterionInput
"""Filter to only include scenes which have markers. `true` or `false`"""
@@ -77,45 +139,173 @@ input SceneFilterType {
"""Filter to only include scenes missing this property"""
is_missing: String
"""Filter to only include scenes with this studio"""
studios: MultiCriterionInput
studios: HierarchicalMultiCriterionInput
"""Filter to only include scenes with this movie"""
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: StringCriterionInput
"""Filter by url"""
url: StringCriterionInput
"""Filter by interactive"""
interactive: Boolean
}
input MovieFilterType {
name: StringCriterionInput
director: StringCriterionInput
synopsis: StringCriterionInput
"""Filter by duration (in seconds)"""
duration: IntCriterionInput
"""Filter by rating"""
rating: IntCriterionInput
"""Filter to only include movies with this studio"""
studios: MultiCriterionInput
studios: HierarchicalMultiCriterionInput
"""Filter to only include movies missing this property"""
is_missing: String
"""Filter by url"""
url: StringCriterionInput
}
input StudioFilterType {
name: StringCriterionInput
details: StringCriterionInput
"""Filter to only include studios with this parent studio"""
parents: MultiCriterionInput
"""Filter by StashID"""
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
title: StringCriterionInput
details: StringCriterionInput
"""Filter by file checksum"""
checksum: StringCriterionInput
"""Filter by path"""
path: StringCriterionInput
"""Filter to only include galleries missing this property"""
is_missing: String
"""Filter to include/exclude galleries that were created from zip"""
is_zip: Boolean
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by organized"""
organized: Boolean
"""Filter by average image resolution"""
average_resolution: ResolutionCriterionInput
"""Filter to only include galleries with this studio"""
studios: HierarchicalMultiCriterionInput
"""Filter to only include galleries with these tags"""
tags: MultiCriterionInput
"""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 by tag name"""
name: StringCriterionInput
"""Filter by tag aliases"""
aliases: StringCriterionInput
"""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
title: StringCriterionInput
"""Filter by file checksum"""
checksum: StringCriterionInput
"""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"""
resolution: ResolutionCriterionInput
"""Filter to only include images missing this property"""
is_missing: String
"""Filter to only include images with this studio"""
studios: HierarchicalMultiCriterionInput
"""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
}
enum CriterionModifier {
"""="""
EQUALS,
@@ -133,6 +323,14 @@ enum CriterionModifier {
INCLUDES_ALL,
INCLUDES,
EXCLUDES,
"""MATCHES REGEX"""
MATCHES_REGEX,
"""NOT MATCHES REGEX"""
NOT_MATCHES_REGEX,
""">= AND <="""
BETWEEN,
"""< OR >"""
NOT_BETWEEN,
}
input StringCriterionInput {
@@ -142,6 +340,7 @@ input StringCriterionInput {
input IntCriterionInput {
value: Int!
value2: Int
modifier: CriterionModifier!
}
@@ -153,4 +352,48 @@ input MultiCriterionInput {
input GenderCriterionInput {
value: GenderEnum
modifier: CriterionModifier!
}
}
input HierarchicalMultiCriterionInput {
value: [ID!]
modifier: CriterionModifier!
depth: Int!
}
enum FilterMode {
SCENES,
PERFORMERS,
STUDIOS,
GALLERIES,
SCENE_MARKERS,
MOVIES,
TAGS,
IMAGES,
}
type SavedFilter {
id: ID!
mode: FilterMode!
name: String!
"""JSON-encoded filter string"""
filter: String!
}
input SaveFilterInput {
"""provide ID to overwrite existing filter"""
id: ID
mode: FilterMode!
name: String!
"""JSON-encoded filter string"""
filter: String!
}
input DestroyFilterInput {
id: ID!
}
input SetDefaultFilterInput {
mode: FilterMode!
"""JSON-encoded filter string - null to clear"""
filter: String
}

View File

@@ -2,12 +2,26 @@
type Gallery {
id: ID!
checksum: String!
path: String!
path: String
title: String
scene: Scene
url: String
date: String
details: String
rating: Int
organized: Boolean!
created_at: Time!
updated_at: Time!
file_mod_time: Time
"""The files in the gallery"""
files: [GalleryFilesType!]! # Resolver
scenes: [Scene!]!
studio: Studio
image_count: Int!
tags: [Tag!]!
performers: [Performer!]!
"""The images in the gallery"""
images: [Image!]! # Resolver
cover: Image
}
type GalleryFilesType {
@@ -16,7 +30,65 @@ type GalleryFilesType {
path: String
}
input GalleryCreateInput {
title: String!
url: String
date: String
details: String
rating: Int
organized: Boolean
scene_ids: [ID!]
studio_id: ID
tag_ids: [ID!]
performer_ids: [ID!]
}
input GalleryUpdateInput {
clientMutationId: String
id: ID!
title: String
url: String
date: String
details: String
rating: Int
organized: Boolean
scene_ids: [ID!]
studio_id: ID
tag_ids: [ID!]
performer_ids: [ID!]
}
input BulkGalleryUpdateInput {
clientMutationId: String
ids: [ID!]
url: String
date: String
details: String
rating: Int
organized: Boolean
scene_ids: BulkUpdateIds
studio_id: ID
tag_ids: BulkUpdateIds
performer_ids: BulkUpdateIds
}
input GalleryDestroyInput {
ids: [ID!]!
delete_file: Boolean
delete_generated: Boolean
}
type FindGalleriesResultType {
count: Int!
galleries: [Gallery!]!
}
}
input GalleryAddInput {
gallery_id: ID!
image_ids: [ID!]!
}
input GalleryRemoveInput {
gallery_id: ID!
image_ids: [ID!]!
}

View File

@@ -0,0 +1,74 @@
type Image {
id: ID!
checksum: String
title: String
rating: Int
o_counter: Int
organized: Boolean!
path: String!
created_at: Time!
updated_at: Time!
file_mod_time: Time
file: ImageFileType! # Resolver
paths: ImagePathsType! # Resolver
galleries: [Gallery!]!
studio: Studio
tags: [Tag!]!
performers: [Performer!]!
}
type ImageFileType {
size: Int
width: Int
height: Int
}
type ImagePathsType {
thumbnail: String # Resolver
image: String # Resolver
}
input ImageUpdateInput {
clientMutationId: String
id: ID!
title: String
rating: Int
organized: Boolean
studio_id: ID
performer_ids: [ID!]
tag_ids: [ID!]
gallery_ids: [ID!]
}
input BulkImageUpdateInput {
clientMutationId: String
ids: [ID!]
title: String
rating: Int
organized: Boolean
studio_id: ID
performer_ids: BulkUpdateIds
tag_ids: BulkUpdateIds
gallery_ids: BulkUpdateIds
}
input ImageDestroyInput {
id: ID!
delete_file: Boolean
delete_generated: Boolean
}
input ImagesDestroyInput {
ids: [ID!]!
delete_file: Boolean
delete_generated: Boolean
}
type FindImagesResultType {
count: Int!
images: [Image!]!
}

View File

@@ -0,0 +1,33 @@
enum JobStatus {
READY
RUNNING
FINISHED
STOPPING
CANCELLED
}
type Job {
id: ID!
status: JobStatus!
subTasks: [String!]
description: String!
progress: Float
startTime: Time
endTime: Time
addTime: Time!
}
input FindJobInput {
id: ID!
}
enum JobStatusUpdateType {
ADD
REMOVE
UPDATE
}
type JobStatusUpdate {
type: JobStatusUpdateType!
job: Job!
}

View File

@@ -1,3 +1,5 @@
scalar Upload
input GenerateMetadataInput {
sprites: Boolean!
previews: Boolean!
@@ -5,15 +7,12 @@ input GenerateMetadataInput {
previewOptions: GeneratePreviewOptionsInput
markers: Boolean!
transcodes: Boolean!
"""gallery thumbnails for cache usage"""
thumbnails: Boolean!
phashes: Boolean!
"""scene ids to generate for"""
sceneIDs: [ID!]
"""marker ids to generate for"""
markerIDs: [ID!]
"""gallery ids to generate for"""
galleryIDs: [ID!]
"""overwrite existing media"""
overwrite: Boolean
@@ -33,10 +32,29 @@ input GeneratePreviewOptionsInput {
}
input ScanMetadataInput {
useFileMetadata: Boolean!
paths: [String!]
"""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"""
@@ -45,8 +63,58 @@ input AutoTagMetadataInput {
tags: [String!]
}
type MetadataUpdateStatus {
progress: Float!
status: String!
message: String!
input ExportObjectTypeInput {
ids: [String!]
all: Boolean
}
input ExportObjectsInput {
scenes: ExportObjectTypeInput
images: ExportObjectTypeInput
studios: ExportObjectTypeInput
performers: ExportObjectTypeInput
tags: ExportObjectTypeInput
movies: ExportObjectTypeInput
galleries: ExportObjectTypeInput
includeDependencies: Boolean
}
enum ImportDuplicateEnum {
IGNORE
OVERWRITE
FAIL
}
enum ImportMissingRefEnum {
IGNORE
FAIL
CREATE
}
input ImportObjectsInput {
file: Upload!
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

@@ -11,6 +11,8 @@ type Movie {
director: String
synopsis: String
url: String
created_at: Time!
updated_at: Time!
front_image_path: String # Resolver
back_image_path: String # Resolver
@@ -28,8 +30,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 +47,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,14 +27,25 @@ 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
created_at: Time!
updated_at: Time!
}
input PerformerCreateInput {
name: String
name: String!
url: String
gender: GenderEnum
birthdate: String
@@ -51,8 +62,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 {
@@ -74,8 +92,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 {
@@ -85,4 +137,4 @@ input PerformerDestroyInput {
type FindPerformersResultType {
count: Int!
performers: [Performer!]!
}
}

View File

@@ -7,6 +7,7 @@ type Plugin {
version: String
tasks: [PluginTask!]
hooks: [PluginHook!]
}
type PluginTask {
@@ -15,6 +16,13 @@ type PluginTask {
plugin: Plugin!
}
type PluginHook {
name: String!
description: String
hooks: [String!]
plugin: Plugin!
}
type PluginResult {
error: String
result: String

View File

@@ -5,6 +5,8 @@ type SceneMarker {
seconds: Float!
primary_tag: Tag!
tags: [Tag!]!
created_at: Time!
updated_at: Time!
"""The path to stream this marker"""
stream: String! # Resolver

View File

@@ -16,6 +16,8 @@ type ScenePathsType {
webp: String # Resolver
vtt: String # Resolver
chapters_vtt: String # Resolver
sprite: String # Resolver
funscript: String # Resolver
}
type SceneMovie {
@@ -32,18 +34,25 @@ type Scene {
url: String
date: String
rating: Int
organized: Boolean!
o_counter: Int
path: String!
phash: String
interactive: Boolean!
created_at: Time!
updated_at: Time!
file_mod_time: Time
file: SceneFileType! # Resolver
paths: ScenePathsType! # Resolver
scene_markers: [SceneMarker!]!
gallery: Gallery
galleries: [Gallery!]!
studio: Studio
movies: [SceneMovie!]!
tags: [Tag!]!
performers: [Performer!]!
stash_ids: [StashID!]!
}
input SceneMovieInput {
@@ -59,13 +68,15 @@ 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!]
}
enum BulkUpdateIdMode {
@@ -87,8 +98,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
}
@@ -113,7 +125,8 @@ type FindScenesResultType {
input SceneParserInput {
ignoreWords: [String!],
whitespaceCharacters: String,
capitalizeTitle: Boolean
capitalizeTitle: Boolean,
ignoreOrganized: Boolean
}
type SceneMovieID {
@@ -129,7 +142,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,16 @@ 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
remote_site_id: String
}
input ScrapedPerformerInput {
@@ -39,5 +46,11 @@ 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
remote_site_id: String
}

View File

@@ -20,11 +20,12 @@ type Scraper {
performer: ScraperSpec
"""Details for scene scraper"""
scene: ScraperSpec
"""Details for gallery scraper"""
gallery: ScraperSpec
"""Details for movie scraper"""
movie: ScraperSpec
}
type ScrapedScenePerformer {
"""Set if performer matched"""
stored_id: ID
@@ -44,6 +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 {
@@ -64,6 +73,8 @@ type ScrapedSceneStudio {
stored_id: ID
name: String!
url: String
remote_site_id: String
}
type ScrapedSceneTag {
@@ -78,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
@@ -87,4 +98,56 @@ type ScrapedScene {
tags: [ScrapedSceneTag!]
performers: [ScrapedScenePerformer!]
movies: [ScrapedSceneMovie!]
remote_site_id: String
duration: Int
fingerprints: [StashBoxFingerprint!]
}
type ScrapedGallery {
title: String
details: String
url: String
date: String
studio: ScrapedSceneStudio
tags: [ScrapedSceneTag!]
performers: [ScrapedScenePerformer!]
}
input StashBoxSceneQueryInput {
"""Index of the configured stash-box instance to use"""
stash_box_index: Int!
"""Instructs query by scene fingerprints"""
scene_ids: [ID!]
"""Query by query string"""
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

@@ -0,0 +1,26 @@
type StashBox {
endpoint: String!
api_key: String!
name: String!
}
input StashBoxInput {
endpoint: String!
api_key: String!
name: String!
}
type StashID {
endpoint: String!
stash_id: String!
}
input StashIDInput {
endpoint: String!
stash_id: String!
}
input StashBoxFingerprintSubmissionInput {
scene_ids: [String!]!
stash_box_index: Int!
}

View File

@@ -1,6 +1,9 @@
type StatsResultType {
scene_count: Int!
scene_size_count: String!
scenes_size: Float!
scenes_duration: Float!
image_count: Int!
images_size: Float!
gallery_count: Int!
performer_count: Int!
studio_count: Int!

View File

@@ -5,16 +5,27 @@ type Studio {
url: String
parent_studio: Studio
child_studios: [Studio!]!
image_path: String # Resolver
scene_count: Int # Resolver
image_count: Int # Resolver
gallery_count: Int # Resolver
stash_ids: [StashID!]!
rating: Int
details: String
created_at: Time!
updated_at: Time!
}
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 {
@@ -22,8 +33,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 {
@@ -33,4 +47,4 @@ input StudioDestroyInput {
type FindStudiosResultType {
count: Int!
studios: [Studio!]!
}
}

View File

@@ -1,24 +1,32 @@
type Tag {
id: ID!
name: String!
aliases: [String!]!
created_at: Time!
updated_at: Time!
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!
aliases: [String!]
"""This should be base64 encoded"""
"""This should be a URL or a base64 encoded data URL"""
image: String
}
input TagUpdateInput {
id: ID!
name: String!
name: String
aliases: [String!]
"""This should be base64 encoded"""
"""This should be a URL or a base64 encoded data URL"""
image: String
}
@@ -29,4 +37,9 @@ input TagDestroyInput {
type FindTagsResultType {
count: Int!
tags: [Tag!]!
}
}
input TagsMergeInput {
source: [ID!]!
destination: ID!
}

View File

@@ -0,0 +1,156 @@
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
urls {
...URLFragment
}
images {
...ImageFragment
}
}
fragment TagFragment on Tag {
name
id
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
urls {
...URLFragment
}
images {
...ImageFragment
}
birthdate {
...FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
...MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
...BodyModificationFragment
}
piercings {
...BodyModificationFragment
}
details
death_date {
...FuzzyDateFragment
}
weight
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
...PerformerFragment
}
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment SceneFragment on Scene {
id
title
details
duration
date
urls {
...URLFragment
}
images {
...ImageFragment
}
studio {
...StudioFragment
}
tags {
...TagFragment
}
performers {
...PerformerAppearanceFragment
}
fingerprints {
...FingerprintFragment
}
}
query FindSceneByFingerprint($fingerprint: FingerprintQueryInput!) {
findSceneByFingerprint(fingerprint: $fingerprint) {
...SceneFragment
}
}
query FindScenesByFingerprints($fingerprints: [String!]!) {
findScenesByFingerprints(fingerprints: $fingerprints) {
...SceneFragment
}
}
query SearchScene($term: String!) {
searchScene(term: $term) {
...SceneFragment
}
}
query SearchPerformer($term: String!) {
searchPerformer(term: $term) {
...PerformerFragment
}
}
query FindPerformerByID($id: ID!) {
findPerformer(id: $id) {
...PerformerFragment
}
}
mutation SubmitFingerprint($input: FingerprintSubmission!) {
submitFingerprint(input: $input)
}

22
main.go
View File

@@ -2,10 +2,13 @@
package main
import (
"os"
"os/signal"
"runtime/pprof"
"syscall"
"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,16 +16,17 @@ import (
func main() {
manager.Initialize()
// perform the post-migration for new databases
if database.Initialize(config.GetDatabasePath()) {
manager.GetInstance().PostMigrate()
}
api.Start()
// stop any profiling at exit
defer pprof.StopCPUProfile()
blockForever()
}
func blockForever() {
select {}
// handle signals
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
<-signals
}

View File

@@ -1,72 +0,0 @@
package api
import (
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager/paths"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
"io/ioutil"
)
type thumbBuffer struct {
path string
dir string
data []byte
}
func newCacheThumb(dir string, path string, data []byte) *thumbBuffer {
t := thumbBuffer{dir: dir, path: path, data: data}
return &t
}
var writeChan chan *thumbBuffer
var touchChan chan *string
func startThumbCache() { // TODO add extra wait, close chan code if/when stash gets a stop mode
writeChan = make(chan *thumbBuffer, 20)
go thumbnailCacheWriter()
}
//serialize file writes to avoid race conditions
func thumbnailCacheWriter() {
for thumb := range writeChan {
exists, _ := utils.FileExists(thumb.path)
if !exists {
err := utils.WriteFile(thumb.path, thumb.data)
if err != nil {
logger.Errorf("Write error for thumbnail %s: %s ", thumb.path, err)
}
}
}
}
// get thumbnail from cache, otherwise create it and store to cache
func cacheGthumb(gallery *models.Gallery, index int, width int) []byte {
thumbPath := paths.GetGthumbPath(gallery.Checksum, index, width)
exists, _ := utils.FileExists(thumbPath)
if exists { // if thumbnail exists in cache return that
content, err := ioutil.ReadFile(thumbPath)
if err == nil {
return content
} else {
logger.Errorf("Read Error for file %s : %s", thumbPath, err)
}
}
data := gallery.GetThumbnail(index, width)
thumbDir := paths.GetGthumbDir(gallery.Checksum)
t := newCacheThumb(thumbDir, thumbPath, data)
writeChan <- t // write the file to cache
return data
}
// create all thumbs for a given gallery
func CreateGthumbs(gallery *models.Gallery) {
count := gallery.ImageCount()
for i := 0; i < count; i++ {
cacheGthumb(gallery, i, models.DefaultGthumbWidth)
}
}

View File

@@ -0,0 +1,150 @@
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.GetFieldContext(ctx)
reqCtx := graphql.GetOperationContext(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) getFields() []string {
var ret []string
for k := range t.inputMap {
ret = append(ret, k)
}
return ret
}
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

@@ -5,11 +5,12 @@ package api
type key int
const (
galleryKey key = 0
performerKey key = 1
sceneKey key = 2
studioKey key = 3
movieKey key = 4
ContextUser key = 5
tagKey key = 6
galleryKey key = iota
performerKey
sceneKey
studioKey
movieKey
tagKey
downloadKey
imageKey
)

View File

@@ -1,49 +1,67 @@
package api
import (
"math/rand"
"strings"
"github.com/gobuffalo/packr/v2"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/utils"
)
var performerBox *packr.Box
var performerBoxMale *packr.Box
type imageBox struct {
box *packr.Box
files []string
}
func newImageBox(box *packr.Box) *imageBox {
return &imageBox{
box: box,
files: box.List(),
}
}
var performerBox *imageBox
var performerBoxMale *imageBox
var performerBoxCustom *imageBox
func initialiseImages() {
performerBox = packr.New("Performer Box", "../../static/performer")
performerBoxMale = packr.New("Male Performer Box", "../../static/performer_male")
performerBox = newImageBox(packr.New("Performer Box", "../../static/performer"))
performerBoxMale = newImageBox(packr.New("Male Performer Box", "../../static/performer_male"))
initialiseCustomImages()
}
func getRandomPerformerImage(gender string) ([]byte, error) {
var box *packr.Box
switch strings.ToUpper(gender) {
case "FEMALE":
box = performerBox
case "MALE":
box = performerBoxMale
default:
box = performerBox
func initialiseCustomImages() {
customPath := config.GetInstance().GetCustomPerformerImageLocation()
if customPath != "" {
logger.Debugf("Loading custom performer images from %s", customPath)
// We need to set performerBoxCustom at runtime, as this is a custom path, and store it in a pointer.
performerBoxCustom = newImageBox(packr.Folder(customPath))
} else {
performerBoxCustom = nil
}
imageFiles := box.List()
index := rand.Intn(len(imageFiles))
return box.Find(imageFiles[index])
}
func getRandomPerformerImageUsingName(name, gender string) ([]byte, error) {
var box *packr.Box
switch strings.ToUpper(gender) {
case "FEMALE":
box = performerBox
case "MALE":
box = performerBoxMale
default:
box = performerBox
func getRandomPerformerImageUsingName(name, gender, customPath string) ([]byte, error) {
var box *imageBox
// If we have a custom path, we should return a new box in the given path.
if performerBoxCustom != nil && len(performerBoxCustom.files) > 0 {
box = performerBoxCustom
}
imageFiles := box.List()
if box == nil {
switch strings.ToUpper(gender) {
case "FEMALE":
box = performerBox
case "MALE":
box = performerBoxMale
default:
box = performerBox
}
}
imageFiles := box.files
index := utils.IntFromString(name) % uint64(len(imageFiles))
return box.Find(imageFiles[index])
return box.box.Find(imageFiles[index])
}

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,19 @@ import (
"sort"
"strconv"
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
)
type Resolver struct{}
type hookExecutor interface {
ExecutePostHooks(ctx context.Context, id int, hookType plugin.HookTriggerEnum, input interface{}, inputFields []string)
}
type Resolver struct {
txnManager models.TransactionManager
hookExecutor hookExecutor
}
func (r *Resolver) Gallery() models.GalleryResolver {
return &galleryResolver{r}
@@ -27,6 +34,9 @@ func (r *Resolver) Query() models.QueryResolver {
func (r *Resolver) Scene() models.SceneResolver {
return &sceneResolver{r}
}
func (r *Resolver) Image() models.ImageResolver {
return &imageResolver{r}
}
func (r *Resolver) SceneMarker() models.SceneMarkerResolver {
return &sceneMarkerResolver{r}
}
@@ -67,6 +77,7 @@ type galleryResolver struct{ *Resolver }
type performerResolver struct{ *Resolver }
type sceneResolver struct{ *Resolver }
type sceneMarkerResolver struct{ *Resolver }
type imageResolver struct{ *Resolver }
type studioResolver struct{ *Resolver }
type movieResolver struct{ *Resolver }
type tagResolver struct{ *Resolver }
@@ -75,64 +86,86 @@ 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()
scenesSizeCount, _ := scenesQB.SizeCount()
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,
SceneSizeCount: scenesSizeCount,
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()
scenesDuration, _ := scenesQB.Duration()
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,
ScenesDuration: scenesDuration,
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) {
@@ -162,31 +195,38 @@ 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]
if !hasKey {
sceneMarkerTag := &models.SceneMarkerTag{Tag: markerPrimaryTag}
tags[markerPrimaryTag.ID] = sceneMarkerTag
keys = append(keys, 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.
@@ -203,13 +243,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

@@ -2,24 +2,167 @@ package api
import (
"context"
"time"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *galleryResolver) Path(ctx context.Context, obj *models.Gallery) (*string, error) {
if obj.Path.Valid {
return &obj.Path.String, nil
}
return nil, nil
}
func (r *galleryResolver) Title(ctx context.Context, obj *models.Gallery) (*string, error) {
return nil, nil // TODO remove this from schema
if obj.Title.Valid {
return &obj.Title.String, nil
}
return nil, nil
}
func (r *galleryResolver) Files(ctx context.Context, obj *models.Gallery) ([]*models.GalleryFilesType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
return obj.GetFiles(baseURL), 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
}
return ret, nil
}
func (r *galleryResolver) Scene(ctx context.Context, obj *models.Gallery) (*models.Scene, error) {
if !obj.SceneID.Valid {
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
}
func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*string, error) {
if obj.Date.Valid {
result := utils.GetYMDFromDatabaseDate(obj.Date.String)
return &result, nil
}
return nil, nil
}
func (r *galleryResolver) URL(ctx context.Context, obj *models.Gallery) (*string, error) {
if obj.URL.Valid {
return &obj.URL.String, nil
}
return nil, nil
}
func (r *galleryResolver) Details(ctx context.Context, obj *models.Gallery) (*string, error) {
if obj.Details.Valid {
return &obj.Details.String, nil
}
return nil, nil
}
func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
}
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
}
return ret, nil
}
func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret *models.Studio, err error) {
if !obj.StudioID.Valid {
return nil, nil
}
qb := models.NewSceneQueryBuilder()
return qb.Find(int(obj.SceneID.Int64))
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) (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) (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) (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
}
func (r *galleryResolver) CreatedAt(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *galleryResolver) UpdatedAt(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}
func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
return &obj.FileModTime.Timestamp, nil
}

View File

@@ -0,0 +1,106 @@
package api
import (
"context"
"time"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
)
func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string, error) {
ret := image.GetTitle(obj)
return &ret, nil
}
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
if obj.Rating.Valid {
rating := int(obj.Rating.Int64)
return &rating, nil
}
return nil, nil
}
func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*models.ImageFileType, error) {
width := int(obj.Width.Int64)
height := int(obj.Height.Int64)
size := int(obj.Size.Int64)
return &models.ImageFileType{
Size: &size,
Width: &width,
Height: &height,
}, nil
}
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*models.ImagePathsType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewImageURLBuilder(baseURL, obj)
thumbnailPath := builder.GetThumbnailURL()
imagePath := builder.GetImageURL()
return &models.ImagePathsType{
Image: &imagePath,
Thumbnail: &thumbnailPath,
}, nil
}
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret []*models.Gallery, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Gallery().FindByImageID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (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 *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) (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
}
func (r *imageResolver) CreatedAt(ctx context.Context, obj *models.Image) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *imageResolver) UpdatedAt(ctx context.Context, obj *models.Image) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}
func (r *imageResolver) FileModTime(ctx context.Context, obj *models.Image) (*time.Time, error) {
return &obj.FileModTime.Timestamp, nil
}

View File

@@ -2,6 +2,7 @@ package api
import (
"context"
"time"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/models"
@@ -53,10 +54,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 +85,50 @@ 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
}
func (r *movieResolver) CreatedAt(ctx context.Context, obj *models.Movie) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *movieResolver) UpdatedAt(ctx context.Context, obj *models.Movie) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}

View File

@@ -2,8 +2,11 @@ package api
import (
"context"
"time"
"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,17 +137,120 @@ 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) 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
}
func (r *performerResolver) CreatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *performerResolver) UpdatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}

View File

@@ -2,8 +2,10 @@ package api
import (
"context"
"time"
"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"
)
@@ -65,12 +67,12 @@ func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.Sc
bitrate := int(obj.Bitrate.Int64)
return &models.SceneFileType{
Size: &obj.Size.String,
Duration: &obj.Duration.Float64,
Duration: handleFloat64(obj.Duration.Float64),
VideoCodec: &obj.VideoCodec.String,
AudioCodec: &obj.AudioCodec.String,
Width: &width,
Height: &height,
Framerate: &obj.Framerate.Float64,
Framerate: handleFloat64(obj.Framerate.Float64),
Bitrate: &bitrate,
}, nil
}
@@ -78,12 +80,16 @@ 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()
funscriptPath := builder.GetFunscriptURL()
return &models.ScenePathsType{
Screenshot: &screenshotPath,
Preview: &previewPath,
@@ -91,62 +97,133 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.S
Webp: &webpPath,
Vtt: &vttPath,
ChaptersVtt: &chaptersVttPath,
Sprite: &spritePath,
Funscript: &funscriptPath,
}, 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 {
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) (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
}
func (r *sceneResolver) CreatedAt(ctx context.Context, obj *models.Scene) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *sceneResolver) UpdatedAt(ctx context.Context, obj *models.Scene) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}
func (r *sceneResolver) FileModTime(ctx context.Context, obj *models.Scene) (*time.Time, error) {
return &obj.FileModTime.Timestamp, nil
}

View File

@@ -2,29 +2,48 @@ package api
import (
"context"
"time"
"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) {
@@ -38,3 +57,11 @@ func (r *sceneMarkerResolver) Preview(ctx context.Context, obj *models.SceneMark
sceneID := int(obj.SceneID.Int64)
return urlbuilders.NewSceneURLBuilder(baseURL, sceneID).GetSceneMarkerStreamPreviewURL(obj.ID), nil
}
func (r *sceneMarkerResolver) CreatedAt(ctx context.Context, obj *models.SceneMarker) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *sceneMarkerResolver) UpdatedAt(ctx context.Context, obj *models.SceneMarker) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}

View File

@@ -2,8 +2,11 @@ package api
import (
"context"
"time"
"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,26 +26,117 @@ 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) (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
}
func (r *studioResolver) CreatedAt(ctx context.Context, obj *models.Studio) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *studioResolver) UpdatedAt(ctx context.Context, obj *models.Studio) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}

View File

@@ -2,31 +2,95 @@ package api
import (
"context"
"time"
"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) Aliases(ctx context.Context, obj *models.Tag) (ret []string, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().GetAliases(obj.ID)
return err
}); err != nil {
return nil, err
}
count, err := qb.CountByTagID(obj.ID)
return ret, err
}
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
}
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
}
func (r *tagResolver) CreatedAt(ctx context.Context, obj *models.Tag) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}
func (r *tagResolver) UpdatedAt(ctx context.Context, obj *models.Tag) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, 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 _, stashPath := range input.Stashes {
exists, err := utils.DirExists(stashPath)
if !exists {
return makeConfigGeneralResult(), err
for _, s := range input.Stashes {
// 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,108 +51,150 @@ 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)
}
c.Set(config.PreviewAudio, input.PreviewAudio)
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 {
c.Set(config.ImageExclude, input.ImageExcludes)
}
if input.VideoExtensions != nil {
c.Set(config.VideoExtensions, input.VideoExtensions)
}
if input.ImageExtensions != nil {
c.Set(config.ImageExtensions, input.ImageExtensions)
}
if input.GalleryExtensions != nil {
c.Set(config.GalleryExtensions, input.GalleryExtensions)
}
c.Set(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
if input.CustomPerformerImageLocation != nil {
c.Set(config.CustomPerformerImageLocation, *input.CustomPerformerImageLocation)
initialiseCustomImages()
}
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
}
if err := config.Write(); err != nil {
if input.ScraperCertCheck != nil {
c.Set(config.ScraperCertCheck, input.ScraperCertCheck)
}
if input.StashBoxes != nil {
if err := c.ValidateStashBoxes(input.StashBoxes); err != nil {
return nil, err
}
c.Set(config.StashBoxes, input.StashBoxes)
}
if err := c.Write(); err != nil {
return makeConfigGeneralResult(), err
}
@@ -143,32 +207,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 := ""
@@ -177,15 +250,106 @@ 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 input.HandyKey != nil {
c.Set(config.HandyKey, *input.HandyKey)
}
if input.FunscriptOffset != nil {
c.Set(config.FunscriptOffset, *input.FunscriptOffset)
}
if err := c.Write(); err != nil {
return makeConfigInterfaceResult(), err
}
return makeConfigInterfaceResult(), nil
}
func (r *mutationResolver) ConfigureDlna(ctx context.Context, input models.ConfigDLNAInput) (*models.ConfigDLNAResult, error) {
c := config.GetInstance()
if input.ServerName != nil {
c.Set(config.DLNAServerName, *input.ServerName)
}
c.Set(config.DLNADefaultIPWhitelist, input.WhitelistedIPs)
currentDLNAEnabled := c.GetDLNADefaultEnabled()
if input.Enabled != nil && *input.Enabled != currentDLNAEnabled {
c.Set(config.DLNADefaultEnabled, *input.Enabled)
// start/stop the DLNA service as needed
dlnaService := manager.GetInstance().DLNAService
if !*input.Enabled && dlnaService.IsRunning() {
dlnaService.Stop(nil)
} else if *input.Enabled && !dlnaService.IsRunning() {
dlnaService.Start(nil)
}
}
c.Set(config.DLNAInterfaces, input.Interfaces)
if err := c.Write(); err != nil {
return makeConfigDLNAResult(), err
}
return makeConfigDLNAResult(), nil
}
func (r *mutationResolver) ConfigureScraping(ctx context.Context, input models.ConfigScrapingInput) (*models.ConfigScrapingResult, error) {
c := config.GetInstance()
refreshScraperCache := false
if input.ScraperUserAgent != nil {
c.Set(config.ScraperUserAgent, input.ScraperUserAgent)
refreshScraperCache = true
}
if input.ScraperCDPPath != nil {
c.Set(config.ScraperCDPPath, input.ScraperCDPPath)
refreshScraperCache = true
}
if input.ExcludeTagPatterns != nil {
c.Set(config.ScraperExcludeTagPatterns, input.ExcludeTagPatterns)
}
c.Set(config.ScraperCertCheck, input.ScraperCertCheck)
if refreshScraperCache {
manager.GetInstance().RefreshScraperCache()
}
if err := c.Write(); err != nil {
return makeConfigScrapingResult(), err
}
return makeConfigScrapingResult(), 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
}

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