Compare commits

...

184 Commits

Author SHA1 Message Date
WithoutPants
1096fe812e Merge pull request #2011 from stashapp/develop
Merge to master for v0.11.0 release
2021-11-16 09:38:54 +11:00
kermieisinthehouse
5f5cd03929 V0.11.0 User Survey (#1971) 2021-11-16 09:23:33 +11:00
7dJx1qP
aa50dbf5e2 add missing trace loglevel (#2007) 2021-11-15 12:26:49 +11:00
Goran Osim
7547a1dd39 Fix Link to Scraping Document (#1988) [skip ci] 2021-11-15 09:07:23 +11:00
InfiniteTF
c571687c99 Resolve performer/studio stashIDs for scraped scenes (#2006)
* Resolve performer/studio stashIDs for scraped scenes
* Check endpoint when matching stashids
2021-11-15 07:51:52 +11:00
InfiniteTF
2a5afecc77 Fix invalid baseurl if host header already contains port (#2005) 2021-11-15 07:50:42 +11:00
InfiniteTF
df7d1427d6 Fix entity creation bug during tag/performer scraping (#2004) 2021-11-15 07:50:08 +11:00
peolic
0d76fede84 Fix config path regression (#1996) 2021-11-13 08:05:05 +11:00
kermieisinthehouse
2c7e0f0571 Apply python path resolution to plugins (#1990)
* Symlink python3 to python
* Apply path resolution to plugins
2021-11-12 09:21:52 +11:00
kermieisinthehouse
a7ed0a7004 Bump viper version, fix nobrowser (#1991) 2021-11-12 09:21:04 +11:00
InfiniteTF
808202ba8a Fix performer tagger field updating (#1977)
* Fix performer tagger field updating
2021-11-11 11:34:46 +11:00
kermieisinthehouse
07dbc2236d Update README.md (#1985) [skip ci] 2021-11-10 16:10:07 +11:00
WithoutPants
225e17f710 Confirm empty stash paths (#1983) 2021-11-10 16:09:09 +11:00
Releck
d316aeca16 Fix identify defaults not returning by using main viper instance (#1979) 2021-11-10 09:32:47 +11:00
kermieisinthehouse
bd0de23459 Spiffy Readme (#1980) [skip ci] 2021-11-10 09:31:40 +11:00
7dJx1qP
bcd6d4fb46 Update scene filename parser input styling (#1956)
* update scene filename parser input styling
* update parser field rating classnames
2021-11-08 11:41:47 +11:00
Frix-416
ffc8043273 French translation (fr-FR) (#1967) 2021-11-08 11:41:05 +11:00
WithoutPants
49b2860909 Improve flag and environment config overrides (#1898)
* Separate overrides from config
* Don't allow changing overridden value
* Write default host and port to config file
* Use existing library value. Hide generated if set
2021-11-08 10:14:11 +11:00
仇柯人
60c4f7e992 change translation typo error (#1963)
change the wrong typo DLAN to DLNA
2021-11-08 08:34:33 +11:00
kermieisinthehouse
1f48a9ce95 Fix Scenes Detail tab string (#1966) 2021-11-08 08:07:41 +11:00
WithoutPants
3aa5f657bc Add display autoplay and playlist continue options (#1921)
* Add display autoplay and playlist continue options
* Include continue parameter in scene links
2021-11-07 09:55:51 +11:00
gitgiggety
25274e2596 Support Is (not) null for all multi criterions (#1785)
* Support Is (not) null for all multi criterions

Add support for the Is null and Is not null modifiers for all cases of
the MultiCriterionInput and HierarchicalMultiCriterionInput. This
partially overlaps the "X Count" filter which sometimes is available
(because it would be the same as "X Count equals 0" and "X Count greater
than 0") but this also enables it for other criterions like the "Parent
Studio" filter for studios or just the "Studios" filter for scenes /
images / galleries, the "Movies" filter for scenes etc.

* Don't crash UI on bad saved filter
* Add missing code for tag parent/child

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-11-07 09:34:33 +11:00
7dJx1qP
e961ba4459 Fix tag hierarchy validation (#1926)
* update tag hierarchy validation
* refactor MergeHierarchy
* update tag hierarchy error message
* rename tag hierarchy function
* add tag path to error message
* Rename EnsureHierarchy to ValidateHierarchy

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-11-07 09:33:46 +11:00
WithoutPants
5bb5f6f2ce Move scene post update hook to outside Identify transaction (#1953)
* Move update post hook call outside transaction
* Make Scene Movies resolver use read transaction
2021-11-06 09:58:52 +11:00
WithoutPants
dbfd92f9a8 Support StashIDs in scrape dialog (#1955) 2021-11-05 12:10:29 +11:00
WithoutPants
392b28915a Fix exclusion pattern inputs blurring on every keypress (#1952)
* Replace ExclusionPatterns with StringListInput
2021-11-05 11:52:00 +11:00
liquid-flow
3671388b8d Rework main navbar (#1769)
* Fix responsive layout
* Refactor MainNavbar
* Stick the navbar to the bottom on mobile
* Fix menu item icon-text vertical alignment

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-11-04 18:12:48 +11:00
WithoutPants
602183cca9 File size scanning fixes (#1944) 2021-11-04 10:50:03 +11:00
stg-annon
206f86e304 Update and split up scraping docs (#1934) 2021-11-04 10:48:58 +11:00
kermieisinthehouse
9134490be2 Add linux ffmpeg urls (#1943) 2021-11-04 08:22:25 +11:00
7dJx1qP
a8a5d547ed Fix checkbox labels (#1947)
* fix create galleries from folders checkbox id
* add missing checkbox ids
2021-11-04 08:21:34 +11:00
7dJx1qP
ed9d80fcfc Add bottom pagination to scene duplicate checker (#1942)
* add bottom pagination to scene duplicate checker
* add top margin

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-11-03 09:22:44 +11:00
kermieisinthehouse
934d685e86 Small docker cleanup (#1941) 2021-11-02 11:39:53 +11:00
dependabot[bot]
32db1dda57 Bump github.com/tidwall/gjson from 1.8.1 to 1.9.3 (#1906)
Bumps [github.com/tidwall/gjson](https://github.com/tidwall/gjson) from 1.8.1 to 1.9.3.
- [Release notes](https://github.com/tidwall/gjson/releases)
- [Commits](https://github.com/tidwall/gjson/compare/v1.8.1...v1.9.3)

---
updated-dependencies:
- dependency-name: github.com/tidwall/gjson
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 11:31:28 +11:00
kermieisinthehouse
0dbee117d8 Update AutoTagging.md (#1935) 2021-11-01 11:17:32 +11:00
peolic
f4ac82f989 Update GridCard.tsx (#1930) 2021-11-01 11:16:58 +11:00
SmallCoccinelle
5f26719d2d Suppress a benign warning (#1932) 2021-11-01 11:16:23 +11:00
InfiniteTF
0d24af4cb4 Extend __typename stripper to fix interface config saving (#1929) 2021-11-01 11:15:03 +11:00
Still Hsu
07a1cdd9f7 Update zh-tw for Auto Identifcation Feature (#1923)
* Add localization strings for the new auto identification feature
2021-10-29 11:46:40 +11:00
7dJx1qP
c6a326ca64 Add stash_ids to performer, scene, studio import/export (#1916)
* add stash_ids to performer, scene, studio import/export
2021-10-29 11:12:39 +11:00
kermieisinthehouse
87036a07bc Start browser on server start (#1832)
* Start browser on server start
* Add config option for opening browser
2021-10-29 10:19:23 +11:00
7dJx1qP
29b14ab4fc move scrape-url-button style to component styles (#1925) 2021-10-29 09:08:43 +11:00
WithoutPants
ad7bb9a46f Clamp page size maximum (#1920) 2021-10-29 09:07:47 +11:00
7dJx1qP
b01c4468d8 Improve scrape url button UX (#1903)
* change button label to icon
* add URLField component
2021-10-28 17:22:26 +11:00
WithoutPants
bf59028bcd Bump go cache (#1919) 2021-10-28 17:00:24 +11:00
7dJx1qP
96fce90cc3 Add delete file and generated files by default config options (#1852)
* add delete file and generated files by default config options
* add alert message with files to be deleted

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-10-28 16:45:44 +11:00
WithoutPants
27cdefbb10 Identify task (#1839)
* Add identify task
* Change type naming
* Debounce folder select text input
* Add generic slice comparison function
2021-10-28 14:25:38 +11:00
WithoutPants
0f64954e5b Identify task (#1839)
* Add identify task
* Change type naming
* Debounce folder select text input
* Add generic slice comparison function
2021-10-28 14:25:17 +11:00
InfiniteTF
c93b5e12b7 Update typescript and eslint config (#1878)
* Update eslint rules
* Update typescript to 4.4
* Disable react/display-name
* Add @typescript-eslint/typescript-estree
2021-10-28 10:27:26 +11:00
WithoutPants
372ea7218e Fix UI crash when no scene phash (#1908) 2021-10-27 18:09:12 +11:00
7dJx1qP
b2897d0cf5 fix studio-details classname typo (#1907) 2021-10-27 16:21:49 +11:00
kermieisinthehouse
5de06d4b62 Convert filesize to float64 for arm32 (#1895)
* Convert filesize to float64 for arm32
2021-10-27 11:55:11 +11:00
WithoutPants
2136ced25c Use a default stash-box name if none provided (#1889)
* Use a default stash-box name if none provided
2021-10-27 11:54:55 +11:00
stashcoder42
bdb8dc94d3 Add page titles using react-helmet (#1831)
* add titles with react-helmet
2021-10-27 09:37:18 +11:00
WithoutPants
e9c7b0aed3 Fix new values not removed correctly when added (#1890)
* Fix new values not removed correctly when added
2021-10-27 09:33:08 +11:00
WithoutPants
a822455a66 Support create missing scraped movie studio (#1899)
* Support create missing scraped movie studio
2021-10-27 08:31:24 +11:00
InfiniteTF
1fffc0519a Split out entity creation from view pages (#1884)
* Split performerCreate page into separate page
* Split studioCreate into a separate page
* Remove Partial types from performer/studio
* Split tagCreate into a separate page
* Split movieCreate into a separate page
* Split out galleryCreate into its own page
* Add loader to scene page
* Fix performer name fallback
* Fix movie layout shift
* Fix prompt comment and switch studio prompt to localized string
2021-10-26 09:43:45 +11:00
kermieisinthehouse
c8182bdb4c Improve Spanish translation (#1894)
* Improve spanish translation
* Change scraper verbiage
2021-10-26 09:12:59 +11:00
kermieisinthehouse
35e646b68b Update italian translation (#1891) 2021-10-26 09:12:09 +11:00
WithoutPants
595e8efb73 Fix race condition in generate task (#1888) 2021-10-26 08:41:40 +11:00
kermieisinthehouse
1e5889ba17 Spanish translation (#1886)
* Spanish translation
2021-10-25 13:07:16 +11:00
kermieisinthehouse
4dd56c3d82 Show duration and filesize in results (#1776)
* Add new query interface
* Refactor query builder
* Change Query interface
* Return duration and filesize in scene query
* Adjust UI for scene metadata
* Introduce new image query interface
* Change image Query interface
* Add megapixels and size to image query
* Update image UI

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-10-25 11:40:13 +11:00
Still Hsu
1b411e3f43 i18n: Localize Setup & Migrate (#1880)
* Add migration & setup localization strings
* Update hard strings in tag panels
2021-10-25 10:59:03 +11:00
InfiniteTF
db1e5c63d0 Fix performer tagger (#1874)
* Fix performer tagger
2021-10-25 10:49:37 +11:00
kermieisinthehouse
cb52eb798d Italian Translation (it-IT) (#1875)
* Italian translation metadata
* Add json file from BViking
2021-10-25 10:41:29 +11:00
kermieisinthehouse
f904c59532 Fix docker (#1883) 2021-10-25 10:36:36 +11:00
SmallCoccinelle
401fc290ef Handle request cancellation more gracefully (#1877)
The version checking code performs its own error management and will
not pass errors to the caller. Hence, it needs to be aware of the types
of errors which can be returned.

In particular, the context.Canceled error will be returned if the
context is aborted through cancelation. This happens when the request
is terminated by tapping CTRL-C or if the browser request is terminated
while we are sitting waiting for the GH API.
2021-10-25 10:09:20 +11:00
WithoutPants
d292ed0b34 Fix deleting folder-based gallery file deleting (#1872)
* Fix deleting gallery image files
* Clarify delete file documentation
2021-10-25 09:29:03 +11:00
WithoutPants
0ab8d32687 Refactor generate task (#1871)
* Refactor generate task
2021-10-23 10:48:42 +11:00
kermieisinthehouse
f80a5e3222 Docker+Build optimizations (#1833)
* Docker CI builds: half the size, less than half the build time
* Add an "Official Build" Designator
* Fix .git constantly invalidating build cache, use distro ffmpeg
* Fix official build detection, add some compiler image docs

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-10-22 13:14:08 +11:00
kermieisinthehouse
7732152c0c Significantly speed up UI build by leveraging esbuild (#1851)
* Use esbuild wherever possible in ui build
2021-10-20 17:37:17 +11:00
InfiniteTF
15acf91b90 Add PHash distance matching to stash-box integration (#1858)
* Add PHash distance matching to stash-box integration
2021-10-20 17:22:25 +11:00
WithoutPants
976038424b Fix parsing ipv6 address with scope id (#1841)
* Fix parsing ipv6 address with scope id

Also allows connections from link local unicast address

* Add unit tests
2021-10-20 16:52:15 +11:00
SmallCoccinelle
8b7720e3bf Enable the appendAssign lint check (#1865)
* Make copies of buffers

Avoid reusing one of the incoming arrays as a append extension, and
make a copy of the data. It's cleaner in the long run and possibly
easier for the GC to maintain.

* Avoid appendAssign problems in tag code

Reuse the existing slice when appending.

* Fix appendAssign in encoder_scene_preview_chunk

Appending and creating a new slice is somewhat unintuitive since the
underlying slice might be extended to satisfy the new capacity. This
sometimes leads to faulty logic.

Rewrite the code so it reuses `args` for all appending, and builds one
array clearly in the code. It follows the general style of the function
where `args` is being built in small incremental batches and avoids
the introduction of new names.

* Enable the appendAssign check

This makes us pass all gocritic warnings.

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-10-20 16:34:19 +11:00
SmallCoccinelle
e513b6ffa5 Cache and reuse the scraper HTTP client (#1855)
* Add Cookies directly to the request

Rather than maintaining a cookie jar on a one-shot HTTP client, maintain
the jar ourselves: make a new jar, then use it to select the right
cookies.

The cookies are set on the request rather than on the client. This will
retain the current behavior as we are always throwing the client away
after each use.

This patch enables the lifting of the http client as well over time.

* Introduce a cached scraper HTTP client

The scraper cache is augmented with an *http.Client. These are safe for
concurrent use, so the pointer can safely be passed around. Push this
into scraper configurations where applicable, next to the txnManagers.

When we issue a loadUrl request, do so on the cached *http.Client,
which will reuse existing idle connections in the client if any are
present.

* Set MaxIdleConnsPerHost. Closes #1850

We allow for up to 8 idle connections to a single host. This should
make concurrent operation toward the same host reuse connections, even
for sizeable concurrency.

The number isn't bumped excessively high. We should probably limit
concurrency toward a single site anyway, since we'll be able to overrun
a site with queries quite easily if we have many concurrent goroutines
issuing requests at the same time.

* Reinstate driverOptions / useCDP check

Use DeMorgan's laws to invert the logic and exit early. Fixes tests
breaking.

* Documentation fixup.

* Use the scraper http.Client when fetching images

Fold image fetchers onto the cached scraper http.Client as well. This
makes the scraper have a single http.Client cache for all its
operations.

Thread the client upwards to the relevant attachment points: either the
cache, or a stash_box instance, which is extended to include a pointer
to the client.

Style roughly follows that of txnManagers.

* Use the same http Client as the GraphQL client use

Rather than using http.DefaultClient, use the same client as the
GraphQL client use in the stash_box subsystem. This localizes the
client used in the subsystem into the constructing New.. call.

* Hoist HTTP client construction

Create a function for initializaing the HTTP Client we use. While here
hoist magic numbers into constants. Introduce a proper static redirect
error and use it in the client code as well.

* Reinstate printCookies

This is a debugging function, and it might still come in handy in the
future at some point.

* Nitpick comment.

* Minor tidy

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-10-20 16:12:24 +11:00
SmallCoccinelle
214a15bc40 Fixups + enable the commentFormatting linter (#1866)
* Add a space after // comments

For consistency, the commentFormatting lint checker suggests a space
after each // comment block. This commit handles all the spots in
the code where that is needed.

* Rewrite documentation on functions

Use the Go idiom of commenting:

* First sentence declares the purpose.
* First word is the name being declared

The reason this style is preferred is such that grep is able to find
names the user might be interested in. Consider e.g.,

    go doc -all pkg/ffmpeg | grep -i transcode

in which case a match will tell you the name of the function you are
interested in.

* Remove old code comment-blocks

There are some commented out old code blocks in the code base. These are
either 3 years old, or 2 years old. By now, I don't think their use is
going to come back any time soon, and Git will track old pieces of
deleted code anyway.

Opt for deletion.

* Reorder imports

Split stdlib imports from non-stdlib imports in files we are touching.

* Use a range over an iteration variable

Probably more go-idiomatic, and the code needed comment-fixing anyway.

* Use time.After rather than rolling our own

The idiom here is common enough that the stdlib contains a function for
it. Use the stdlib function over our own variant.

* Enable the commentFormatting linter
2021-10-20 16:10:46 +11:00
SmallCoccinelle
e14bb8432c Enable gocritic (#1848)
* Don't capitalize local variables

ValidCodecs -> validCodecs

* Capitalize deprecation markers

A deprecated marker should be capitalized.

* Use re.MustCompile for static regexes

If the regex fails to compile, it's a programmer error, and should be
treated as such. The regex is entirely static.

* Simplify else-if constructions

Rewrite

   else { if cond {}}

to

   else if cond {}

* Use a switch statement to analyze formats

Break an if-else chain. While here, simplify code flow.

Also introduce a proper static error for unsupported image formats,
paving the way for being able to check against the error.

* Rewrite ifElse chains into switch statements

The "Effective Go" https://golang.org/doc/effective_go#switch document
mentions it is more idiomatic to write if-else chains as switches when
it is possible.

Find all the plain rewrite occurrences in the code base and rewrite.
In some cases, the if-else chains are replaced by a switch scrutinizer.
That is, the code sequence

  if x == 1 {
      ..
  } else if x == 2 {
      ..
  } else if x == 3 {
      ...
  }

can be rewritten into

  switch x {
  case 1:
    ..
  case 2:
    ..
  case 3:
    ..
  }

which is clearer for the compiler: it can decide if the switch is
better served by a jump-table then a branch-chain.

* Rewrite switches, introduce static errors

Introduce two new static errors:

* `ErrNotImplmented`
* `ErrNotSupported`

And use these rather than forming new generative errors whenever the
code is called. Code can now test on the errors (since they are static
and the pointers to them wont change).

Also rewrite ifElse chains into switches in this part of the code base.

* Introduce a StashBoxError in configuration

Since all stashbox errors are the same, treat them as such in the code
base. While here, rewrite an ifElse chain.

In the future, it might be beneifical to refactor configuration errors
into one error which can handle missing fields, which context the error
occurs in and so on. But for now, try to get an overview of the error
categories by hoisting them into static errors.

* Get rid of an else-block in transaction handling

If we succesfully `recover()`, we then always `panic()`. This means the
rest of the code is not reachable, so we can avoid having an else-block
here.

It also solves an ifElse-chain style check in the code base.

* Use strings.ReplaceAll

Rewrite

    strings.Replace(s, o, n, -1)

into

    strings.ReplaceAll(s, o, n)

To make it consistent and clear that we are doing an all-replace in the
string rather than replacing parts of it. It's more of a nitpick since
there are no implementation differences: the stdlib implementation is
just to supply -1.

* Rewrite via gocritic's assignOp

Statements of the form

    x = x + e

is rewritten into

    x += e

where applicable.

* Formatting

* Review comments handled

Stash-box is a proper noun.

Rewrite a switch into an if-chain which returns on the first error
encountered.

* Use context.TODO() over context.Background()

Patch in the same vein as everything else: use the TODO() marker so we
can search for it later and link it into the context tree/tentacle once
it reaches down to this level in the code base.

* Tell the linter to ignore a section in manager_tasks.go

The section is less readable, so mark it with a nolint for now. Because
the rewrite enables a ifElseChain, also mark that as nolint for now.

* Use strings.ReplaceAll over strings.Replace

* Apply an ifElse rewrite

else { if .. { .. } } rewrite into else if { .. }

* Use switch-statements over ifElseChains

Rewrite chains of if-else into switch statements. Where applicable,
add an early nil-guard to simplify case analysis. Also, in
ScanTask's Start(..), invert the logic to outdent the whole block, and
help the reader: if it's not a scene, the function flow is now far more
local to the top of the function, and it's clear that the rest of the
function has to do with scene management.

* Enable gocritic on the code base.

Disable appendAssign for now since we aren't passing that check yet.

* Document the nolint additions

* Document StashBoxBatchPerformerTagInput
2021-10-18 14:12:40 +11:00
Still Hsu
3e6e830f45 Update zh-tw Localization & Fix i18n Grammar Issue (#1807)
* Refactor parent/child strings
* Add new zh-tw strings

Signed-off-by: Still Hsu <dev@stillu.cc>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-10-16 09:56:46 +11:00
7dJx1qP
95e11d4aa8 Add clear button to list filter search input (#1845)
* add clear button to list filter search input
2021-10-15 14:42:32 +11:00
7dJx1qP
cf43a825d8 Add performer star ratings to main view (#1844)
* add performer star ratings to main view
* remove ratings from details and edit tab. move ratings hotkeys from edit tab to main view
2021-10-15 14:17:40 +11:00
kermieisinthehouse
5ec70ac3e0 Fix List filter styles, fix freeones spam (#1853)
* Fix List filter styles, fix freeones spam
2021-10-15 14:02:49 +11:00
WithoutPants
39fdde273d Scan refactor (#1816)
* Add file scanner
* Scan scene changes
* Split scan files
* Generalise scan
* Refactor ffprobe
* Refactor ffmpeg encoder
* Move scene scan code to scene package
* Move matchExtension to utils
* Refactor gallery scanning
* Refactor image scanning
* Prevent race conditions on identical hashes
* Refactor image thumbnail generation
* Perform count concurrently
* Allow progress increment before total set
* Make progress updates more frequent
2021-10-15 10:39:48 +11:00
WithoutPants
3d5ee16e90 Fix colours on console when logging to file (#1846)
* Fix colours on console when logging to file
2021-10-15 10:38:32 +11:00
7dJx1qP
1152e1acac Fix performer icon link padding (#1843)
* fix performer url icon link padding
2021-10-15 08:56:41 +11:00
7dJx1qP
602a32bd32 Add id to hierarchy filter checkbox input (#1842)
* add id to hierarchy filter checkbox input
2021-10-15 08:55:52 +11:00
SmallCoccinelle
655d3ae969 Toward better context handling (#1835)
* Use the request context

The code uses context.Background() in a flow where there is a
http.Request. Use the requests context instead.

* Use a true context in the plugin example

Let AddTag/RemoveTag take a context and use that context throughout
the example.

* Avoid the use of context.Background

Prefer context.TODO over context.Background deep in the call chain.

This marks the site as something which we need to context-handle
later, and also makes it clear to the reader that the context is
sort-of temporary in the code base.

While here, be consistent in handling the `act` variable in each
branch of the if .. { .. } .. check.

* Prefer context.TODO over context.Background

For the different scraping operations here, there is a context
higher up the call chain, which we ought to use. Mark the call-sites
as TODO for now, so we can come back later on a sweep of which parts
can be context-lifted.

* Thread context upwards

Initialization requires context for transactions. Thread the context
upward the call chain.

At the intialization call, add a context.TODO since we can't break this
yet. The singleton assumption prevents us from pulling it up into main for
now.

* make tasks context-aware

Change the task interface to understand contexts.

Pass the context down in some of the branches where it is needed.

* Make QueryStashBoxScene context-aware

This call naturally sits inside the request-context. Use it.

* Introduce a context in the JS plugin code

This allows us to use a context for HTTP calls inside the system.

Mark the context with a TODO at top level for now.

* Nitpick error formatting

Use %v rather than %s for error interfaces.
Do not begin an error strong with a capital letter.

* Avoid the use of http.Get in FFMPEG download chain

Since http.Get has no context, it isn't possible to break out or have
policy induced. The call will block until the GET completes. Rewrite
to use a http Request and provide a context.

Thread the context through the call chain for now. provide
context.TODO() at the top level of the initialization chain.

* Make getRemoteCDPWSAddress aware of contexts

Eliminate a call to http.Get and replace it with a context-aware
variant.

Push the context upwards in the call chain, but plug it before the
scraper interface so we don't have to rewrite said interface yet.

Plugged with context.TODO()

* Scraper: make the getImage function context-aware

Use a context, and pass it upwards. Plug it with context.TODO()
up the chain before the rewrite gets too much out of hand for now.

Minor tweaks along the way, remove a call to context.Background()
deep in the call chain.

* Make NOTIFY request context-aware

The call sits inside a Request-handler. So it's natural to use the
requests context as the context for the outgoing HTTP request.

* Use a context in the url scraper code

We are sitting in code which has a context, so utilize it for the
request as well.

* Use a context when checking versions

When we check the version of stash on Github, use a context. Thread
the context up to the initialization routine of the HTTP/GraphQL
server and plug it with a context.TODO() for now.

This paves the way for providing a context to the HTTP server code in a
future patch.

* Make utils func ReadImage context-aware

In almost all of the cases, there is a context in the call chain which
is a natural use. This is true for all the GraphQL mutations.

The exception is in task_stash_box_tag, so plug that task with
context.TODO() for now.

* Make stash-box get context-aware

Thread a context through the call chain until we hit the Client API.
Plug it with context.TODO() there for now.

* Enable the noctx linter

The code is now free of any uncontexted HTTP request. This means we
pass the noctx linter, and we can enable it in the code base.
2021-10-14 15:32:41 +11:00
SmallCoccinelle
41a1fb8aec Add collation to directory listings (#1823)
* Add collation to directory listings. Closes #1806

Introduce a new `locale` arg to the `Query.directory` field. Set "en"
as the default for the field for backward compatibility. Use the given
locale, sending it through a language matcher, and use `x/text` as the
collation engine for the matched language.

Augment the file `ListDirs` call to optionally take a Collator. If the
Collator is given, sort file listings according to the collators rules.

While here, document the GraphQL schema a bit more.

Add matchers by looking at the current front-end locales, and make sure
each of these occur in the matcher list.

* Language matcher touchups

* Avoid having `en-US` twice.
* Introduce `en-AU`.

* Pass IgnoreCase and Numeric collation

Allow the collator to be configured with options. Pass the options
IgnoreCase and Numeric to the collator.
2021-10-14 15:16:45 +11:00
WithoutPants
4eeef22c15 Generalise tagger view to all scraping sources (#1812)
* Refactor Tagger view
* Support link to existing studio/performer
* Allow stash id field selection
2021-10-14 08:53:04 +11:00
kermieisinthehouse
1217f3fbc1 add status badge (#1834) 2021-10-12 19:41:35 +11:00
SmallCoccinelle
c6f6205e4f Errorlint sweep + minor linter tweaks (#1796)
* Replace error assertions with Go 1.13 style

Use `errors.As(..)` over type assertions. This enables better use of
wrapped errors in the future, and lets us pass some errorlint checks
in the process.

The rewrite is entirely mechanical, and uses a standard idiom for
doing so.

* Use Go 1.13's errors.Is(..)

Rather than directly checking for error equality, use errors.Is(..).

This protects against error wrapping issues in the future.

Even though something like sql.ErrNoRows doesn't need the wrapping, do
so anyway, for the sake of consistency throughout the code base.

The change almost lets us pass the `errorlint` Go checker except for
a missing case in `js.go` which is to be handled separately; it isn't
mechanical, like these changes are.

* Remove goconst

goconst isn't a useful linter in many cases, because it's false positive
rate is high. It's 100% for the current code base.

* Avoid direct comparison of errors in recover()

Assert that we are catching an error from recover(). If we are,
check that the error caught matches errStop.

* Enable the "errorlint" checker

Configure the checker to avoid checking for errorf wraps. These are
often false positives since the suggestion is to blanket wrap errors
with %w, and that exposes the underlying API which you might not want
to do.

The other warnings are good however, and with the current patch stack,
the code base passes all these checks as well.

* Configure rowserrcheck

The project uses sqlx. Configure rowserrcheck to include said package.

* Mechanically rewrite a large set of errors

Mechanically search for errors that look like

    fmt.Errorf("...%s", err.Error())

and rewrite those into

    fmt.Errorf("...%v", err)

The `fmt` package is error-aware and knows how to call err.Error()
itself.

The rationale is that this is more idiomatic Go; it paves the
way for using error wrapping later with %w in some sites.

This patch only addresses the entirely mechanical rewriting caught by
a project-side search/replace. There are more individual sites not
addressed by this patch.
2021-10-12 14:03:08 +11:00
SmallCoccinelle
38384f2c60 Improve oshash code (#1829)
Reduce allocations. Don't create intermediary arrays which we then
consume right after. Manually fuse the arrays and decode straight into
the sum instead.

Furthermore, don't invoke a Reader, but carve out the locations via a
loop, directly.

These two changes taken together speeds up oshash computations by a
factor of 10 according to the benchmark tests. The main reason for
this change is a much lowered memory allocation rate which in turn
improves GC pressure.

While here, add a benchmark for oshash computations and use it for
testing the performance.
2021-10-12 11:59:51 +11:00
WithoutPants
e9d48683f8 Autotag scraper (#1817)
* Refactor scraper structures
* Move matching code into new package
* Add autotag scraper
* Always check first letter of auto-tag names
* Account for nulls

Co-authored-by: Kermie <kermie@isinthe.house>
2021-10-11 23:06:06 +11:00
WithoutPants
b5381ff071 Add option to disable create from dropdown (#1814)
* Convert config hooks to common context
* Add option to disable creating from dropdown
2021-10-11 17:45:58 +11:00
WithoutPants
46ae4581b8 Refactor clean task (#1805)
* Refactor clean task
* Use path sorting for scenes and galleries
* Add changelog
2021-10-11 16:45:24 +11:00
WithoutPants
94e8abcb50 Merge pull request #1827 from stashapp/master
Merge master to develop for version tag
2021-10-11 15:33:23 +11:00
WithoutPants
f4b783871a Merge pull request #1824 from stashapp/develop
Merge develop to master for 0.10
2021-10-11 12:58:53 +11:00
WithoutPants
29cd627ed2 Finalise changelog 2021-10-11 12:30:49 +11:00
kermieisinthehouse
c31c7c3c99 Fix exclude filter query performance (#1815)
* Fix query performance
* Reorder changelog changes by size to prepare for release
2021-10-10 19:02:26 +11:00
kermieisinthehouse
04ca11e62e Fix setting config locking out proxy users (#1820) 2021-10-10 10:54:15 +11:00
InfiniteTF
fb5f9162d0 Fix authentication when using a reverse proxy with subpath prefix (#1818)
* Fix authentication when using a reverse proxy with subpath prefix
2021-10-09 17:32:43 +11:00
WithoutPants
47ae1be53c Add classnames to count popovers (#1813) 2021-10-06 21:12:28 +11:00
WithoutPants
11fa8ce581 Make generate options optional (#1809) 2021-10-06 14:14:23 +11:00
WithoutPants
428c6442d5 Revert "Make generate args optional (#1801)" (#1808)
This reverts commit 1e89e9dd82.
2021-10-06 13:44:15 +11:00
WithoutPants
1e89e9dd82 Make generate args optional (#1801)
* Make generate args optional with defaults
2021-10-06 08:11:12 +11:00
kermieisinthehouse
f1da6cb1b2 Disallow access in publicly exposed services (#1761)
* Add security against publicly exposed services
* Add trusted proxies setting, validate proxy chain against internet access
* Validate chain on local proxies too
* Move authentication handler to separate file
* Add startup check and log if tripwire is active

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-10-04 18:16:01 +11:00
WithoutPants
dcf58b99a6 Fix panic when serving missing marker routes (#1797) 2021-10-04 11:10:20 +11:00
SmallCoccinelle
a5ca8fc678 Enable safe linters (#1786)
* Enable safe linters

Enable the linters dogsled, rowserrcheck, and sqlclosecheck.

These report no errors currently in the code base.

Enable misspell.

Misspell finds two spelling mistakes in comments, which are fixed by the
patch as well.

Add and sort linters which are relatively
safe to add over time. Comment them out for now.

* Close the response body

If we can get a HTTP response, it has a body which ought to be closed.

By doing so, we avoid potentially leaking connections.

* Enable the exportloopref linter

There are two places in the code with these warnings. Fix them while
enabling the linter.

* Remove redundant types in tests

If a slice already determines the type, the inner type declaration is
redundant. Remove the inner declarations.

* Mark autotag test cases as parallel

Autotag test cases is by far the outlier when it comes to test time.
While go test runs test cases in parallel,
it doesn't do so inside a given package, unless one marks the test cases
as parallel.

This change provides a significant speedup on a 8-core machine for test
runs.
2021-10-03 11:48:03 +11:00
WithoutPants
17f5642ebd Add parent/sub-tags to tag cards (#1792) 2021-10-03 11:37:28 +11:00
WithoutPants
7464454da5 Allow setting metadata directory from UI (#1782) 2021-10-03 11:35:30 +11:00
WithoutPants
52193586de Don't show studio error message while loading (#1791) 2021-10-03 11:01:07 +11:00
bnkai
ea7a4f8d33 Fix Studio -> Stash ID filter (#1793) 2021-10-03 11:00:49 +11:00
InfiniteTF
73ea195668 Fix video player sizing (#1764)
* Fix video player sizing
* Add explanation
2021-10-01 17:30:43 +10:00
Tweeticoats
cca156b5f8 Add container_name to docker compose (#1689)
* Add container_name to docker compose
* Add ports to docker compose
* Provide instructions for DLNA
2021-10-01 16:26:32 +10:00
WithoutPants
ca0a8b00ec Only group by if using having clauses (#1750)
* Only group by if using having clauses
* Change tag sorting SQL
* Add sorting unit tests
2021-10-01 16:24:58 +10:00
WithoutPants
e3480531a7 Lightbox pan, zoom and display mode options (#1708)
* Rewrite lightbox code
* Don't render offscreen images
* Scroll up to zoom in
* Support touch gestures
* Add reset zoom button
* Align top of image on original/fit horizontal
* Add scrollmode setting
* Add scale up option
* Add option to maintain zoom when transisitioning
* Fix image slideshow wrap around
* Wrap around on previous on first page/image
* Fix single page issues
* Fix two-pointer zoom mode incorrectly activated
2021-10-01 11:52:32 +10:00
gitgiggety
dabf5acefe Filter tag by hierarchy (#1746)
* Add API support for filtering tags by parent / children
* Add parent & child tags filters for tags to UI
* Add API support for filtering tags by parent / child count
* Add parent & child count filters for tags to UI
* Update db generator
* Add missing build tag
* Add unit tests

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-10-01 11:50:06 +10:00
kermieisinthehouse
df2c9e9754 Fix broken links (#1787) 2021-09-30 10:22:25 +10:00
WithoutPants
9e541956f2 Don't preload videos (#1780) 2021-09-30 09:50:30 +10:00
SmallCoccinelle
bc6d04dc2a Remove new-from-rev from the linter (#1783)
* Fix all revive warnings in the code base

All of these are of the form

```
var Identifier Type = Expr
```

where the `Type` is known from the output of `Expr` and can be omitted
as a result.

* Handle unchecked errors

* Remove new-from-rev

Since the project passes all linter checks now, including older
revisions, we can remove new-from-rev. While here, reorder the linter
config file, and move the enabled linters up and settings down.

* Fix failing test cases

Studio & Performer export tests use local time rather than UTC. This
fixes the test cases so integration test
passes.
2021-09-28 09:29:45 +10:00
SmallCoccinelle
b94eecae24 Fix integration-testing in the build action (#1784)
When the golangci workflow action was added, we moved large parts of
validation to the parallel action. However, integration testing still
has to happen on the main build action, as it isn't covered by the
validation action.

Reenable the `it` target through docker container build.

This fixes backend test errors sneaking through the build action.
2021-09-28 09:28:05 +10:00
WithoutPants
6ce57a9a43 Fix styling for scene duration on empty system (#1781) 2021-09-27 16:57:24 +10:00
WithoutPants
94d192b833 Don't scan zero length files or directories (#1779)
* Don't scan zero length files or directories
2021-09-27 16:49:30 +10:00
WithoutPants
479bd438df Accept svg for tag images in file selector (#1778)
* Accept svg for tag images
2021-09-27 12:27:05 +10:00
WithoutPants
b957a87a78 Fix scene marker sort by scene updated SQL error (#1777) 2021-09-27 12:26:38 +10:00
gitgiggety
be94e52f21 Add movie count to performer and studio card (#1760)
* Add movies and movie_count properties to Performer type

Extend the GraphQL API to allow getting the movies and movie count by
performer.

* Add movies count to performer card

* Add movies and movie_count properties to Studio type

Extend the GraphQL API to allow getting the movies and movie count by
studio.

* Add movies count to studio card
2021-09-27 11:31:49 +10:00
Eng Zer Jun
62af723017 refactor: move from io/ioutil to io and os package (#1772)
The io/ioutil package has been deprecated as of Go 1.16, see
https://golang.org/doc/go1.16#ioutil. This commit replaces the existing
io/ioutil functions with their new definitions in io and os packages.

Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2021-09-27 10:55:23 +10:00
SmallCoccinelle
a2cce0ba77 Add golangci-lint workflow (#1759)
* Add golangci-lint workflow

* Add a bit more lenient linter timeout

1 Minute isn't always enough, so bump to 3.

* Document golangci, add make target

Document how to get golangci-lint in the README file. While here,
provide a QOL target in the makefile
for the linter, and make it part of validation.

* Introduce .golangci.yml

This is the default golangci-lint configuration file location. Use it.

Move configuration into the yaml file, and enable the default set of
linters; we know we pass most of those.

* Add gofmt and revive to golangci-lint

Read the golangci-lint source code to figure out the configuration format.

Copy the configuration from `revive.toml` into the linter configuration.

* Do not set simplify on gofmt

The project currently runs without simplify. So for consistency, don't
make that a requirement for the linter.

* Add new-from-rev

Older issues should not be considered a failure for new PRs and issues.
Use new-from-from to make the current develop as the point-in-time for
when we consider errors.

Once in the tree, we can go and fix the older errors in separate
patches, taking a little bit at a time.

* Move to golangci-lint

Rewrite the way we run targets in the makefile, so it is split between
frontend and backend.

Use the frontend build steps in build.yml

Update README to reflect the new world order.

* Remove check-gofmt.sh

The tool now runs as part of golangci-lint, in particular through the
'validate' target in the Makefile.

* Remove targets for golangci-lint

Fold these targets into the `lint` target. While here, update README.
2021-09-27 10:41:59 +10:00
gitgiggety
1d04b550b9 Movie card visual consistency (#1758)
* Support getting scenes on movies in the API

* Make movie card visually consistent with scene etc

* Add date
* Add synopsis
* Show scene count with hover listing the scenes
* Move scene index to button

* Move scene number to own section

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-09-26 13:52:08 +10:00
gitgiggety
5fdab995f5 Fix list state navigation (#1765)
* Fix sort direction being lost filtering scenes list

Fix thevsort direction being lost on the scenes (& images & galleries?)
lists when filtering the list by using the "badges" on the tags /
performers / ... popovers.

* Fix using back button not clearing filters to empty

Fix (re)setting of a lists criterion based on the URL in case the new
URL doesn't have criterions / "c" set. When it's unset it should be
assumed the criterions must be empty, instead of maintaining the current
criterions (which could be populated wirh the old criterions when the
list was shown before).

* Set page to 1 on list when applying new filter
2021-09-26 13:51:45 +10:00
gitgiggety
f977d0e18a Add date & details to gallery card (#1763)
* Add date & details to gallery card

Make the gallery card visually consistent with the scenes card. So move
the "X images" to a button at the bottom (where tags, performers and
scenes are shown as well) and add the date & details to the card.
2021-09-26 13:16:49 +10:00
bnkai
ba2a79700a Remove last jwt-go references (#1770) 2021-09-26 10:11:42 +10:00
InfiniteTF
2f664fe826 Disable vips in Windows (#1767) 2021-09-26 10:01:45 +10:00
WithoutPants
b14d5c5650 Migrate to golang-jwt (#1757) 2021-09-24 11:57:49 +10:00
SmallCoccinelle
a9e2a590b2 Lint checks phase 2 (#1747)
* Log 3 unchecked errors

Rather than ignore errors, log them at
the WARNING log level.

The server has been functioning without these, so assume they are not at
the ERROR level.

* Log errors in concurrency test

If we can't initialize the configuration, treat the test as a failure.

* Undo the errcheck on configurations for now.

* Handle unchecked errors in pkg/manager

* Resolve unchecked errors

* Handle DLNA/DMS unchecked errors

* Handle error checking in concurrency test

Generalize config initialization, so we can initialize a configuration
without writing it to disk.

Use this in the test case, since otherwise the test fails to write.

* Handle the remaining unchecked errors

* Heed gosimple in update test

* Use one-line if-initializer statements

While here, fix a wrong variable capture error.

* testing.T doesn't support %w

use %v instead which is supported.

* Remove unused query builder functions

The Int/String criterion handler functions are now generalized.

Thus, there's no need to keep these functions around anymore.

* Mark filterBuilder.addRecursiveWith nolint

The function is useful in the future and no other refactors are looking
nice.

Keep the function around, but tell the linter to ignore it.

* Remove utils.Btoi

There are no users of this utility function

* Return error on scan failure

If we fail to scan the row when looking for the
unique checksum index, then report the error upwards.

* Fix comments on exported functions

* Fix typos

* Fix startup error
2021-09-23 17:15:50 +10:00
InfiniteTF
9cb1eccadb Improve image scanning performance and thumbnail generation (#1655)
* Improve image scanning performance and thumbnail generation
* Add vips-tools to build image
* Add option to write generated thumbnails to disk
* Fallback to image if thumbnail generation fails

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-09-23 15:22:14 +10:00
kermieisinthehouse
1e8a8efe3e Escape all regexp control characters (#1753)
* Escape all regexp control characters
* Add #1752 unit tests

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-09-23 13:08:08 +10:00
WithoutPants
cceaff780b Update build.yml 2021-09-23 12:41:39 +10:00
WithoutPants
8d9eb7f1e4 Create temp UI file so that embed doesn't fail 2021-09-23 11:13:32 +10:00
SmallCoccinelle
4a0c4c4847 Reorder waitgroup completion (#1748)
Rather than passing a pointer to a waitgroup into task.Start(..)
functions, handle the waitgroup.Done() at the callsite.

This makes waitgroup handling local to its definition rather than it
being spread out over multiple files. Tasks now simply execute, and
the policy of waiting on them is handled by the caller.
2021-09-22 13:22:59 +10:00
WithoutPants
56111433a1 Replace packr with go embed (#1751)
* Embed performer images
* Embed schema migrations
* Update dependencies
* Embed UI
* Remove remaining packr references
2021-09-22 13:08:34 +10:00
WithoutPants
f292238e7f Only close DB if not nil (#1749) 2021-09-21 21:57:50 +10:00
dependabot[bot]
25182997f1 Bump axios from 0.21.1 to 0.21.2 in /ui/v2.5 (#1745)
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-21 20:32:55 +10:00
InfiniteTF
a4ed9515c7 Support subpaths when serving stash through a reverse proxy (#1719)
* Support subpaths when serving stash through a reverse proxy
* Add README documentation

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-09-21 14:12:10 +10:00
WithoutPants
f3c8407c40 Cancel in progress builds when new action starts 2021-09-21 12:05:46 +10:00
gitgiggety
3e526a49a4 Add indexes for path and checksum to images (#1740)
* Add indexes for path and checksum to images

The scenes table has unique indexes/constraints on path and checksum
colums. The images table doesn't, which doesn't really make sense, as
scanning uses these colums extensively which warrents an index, and both
should be unique as well.

Adding these indexes thus heavily improves the scanning tasks
performance. On a database containing 4700 images a (re)scan of those
4700 files, which thus shouldn't do anything, took 1.2 seconds, with the
indexes added this only takes 0.4 seconds. Taking the same test on a
generated database containing 4M images + the actual 4700 images took 26
minutes for a rescan, and with the index existing also only takes 0.4
seconds.

* Add images.checksum unique constraint in code with fallback

Work around the issue where in some cases duplicate images (/checksums
on images) might exist. This as discussed in #1740 by creating the index
on startup and in case of an error logging the duplicates. This so the
users where this scenario exists can correct the database (by searching
on the logged checksum(s) and removing the duplicates) and after a
restart the unique index / constraint will still be created. In case
when creating the unique index fails a "normal" / non-unique index is
created as surrogate so the user will still get the performance benefit
(for example during scanning) without being forced to remove the
duplicates and restart beforehand. This surrogate is also automatically
cleaned up after the unique index is succesfully created.
2021-09-21 11:48:52 +10:00
SmallCoccinelle
b7c229dc70 Set NORMAL sync on sqlite3 (#1684)
NORMAL sync is safe when using WAL journaliing.

It cuts the amount of sync calls to the disk, resulting in faster write
operation. On power loss, the database will perhaps lose some ongoing
commits, but that is all.

The expectation is that if the database lives on the same disk as the
stash, this could help performance quite a bit under heavier operation.
2021-09-21 11:38:56 +10:00
SmallCoccinelle
87709fd018 Errcheck phase 1 (#1715)
* Avoid redundant logging in migrations

Return the error and let the caller handle the logging of the error if
needed.

While here, defer m.Close() to the function boundary.

* Treat errors as values

Use %v rather than %s and pass the errors directly.

* Generate a wrapped error on stat-failure

* Log 3 unchecked errors

Rather than ignore errors, log them at
the WARNING log level.

The server has been functioning without these, so assume they are not at
the ERROR level.

* Propagate errors upward

Failure in path generation was ignored. Propagate the errors upward the
call stack, so it can be handled at the level of orchestration.

* Warn on errors

Log errors rather than quenching them.

Errors are logged at the Warn-level for now.

* Check error when creating test databases

Use the builtin log package and stop the program fatally on error.

* Add warnings to uncheck task errors

Focus on the task system in a single commit, logging unchecked
errors as warnings.

* Warn-on-error in API routes

Look through the API routes, and make sure errors are being logged if
they occur. Prefer the Warn-log-level because none of these has proven
to be fatal in the system up until now.

* Propagate error when adding Util API

* Propagate error on adding util API

* Return unhandled error

* JS log API: propagate and log errors

* JS Plugins: log GQL addition failures.

* Warn on failure to write to stdin

* Warn on failure to stop task

* Wrap viper.BindEnv

The current viper code only errors if no name is provided, so it should
never fail. Rewrite the code flow to factor through a panic-function.

This removes error warnings from this part of the code.

* Log errors in concurrency test

If we can't initialize the configuration, treat the test as a failure.

* Warn on errors in configuration code

* Plug an unchecked error in gallery zip walking

* Warn on screenshot serving failure

* Warn on encoder screenshot failure

* Warn on errors in path-handling code

* Undo the errcheck on configurations for now.

* Use one-line initializers where applicable

rather than using

  err := f()
  if err!= nil { ..

prefer the shorter

  if err := f(); err != nil { ..

If f() isn't too long of a name, or wraps a function with a body.
2021-09-21 09:34:25 +10:00
bnkai
af6232ec97 Fix Query for Scene Markers (#1743) 2021-09-20 09:52:19 +10:00
liquid-flow
919249f851 Fix Scene Player CLS issue (#1739) 2021-09-19 11:31:52 +10:00
liquid-flow
13fda2ad85 Fix scene edit panel layout (#1737)
* Fix scene edit panel layout
2021-09-19 11:07:23 +10:00
gitgiggety
82e4ad4130 Execute Gallery.Create.Post plugin hook during scan (#1731)
* Execute Gallery.Create.Post plugin hook during scan

Fix issue where Gallery.Create.Post hook is not executed when a new
gallery is created during scan, when the gallery is created based on the
folder.

* Fix Gallery.Create.Post hook being invoked in transaction

Invoke the Gallery.Create.Post hook during zip scan after the
transaction has been committed. This is necessary to allow the plugin to
access the gallery (using GraphQL API). Otherwise the API obviously uses
a different database transaction which can't find the gallery as it
isn't committed yet.
2021-09-19 10:28:34 +10:00
gitgiggety
b83ce29ac4 Scraper log improvements (#1741)
* Fix logs from scraper and plugins not being shown in UI

Using `logger.` in the logger package to write logs is "incorrect". This
as the package contains a variable named `logger` which contains the
logrus instance. So instead of the log line being handled by the custom
log implementation / wrapper which makes sure the lines are shown in the
UI as well, it's written to logrus directly meaning the wrapper is
skipped.

This "issue" is obviously triggered because in any other place
`logger.X` can be used and it will used the custom logger package /
wrapper which works fine.

* Add plugin / scraper name to logging output

Indicate which plugin / scraper wrote a log message by including its
name to the `[Scrape]` prefix.

* Add missing addLogItem call
2021-09-19 10:06:34 +10:00
WithoutPants
66f92c5dcc Fix autotag tag alias (#1713)
* Add unit tests for tag alias
* Fix tag alias autotag
* Don't autotag non-path galleries
* Add studio alias tests
2021-09-17 09:23:56 +10:00
stg-annon
d29699fa30 Support scraper logging to specific log levels (#1648)
* init scrapper log levels
* Refactor plugin logging

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-09-17 09:09:44 +10:00
WithoutPants
501ed7c2c2 Make hierarchical criterion depth input optional (#1733) 2021-09-16 20:41:07 +10:00
MartinWe13
e0c910d9e8 Adjustments German translation (#1730)
* Update de-DE.json

A few (first) adjustments to the German translation. Partly the words are chosen too long and translated not very technically.
2021-09-16 12:10:08 +10:00
gitgiggety
f3119a6c38 Add AND, OR and NOT support to studio filter (#1726) 2021-09-16 12:09:23 +10:00
Still Hsu
23f852cd91 Add new zh-tw localizations & move non-legacy localization keys (#1693)
* Add new localizations & move non-legacy localization keys

Signed-off-by: Still Hsu <dev@stillu.cc>

* Fix sv-SE localization keys

Signed-off-by: Still Hsu <dev@stillu.cc>

* Fix duplicated key
* Add missing internationalisation

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2021-09-16 09:04:50 +10:00
gitgiggety
2274db16b7 Generate screenshot images for markers (#1604)
* Generate screenshot images for markers

In some scenarios it might not be possible to use the preview video or
image of markers, i.e. when only static images are supported like in
Kodi. So generate a static screenshot as well.

* Make generating animated and static image optional for markers
* Use screenshot for markers when preview type is set to static image
2021-09-15 12:27:05 +10:00
liquid-flow
f5e4e7742e [Bug Fix] Disable float-on-scroll JWPlayer feature on mobile (#1721)
* Disable video float on mobile
2021-09-15 10:13:45 +10:00
Perdition
3be23999ef Update sv-SE.json (#1722)
Fixes a bunch of awkward sounding terms and other errors.
2021-09-14 23:21:59 +10:00
liquid-flow
612ecb72fc [Bug Fix] Fix scene markers editor mobile UI (#1718)
* Fix mobile layout
2021-09-14 20:07:53 +10:00
gitgiggety
b78060d361 Add rating sorting to galleries (#1720)
* Add rating sorting to galleries

Fixes: #1717
2021-09-14 18:11:12 +10:00
WithoutPants
1a3a2f1f83 Scrape scene by name (#1712)
* Support scrape scene by name in configs
* Initial scene querying
* Add to manual
2021-09-14 14:54:53 +10:00
gitgiggety
565064b441 Make IHierarchicalLabeledIdCriterion backwards compatible (#1716)
Add some form of backwards compatibility in the UI for
`IHierarchicalLabeledIdCriterion`. With this backwards compatibility
it's possible to recall saved filters which use the tags filter.
Otherwise stuff would blow up because the saved filter has a different
structure than what the `IHierarchicalLabeledIdCriterion` expects.
2021-09-11 10:01:57 +10:00
WithoutPants
13a289a4a8 Fix studio scene count sort (#1714) 2021-09-09 19:44:02 +10:00
gitgiggety
04e5ac9c2f Studio aliases (#1660)
* Add migration to create studio aliases table
* Refactor studioQueryBuilder.Query to use filterBuilder
* Expand GraphQL API with aliases support for studio
* Add aliases support for studios to the UI
* List aliases in details panel
* Allow editing aliases in edit panel
* Add 'aliases' filter when searching
* Find studios by alias in filter / select
* Add auto-tagging based on studio aliases
* Support studio aliases for filename parsing
* Support importing and exporting of studio aliases
* Search for studio alias as well during scraping
2021-09-09 18:13:42 +10:00
gitgiggety
c91ffe1e58 Tag hierarchy (#1519)
* Add migration script for tag relations table
* Expand hierarchical filter features

Expand the features of the hierarchical multi input filter with support
for using a relations table, which only has parent_id and child_id
columns, and support adding an additional intermediate table to join on,
for example for scenes and tags which are linked by the scenes_tags
table as well.

* Add hierarchical filtering for tags
* Add hierarchical tags support to scene markers

Refactor filtering of scene markers to filterBuilder and in the process
add support for hierarchical tags as well.

* List parent and child tags on tag details page
* Support setting parent and child tags

Add support for setting parent and child tags during tag creation and
tag updates.

* Validate no loops are created in tags hierarchy
* Update tag merging to support tag hierarcy
* Add unit tests for tags.EnsureUniqueHierarchy
* Fix applying recursive to with clause

The SQL `RECURSIVE` of a `WITH` clause only needs to be applied once,
imediately after the `WITH`. So this fixes the query building to do just
that, automatically applying the `RECURSIVE` keyword when any added with
clause is added as recursive.

* Rename hierarchical root id column
* Rewrite hierarchical filtering for performance

Completely rewrite the hierarchical filtering to optimize for
performance. Doing the recursive query in combination with a complex
query seems to break SQLite optimizing some things which means that the
recursive part might be 2,5 second slower than adding a static
`VALUES()` list. This is mostly noticable in case of the tag hierarchy
where setting an exclusion with any depth (or depth: all) being applied
has this performance impact of 2,5 second. "Include" also suffered this
issue, but some rewritten query by joining in the *_tags table in one
pass and applying a `WHERE x IS NOT NULL` filter did seem to optimize
that case. But that optimization isn't applied to the `IS NULL` filter
of "exclude". Running a simple query beforehand to get all (recursive)
items and then applying them to the query doesn't have this performance
penalty.

* Remove UI references to child studios and tags
* Add parents to tag export
* Support importing of parent relationship for tags
* Assign stable ids to parent / child badges
* Silence Apollo warning on parents/children fields on tags

Silence warning triggered by Apollo GraphQL by explicitly instructing it
to use the incoming parents/children values. By default it already does
this, but it triggers a warning as it might be unintended that it uses
the incoming values (instead of for example merging both arrays).
Setting merge to false still applies the same behaviour (use only
incoming values) but silences the warning as it's explicitly configured
to work like this.

* Rework detecting unique tag hierarchy

Completely rework the unique tag hierarchy to detect invalid hierarchies
for which a tag is "added in the middle". So when there are tags A <- B
and A <- C, you could previously edit tag B and add tag C as a sub tag
without it being noticed as parent A being applied twice (to tag C).
While afterwards saving tag C would fail as tag A was applied as parent
twice. The updated code correctly detects this scenario as well.

Furthermore the error messaging has been reworked a bit and the message
now mentions both the direct parent / sub tag as well as the tag which
would results in the error. So in aboves example it would now show the
message that tag C can't be applied because tag A already is a parent.

* Update relations on cached tags when needed

Update the relations on cached tags when a tag is created / updated /
deleted so these always reflect the correct state. Otherwise (re)opening
a tag might still show the old relations untill the page is fully
reloaded or the list is navigated. But this obviously is strange when
you for example have tag A, create or update tag B to have a relation to
tag A, and from tags B page click through to tag A and it doesn't show
that it is linked to tag B.
2021-09-09 14:58:43 +10:00
SmallCoccinelle
82a41e17c7 Avoid wrapping strings.Replace in Contains (#1710)
The strings.Replace function counts the number of replacements. If 0,
the original string is returned. Hence, there is no need to check if a
replacement will happen before doing the work.
2021-09-09 14:10:39 +10:00
SmallCoccinelle
4b00d24248 Remove unused (#1709)
* Remove stuff which isn't being used

Some fields, functions and structs aren't in use by the project. Remove
them for janitorial reasons.

* Remove more unused code

All of these functions are currently not in use. Clean up the code by
removal, since the version control has the code if need be.

* Remove unused functions

There's a large set of unused functions and variables in the code base.
Remove these, so it clearer what code to support going forward.

Dead code has been eliminated.

Where applicable, comment const-sections in tests, so reserved
identifiers are still known.

* Fix use-def of tsURL

The first def of tsURL doesn't matter because there's no use before
we hit the 2nd def.

* Remove dead code assignment

Setting logFile = "" is effectively dead code, because there's no use
of it later.

* Comment out found

The variable 'found' is dead in the function (because no post-process
action is following it). Comment it for now.

* Comment dead code in tests

These might provide hints as to what isn't covered at the moment.

* Dead code removal

In the case of constants where iota is involved, move the iota so it
matches the current key values.

This avoids problems with persistently stored key IDs.
2021-09-09 14:10:08 +10:00
kermieisinthehouse
265d5f4c70 Apple Silicon Support, Bump Go to 1.17, refactor docker/build/x86_64/Dockerfile (#1646)
* Bump Go to 1.17, refactor build/x86_64 Dockerfile to make better use of multi-stage
* Bump to 1.17 from 1.16
* Bump packr version, provide needed legacy env var
* Add apple silicon support, fix macos build chain
* Update unused travis ci
2021-09-08 15:30:15 +10:00
SmallCoccinelle
4545da9af0 Avoid redundant break statements (#1705)
In Go, a switch-case automatically breaks on end. This makes
the break statement redundant.
2021-09-08 11:23:21 +10:00
SmallCoccinelle
e7f6cb22b7 Error strings noncapitalized (#1704)
* Fix error string capitalization

Error strings often follow another string. Hence, they should not be
capitalized, unless referencing a name.

* Uncapitalize more error strings

While here, use %v on the error directly, which makes it easier to wrap
the error later with %w if need be.

* Uncapitalize more error strings

While here, rename Url to URL as a nitpick.
2021-09-08 11:23:10 +10:00
SmallCoccinelle
d2a0a8fe4c Correct error propagation (#1703)
Rename an inner error to imageErr. This avoids a shadowing of an error
in the following lines which aren't returned otherwise.
2021-09-08 11:22:56 +10:00
Alpaca Serious
b482fbc796 Swedish Translation (#1691)
* Added Swedish translation
* Update SettingsInterfacePanel.tsx
* Update index.ts
* Update sv-SE.json
2021-09-08 11:08:07 +10:00
SmallCoccinelle
a3f38d8edf When stopping, close the database (#1686)
* When stopping, close the database

This patch is likely to cause errornous behavior in the application.
This is due to the fact that the application doesn't gracefully shut
down, but is forcefully terminated.

However, the purpose is to uncover what needs to be done, to make
it a more graceful shutdown.
2021-09-07 14:28:40 +10:00
fnoopv
651d2e6373 Add missing translation (#1701) 2021-09-07 13:30:59 +10:00
SmallCoccinelle
b76283df08 Fix data race in progress_test (#1696)
The call to p.ExecuteTask happens in a separate go-routine. It writes,
under m.mutex, into the job's details. However, the test goroutine
itself reads j.Details which is a read race.

Protect the reads in the test by the lock.

This makes `package job` pass `go test -race`
2021-09-07 13:30:26 +10:00
SmallCoccinelle
489db34db2 Remove assignments to _ (#1685)
It's a no-op if we are not using it, so it can be safely removed.
2021-09-07 13:18:32 +10:00
gitgiggety
b2b05fb332 Add Movie option to Scene bulk edit (#1676)
* Add Movie option to Scene bulk edit
2021-09-07 12:44:18 +10:00
gitgiggety
7a468413da Add movies tab to Studios and Performers page (#1675)
* Add movies tab to Studios page
* Add performers filter to movies
* Add movies tab to performers page
2021-09-07 12:16:33 +10:00
WithoutPants
4625e1f955 Unify scrape refactor (#1630)
* Unify scraped types
* Make name fields optional
* Unify single scrape queries
* Change UI to use new interfaces
* Add multi scrape interfaces
* Use images instead of image
2021-09-07 11:54:22 +10:00
WithoutPants
04e146f290 Merge pull request #1700 from stashapp/master
Merge master to develop for version tag
2021-09-06 17:03:37 +10:00
WithoutPants
ab10cf8251 Merge pull request #1697 from stashapp/develop
Merge develop to master for 0.9
2021-09-06 12:47:07 +10:00
1361 changed files with 163819 additions and 44249 deletions

View File

@@ -15,16 +15,10 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Packr2 artifacts
**/*-packr.go
# GraphQL generated output
pkg/models/generated_*.go
ui/v2.5/src/core/generated-*.tsx
# packr generated files
*-packr.go
####
# Jetbrains
####
@@ -60,4 +54,4 @@ ui/v2.5/build
stash
dist
docker
docker

View File

@@ -8,8 +8,12 @@ on:
release:
types: [ published ]
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
env:
COMPILER_IMAGE: stashapp/compiler:4
COMPILER_IMAGE: stashapp/compiler:5
jobs:
build:
@@ -43,43 +47,52 @@ jobs:
- name: Cache go build
uses: actions/cache@v2
env:
cache-name: cache-go-cache
# increment the number suffix to bump the cache
cache-name: cache-go-cache-1
with:
path: .go-cache
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('go.mod', '**/go.sum') }}
- name: Start build container
env:
official-build: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/develop') || (github.event_name == 'release' && github.ref != 'refs/tags/latest_develop') }}
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
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 --env OFFICIAL_BUILD=${{ env.official-build }} -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"
run: docker exec -t build /bin/bash -c "make validate-frontend"
# 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"
# Static validation happens in the linter workflow in parallel to this workflow
# Run Dynamic validation here, to make sure we pass all the projects integration tests
#
# create UI file so that the embed doesn't fail
- name: Test Backend
run: |
mkdir -p ui/v2.5/build
touch ui/v2.5/build/index.html
docker exec -t build /bin/bash -c "make 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"
run: docker exec -t build /bin/bash -c "make ui"
- 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-osx-intel"
docker exec -t build /bin/bash -c "make cross-compile-osx-applesilicon"
docker exec -t build /bin/bash -c "make cross-compile-linux"
docker exec -t build /bin/bash -c "make cross-compile-linux-arm64v8"
docker exec -t build /bin/bash -c "make cross-compile-linux-arm32v7"
@@ -94,7 +107,7 @@ jobs:
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'}}
@@ -118,13 +131,13 @@ jobs:
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' }}
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
uses: marvinpinto/action-automatic-releases@v1.1.2
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
@@ -133,13 +146,14 @@ jobs:
title: "${{ env.STASH_VERSION }}: Latest development build"
files: |
dist/stash-osx
dist/stash-osx-applesilicon
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
@@ -148,6 +162,7 @@ jobs:
allow_override: true
files: |
dist/stash-osx
dist/stash-osx-applesilicon
dist/stash-win.exe
dist/stash-linux
dist/stash-linux-arm64v8
@@ -155,7 +170,7 @@ jobs:
dist/stash-pi
CHECKSUMS_SHA1
gzip: false
- name: Development Docker
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
env:
@@ -163,7 +178,7 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
docker info
docker buildx create --name builder --use
docker buildx inspect --bootstrap
@@ -177,7 +192,7 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
docker run --rm --privileged docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64
docker info
docker buildx create --name builder --use
docker buildx inspect --bootstrap

60
.github/workflows/golangci-lint.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Lint (golangci-lint)
on:
push:
tags:
- v*
branches:
- master
- develop
pull_request:
env:
COMPILER_IMAGE: stashapp/compiler:5
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout
run: git fetch --prune --unshallow --tags
- name: Pull compiler image
run: docker pull $COMPILER_IMAGE
- 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: Generate Backend
run: docker exec -t build /bin/bash -c "make generate-backend"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v2
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v1.42.1
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
args: --modules-download-mode=vendor --timeout=3m
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
# Optional: if set to true then the action will use pre-installed Go.
# skip-go-installation: true
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
# skip-pkg-cache: true
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
# skip-build-cache: true
- name: Cleanup build container
run: docker rm -f -v build

6
.gitignore vendored
View File

@@ -15,16 +15,10 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Packr2 artifacts
**/*-packr.go
# GraphQL generated output
pkg/models/generated_*.go
ui/v2.5/src/core/generated-*.tsx
# packr generated files
*-packr.go
####
# Jetbrains
####

91
.golangci.yml Normal file
View File

@@ -0,0 +1,91 @@
# options for analysis running
run:
timeout: 3m
modules-download-mode: vendor
linters:
disable-all: true
enable:
# Default set of linters from golangci-lint
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
# Linters added by the stash project.
- dogsled
- errorlint
# - exhaustive
- exportloopref
- gocritic
# - goerr113
- gofmt
# - gomnd
# - ifshort
- misspell
# - nakedret
- noctx
- revive
- rowserrcheck
- sqlclosecheck
# Project-specific linter overrides
linters-settings:
gofmt:
simplify: false
errorlint:
# Disable errorf because there are false positives, where you don't want to wrap
# an error.
errorf: false
asserts: true
comparison: true
revive:
ignore-generated-header: true
severity: error
confidence: 0.8
error-code: 1
warning-code: 1
rules:
- name: blank-imports
disabled: true
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
disabled: true
- name: if-return
disabled: true
- name: increment-decrement
- name: var-naming
disabled: true
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
disabled: true
- name: indent-error-flow
disabled: true
- name: errorf
- name: empty-block
disabled: true
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
rowserrcheck:
packages:
- github.com/jmoiron/sqlx

View File

@@ -1,64 +0,0 @@
project_name: stash
before:
hooks:
- go mod download
builds:
- binary: stash-win
ldflags:
- "-extldflags '-static'"
env:
- CGO_ENABLED=1
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
flags:
- -tags
- extended
goos:
- windows
goarch:
- amd64
- binary: stash-osx
env:
- CGO_ENABLED=1
- CC=o64-clang
- CXX=o64-clang++
flags:
- -tags
- extended
goos:
- darwin
goarch:
- amd64
- binary: stash-linux
env:
- CGO_ENABLED=1
flags:
- -tags
- extended
goos:
- linux
goarch:
- amd64
archive:
format: tar.gz
format_overrides:
- goos: windows
format: zip
name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}"
replacements:
amd64: 64bit
386: 32bit
arm: ARM
arm64: ARM64
darwin: macOS
linux: Linux
windows: Windows
openbsd: OpenBSD
netbsd: NetBSD
freebsd: FreeBSD
dragonfly: DragonFlyBSD
files:
- README.md
- LICENSE
release:
draft: true

View File

@@ -1,118 +0,0 @@
if: tag != latest_develop # dont build for the latest_develop tagged version
dist: xenial
git:
depth: false
language: go
go:
- 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
- make generate
- CI=false make ui-validate ui-only
#- go get -v github.com/mgechev/revive
script:
# left lint off to avoid getting extra dependency
#- make lint
- make fmt-check vet it
after_success:
- docker pull stashapp/compiler:4
- sh ./scripts/cross-compile.sh
- git describe --tags --exclude latest_develop | tee CHECKSUMS_SHA1
- sha1sum dist/stash-* | sed 's/dist\///g' | tee -a CHECKSUMS_SHA1
- 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then sh ./scripts/upload-pull-request.sh; fi'
before_deploy:
# push the latest tag when on the develop branch
- if [ "$TRAVIS_BRANCH" = "develop" ]; then git tag -f latest_develop; git push -f --tags; fi
- 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
deploy:
# latest develop release
- provider: releases
# use the v2 release provider for proper release note setting
edge: true
api_key:
secure: tGJ2q62CfPdayid2qEtW2aGRhMgCl3lBXYYQqp3eH0vFgIIf6cs7IDX7YC/x3XKMEQ/iMLZmtCXZvSTqNrD6Sk7MSnt30GIs+4uxIZDnnd8mV5X3K4n4gjD+NAORc4DrQBvUGrYMKJsR5gtkH0nu6diWb1o1If7OiJEuCPRhrmQYcza7NUdABnA9Z2wn2RNUV9Ga33WUCqLMEU5GtNBlfQPiP/khCQrqn/ocR6wUjYut3J6YagzqH4wsfJi3glHyWtowcNIw1LZi5zFxHD/bRBT4Tln7yypkjWNq9eQILA6i6kRUGf7ggyTx26/k8n4tnu+QD0vVh4EcjlThpU/LGyUXzKrrxjRwaDZnM0oYxg5AfHcBuAiAdo0eWnV3lEWRfTJMIVb9MPf4qDmzR4RREfB5OXOxwq3ODeCcJE8sTIMD/wBPZrlqS/QrRpND2gn2X4snkVukN9t9F4CMTFMtVSzFV7TDJW5E5Lq6VEExulteQhs6kcK9NRPNAaLgRQAw7X9kVWfDtiGUP+fE2i8F9Bo8bm7sOT5O5VPMPykx3EgeNg1IqIgMTCsMlhMJT4xBJoQUgmd2wWyf3Ryw+P+sFgdb5Sd7+lFgJBjMUUoOxMxAOiEgdFvCXcr+/Udyz2RdtetU1/6VzXzLPcKOw0wubZeBkISqu7o9gpfdMP9Eq00=
file:
- dist/stash-osx
- dist/stash-win.exe
- dist/stash-linux
- dist/stash-linux-arm64v8
- dist/stash-linux-arm32v7
- dist/stash-pi
- CHECKSUMS_SHA1
skip_cleanup: true
overwrite: true
name: "${STASH_VERSION}: Latest development build"
release_notes: "**${RELEASE_DATE}**\n This is always the latest committed version on the develop branch. Use as your own risk!"
prerelease: true
on:
repo: stashapp/stash
branch: develop
# docker image build for develop release
- provider: script
skip_cleanup: true
script: bash ./docker/ci/x86_64/docker_push.sh development
on:
repo: stashapp/stash
branch: develop
# official master release - only build when tagged
- provider: releases
api_key:
secure: tGJ2q62CfPdayid2qEtW2aGRhMgCl3lBXYYQqp3eH0vFgIIf6cs7IDX7YC/x3XKMEQ/iMLZmtCXZvSTqNrD6Sk7MSnt30GIs+4uxIZDnnd8mV5X3K4n4gjD+NAORc4DrQBvUGrYMKJsR5gtkH0nu6diWb1o1If7OiJEuCPRhrmQYcza7NUdABnA9Z2wn2RNUV9Ga33WUCqLMEU5GtNBlfQPiP/khCQrqn/ocR6wUjYut3J6YagzqH4wsfJi3glHyWtowcNIw1LZi5zFxHD/bRBT4Tln7yypkjWNq9eQILA6i6kRUGf7ggyTx26/k8n4tnu+QD0vVh4EcjlThpU/LGyUXzKrrxjRwaDZnM0oYxg5AfHcBuAiAdo0eWnV3lEWRfTJMIVb9MPf4qDmzR4RREfB5OXOxwq3ODeCcJE8sTIMD/wBPZrlqS/QrRpND2gn2X4snkVukN9t9F4CMTFMtVSzFV7TDJW5E5Lq6VEExulteQhs6kcK9NRPNAaLgRQAw7X9kVWfDtiGUP+fE2i8F9Bo8bm7sOT5O5VPMPykx3EgeNg1IqIgMTCsMlhMJT4xBJoQUgmd2wWyf3Ryw+P+sFgdb5Sd7+lFgJBjMUUoOxMxAOiEgdFvCXcr+/Udyz2RdtetU1/6VzXzLPcKOw0wubZeBkISqu7o9gpfdMP9Eq00=
file:
- dist/stash-osx
- dist/stash-win.exe
- dist/stash-linux
- 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
overwrite: true
# don't write the body. To be done manually for now. In future we might
# want to generate the changelog or get it from a file
name: ${STASH_VERSION}
on:
repo: stashapp/stash
tags: true
# make sure we don't release using the latest_develop tag
condition: $TRAVIS_TAG != latest_develop
# docker image build for master release
- provider: script
skip_cleanup: true
script: bash ./docker/ci/x86_64/docker_push.sh latest
on:
repo: stashapp/stash
tags: true
# make sure we don't release using the latest_develop tag
condition: $TRAVIS_TAG != latest_develop

106
Makefile
View File

@@ -9,7 +9,7 @@ endif
ifdef IS_WIN
SEPARATOR := &&
SET := set
else
else
SEPARATOR := ;
SET := export
endif
@@ -23,9 +23,8 @@ ifdef OUTPUT
endif
export CGO_ENABLED = 1
export GO111MODULE = on
.PHONY: release pre-build install clean
.PHONY: release pre-build
release: generate ui build-release
@@ -42,16 +41,22 @@ ifndef STASH_VERSION
$(eval STASH_VERSION := $(shell git describe --tags --exclude latest_develop))
endif
ifndef OFFICIAL_BUILD
$(eval OFFICIAL_BUILD := false)
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)')
go build $(OUTPUT) -mod=vendor -v -tags "sqlite_omit_load_extension osusergo netgo" -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS)"
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/pkg/api.officialBuild=$(OFFICIAL_BUILD)')
go build $(OUTPUT) -mod=vendor -v -tags "sqlite_omit_load_extension osusergo netgo" $(GO_BUILD_FLAGS) -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: GO_BUILD_FLAGS := -trimpath
build-release: build
build-release-static: EXTRA_LDFLAGS := -extldflags=-static -s -w
build-release-static: GO_BUILD_FLAGS := -trimpath
build-release-static: build
# cross-compile- targets should be run within the compiler docker container
@@ -62,13 +67,21 @@ 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
cross-compile-osx-intel: export GOOS := darwin
cross-compile-osx-intel: export GOARCH := amd64
cross-compile-osx-intel: export CC := o64-clang
cross-compile-osx-intel: export CXX := o64-clang++
cross-compile-osx-intel: OUTPUT := -o dist/stash-osx
# can't use static build for OSX
cross-compile-osx: build-release
cross-compile-osx-intel: build-release
cross-compile-osx-applesilicon: export GOOS := darwin
cross-compile-osx-applesilicon: export GOARCH := arm64
cross-compile-osx-applesilicon: export CC := oa64e-clang
cross-compile-osx-applesilicon: export CXX := oa64e-clang++
cross-compile-osx-applesilicon: OUTPUT := -o dist/stash-osx-applesilicon
# can't use static build for OSX
cross-compile-osx-applesilicon: build-release
cross-compile-linux: export GOOS := linux
cross-compile-linux: export GOARCH := amd64
@@ -95,20 +108,26 @@ 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
clean:
packr2 clean
cross-compile-all:
make cross-compile-windows
make cross-compile-osx-intel
make cross-compile-osx-applesilicon
make cross-compile-linux
make cross-compile-linux-arm64v8
make cross-compile-linux-arm32v7
make cross-compile-pi
# Regenerates GraphQL files
.PHONY: generate
generate:
go generate -mod=vendor
generate: generate-backend generate-frontend
.PHONY: generate-frontend
generate-frontend:
cd ui/v2.5 && yarn run gqlgen
.PHONY: generate-backend
generate-backend:
go generate -mod=vendor
# Regenerates stash-box client files
.PHONY: generate-stash-box-client
generate-stash-box-client:
@@ -119,23 +138,13 @@ generate-stash-box-client:
fmt:
go fmt ./...
# Ensures that changed files have had gofmt run on them
.PHONY: fmt-check
fmt-check:
sh ./scripts/check-gofmt.sh
# Runs go vet on the project's source code.
.PHONY: vet
vet:
go vet -mod=vendor ./...
.PHONY: lint
lint:
revive -config revive.toml -exclude ./vendor/... ./...
golangci-lint run
# runs unit tests - excluding integration tests
.PHONY: test
test:
test:
go test -mod=vendor ./...
# runs all tests - including integration tests
@@ -148,23 +157,19 @@ it:
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
# installs UI dependencies. Run when first cloning repository, or if UI
# dependencies have changed
.PHONY: pre-ui
pre-ui:
cd ui/v2.5 && yarn install --frozen-lockfile
.PHONY: ui-only
ui-only: pre-build
.PHONY: ui
ui: pre-build
$(SET) REACT_APP_DATE="$(BUILD_DATE)" $(SEPARATOR) \
$(SET) REACT_APP_GITHASH=$(GITHASH) $(SEPARATOR) \
$(SET) REACT_APP_STASH_VERSION=$(STASH_VERSION) $(SEPARATOR) \
cd ui/v2.5 && yarn build
.PHONY: ui
ui: ui-only
packr2
.PHONY: ui-start
ui-start: pre-build
$(SET) REACT_APP_DATE="$(BUILD_DATE)" $(SEPARATOR) \
@@ -181,12 +186,19 @@ fmt-ui:
ui-validate:
cd ui/v2.5 && yarn run validate
# just repacks the packr files - use when updating migrations and packed files without
# rebuilding the UI
.PHONY: packr
packr:
packr2
# runs all of the tests and checks required for a PR to be accepted
.PHONY: validate
validate: ui-validate fmt-check vet lint it
validate: validate-frontend validate-backend
# runs all of the frontend PR-acceptance steps
.PHONY: validate-frontend
validate-frontend: ui-validate
# runs all of the backend PR-acceptance steps
.PHONY: validate-backend
validate-backend: lint it
# locally builds and tags a 'stash/build' docker image
.PHONY: docker-build
docker-build: pre-build
docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/build -f docker/build/x86_64/Dockerfile .

145
README.md
View File

@@ -1,80 +1,46 @@
# Stash
https://stashapp.cc
[![Build Status](https://travis-ci.org/stashapp/stash.svg?branch=master)](https://travis-ci.org/stashapp/stash)
[![Build](https://github.com/stashapp/stash/actions/workflows/build.yml/badge.svg?branch=develop&event=push)](https://github.com/stashapp/stash/actions/workflows/build.yml)
[![Docker pulls](https://img.shields.io/docker/pulls/stashapp/stash.svg)](https://hub.docker.com/r/stashapp/Stash 'DockerHub')
[![Go Report Card](https://goreportcard.com/badge/github.com/stashapp/stash)](https://goreportcard.com/report/github.com/stashapp/stash)
[![Discord](https://img.shields.io/discord/559159668438728723.svg?logo=discord)](https://discord.gg/2TsNFKt)
https://stashapp.cc
### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.**
![demo image](docs/readme_assets/demo_image.png)
**Stash is a locally hosted web-based app written in Go which organizes and serves your porn.**
* 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.
* Stash gathers 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 and sites.
* Stash 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.
* Stash provides statistics about performers, tags, studios and more.
You can [watch a SFW demo video](https://vimeo.com/545323354) to see it in action.
For further information you can [read the in-app manual](ui/v2.5/src/docs/en).
# Installing stash
# Installing Stash
## via Docker
<img src="docs/readme_assets/windows_logo.svg" width="100%" height="75"> Windows | <img src="docs/readme_assets/mac_logo.svg" width="100%" height="75"> MacOS| <img src="docs/readme_assets/linux_logo.svg" width="100%" height="75"> Linux | <img src="docs/readme_assets/docker_logo.svg" width="100%" height="75"> Docker
:---:|:---:|:---:|:---:
[Latest Release](https://github.com/stashapp/stash/releases/latest/download/stash-win.exe) <br /> <sup><sub>[Development Preview](https://github.com/stashapp/stash/releases/download/latest_develop/stash-win.exe)</sub></sup> | [Latest Release (Apple Silicon)](https://github.com/stashapp/stash/releases/latest/download/stash-osx-applesilicon) <br /> <sup><sub>[Development Preview (Apple Silicon)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-osx-applesilicon)</sub></sup> <br>[Latest Release (Intel)](https://github.com/stashapp/stash/releases/latest/download/stash-osx) <br /> <sup><sub>[Development Preview (Intel)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-osx)</sub></sup> | [Latest Release (amd64)](https://github.com/stashapp/stash/releases/latest/download/stash-linux) <br /> <sup><sub>[Development Preview (amd64)](https://github.com/stashapp/stash/releases/download/latest_develop/stash-linux)</sub></sup> <br /> [More Architectures...](https://github.com/stashapp/stash/releases/latest) | [Instructions](docker/production/README.md) <br /> <sup><sub> [Sample docker-compose.yml](docker/production/docker-compose.yml)</sub></sup>
Follow [this README.md in the docker directory.](docker/production/README.md)
## Pre-Compiled Binaries
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.
## Getting Started
Run the executable (double click the exe on windows or run `./stash-osx` / `./stash-linux` from the terminal on macOS / Linux) to get started.
*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 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.
Stash requires ffmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
# Usage
## 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.
Download and run Stash. It will prompt you for some configuration options and a directory to index (you can also do this step afterward)
**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).
**If you'd like to automatically retrieve and organize information about your entire library,** You will need to download some [scrapers](https://github.com/stashapp/stash/tree/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 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 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 same directory as the `config.yml` file, or the `~/.stash` directory. Stash detects these and starts up using HTTPS rather than HTTP.
# Customization
## Themes and CSS Customization
There is a [directory of community-created themes](https://github.com/stashapp/stash/wiki/Themes) on our Wiki, along with instructions on how to install them.
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).
# Support (FAQ)
Answers to other Frequently Asked Questions can be found [on our Wiki](https://github.com/stashapp/stash/wiki/FAQ)
@@ -82,79 +48,18 @@ Answers to other Frequently Asked Questions can be found [on our Wiki](https://g
For issues not addressed there, there are a few options.
* 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)
* Check the in-app documentation, in the top right corner of the app (also available [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en)
* Join the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support.
# Compiling From Source Code
# Customization
## Pre-requisites
## 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.
* [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/) - 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).
You can also make Stash interface fit your desired style with [Custom CSS snippets](https://github.com/stashapp/stash/wiki/Custom-CSS-snippets).
NOTE: You may need to run the `go get` commands outside the project directory to avoid modifying the projects module file.
# For Developers
## Environment
Pull requests are welcome!
### macOS
TODO
### Windows
1. Download and install [Go for Windows](https://golang.org/dl/)
2. Download and install [MingW](https://sourceforge.net/projects/mingw-w64/)
3. Search for "advanced system settings" and open the system properties dialog.
1. Click the `Environment Variables` button
2. Add `GO111MODULE=on`
3. Under system variables find the `Path`. Edit and add `C:\Program Files\mingw-w64\*\mingw64\bin` (replace * with the correct path).
NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
## Commands
* `make generate` - Generate Go and UI GraphQL files
* `make build` - Builds the binary (make sure to build the UI as well... see below)
* `make pre-ui` - Installs the UI dependencies. Only needs to be run once before building the UI for the first time, or if the dependencies are updated
* `make fmt-ui` - Formats the UI source code.
* `make ui` - Builds the frontend and the packr2 files
* `make packr` - Generate packr2 files (sub-target of `ui`. Use to regenerate packr2 files without rebuilding UI)
* `make vet` - Run `go vet`
* `make lint` - Run the linter
* `make fmt` - Run `go fmt`
* `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
1. Run `make generate` to create generated files
2. Run `make ui` to compile the frontend
3. Run `make build` to build the executable for your current platform
## Cross compiling
This project uses a modification of the [CI-GoReleaser](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment
where the app can be cross-compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following
command to open a bash shell to the container to poke around:
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashappdev/compiler:latest /bin/bash`
## Profiling
Stash can be profiled using the `--cpuprofile <output profile filename>` command line flag.
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>`
See [Development](docs/DEVELOPMENT.md) and [Contributing](docs/CONTRIBUTING.md) for information on working with the codebase, getting a local development setup, and contributing changes.

View File

@@ -1,61 +1,38 @@
# this dockerfile must be built from the top-level stash directory
# ie from top=level stash:
# docker build -t stash/build -f docker/build/x86_64/Dockerfile .
# This dockerfile should be built with `make docker-build` from the stash root.
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_lts.x | bash -
# prevent caching of the key
ADD https://dl.yarnpkg.com/debian/pubkey.gpg yarn.gpg
RUN cat yarn.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
rm yarn.gpg
RUN apt-get update && \
apt-get install -y nodejs yarn xz-utils --no-install-recommends || exit 1; \
rm -rf /var/lib/apt/lists/*;
ENV PACKR2_VERSION=2.0.2
ENV PACKR2_SHA=f95ff4c96d7a28813220df030ad91700b8464fe292ab3e1dc9582305c2a338d2
ENV PACKR2_DOWNLOAD_FILE=packr_${PACKR2_VERSION}_linux_amd64.tar.gz
ENV PACKR2_DOWNLOAD_URL=https://github.com/gobuffalo/packr/releases/download/v${PACKR2_VERSION}/${PACKR2_DOWNLOAD_FILE}
WORKDIR /
RUN wget ${PACKR2_DOWNLOAD_URL}; \
echo "$PACKR2_SHA $PACKR2_DOWNLOAD_FILE" | sha256sum -c - || exit 1; \
tar -xzf $PACKR2_DOWNLOAD_FILE -C /usr/bin/ packr2; \
rm $PACKR2_DOWNLOAD_FILE;
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN wget -O /ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
tar xf /ffmpeg.tar.xz && \
rm ffmpeg.tar.xz && \
mv /ffmpeg*/ /ffmpeg/
# copy the ui yarn stuff so that it doesn't get rebuilt every time
# Build Frontend
FROM node:alpine as frontend
RUN apk add --no-cache make
## cache node_modules separately
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
WORKDIR /stash
RUN yarn --cwd ui/v2.5 install --frozen-lockfile
RUN yarn --cwd ui/v2.5 install --frozen-lockfile.
COPY Makefile /stash/
COPY ./graphql /stash/graphql/
COPY ./ui /stash/ui/
RUN make generate-frontend
ARG GITHASH
ARG STASH_VERSION
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
COPY . /stash/
ENV GO111MODULE=on
RUN make generate
RUN make ui
# Build Backend
FROM golang:1.17-alpine as backend
RUN apk add --no-cache make alpine-sdk
WORKDIR /stash
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
COPY ./scripts /stash/scripts/
COPY ./vendor /stash/vendor/
COPY ./pkg /stash/pkg/
COPY --from=frontend /stash /stash/
RUN make generate-backend
ARG GITHASH
ARG STASH_VERSION
RUN make build
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/
# Final Runnable Image
FROM alpine:latest
RUN apk add --no-cache ca-certificates vips-tools ffmpeg
COPY --from=backend /stash/stash /usr/bin/
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999
CMD ["stash"]
ENTRYPOINT ["stash"]

View File

@@ -1,13 +1,13 @@
# Introduction
This dockerfile is used to build a stash docker container using the current source code.
This dockerfile is used to build a stash docker container using the current source code. This is ideal for testing your current branch in docker. Note that it does not include python, so python-based scrapers will not work in this image. The production docker images distributed by the project contain python and the necessary packages.
# Building the docker container
From the top-level directory (should contain `main.go` file):
```
docker build -t stash/build -f ./docker/build/x86_64/Dockerfile .
make docker-build
```

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM ubuntu:20.04 AS prep
FROM --platform=$BUILDPLATFORM alpine:latest AS binary
ARG TARGETPLATFORM
WORKDIR /
COPY stash-* /
@@ -8,15 +8,12 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-pi; \
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
FROM --platform=$TARGETPLATFORM alpine:latest AS app
COPY --from=binary /stash /usr/bin/
RUN apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg vips-tools && pip install --no-cache-dir mechanicalsoup cloudscraper
RUN ln -s /usr/bin/python3 /usr/bin/python
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999
CMD ["stash"]

View File

@@ -10,5 +10,5 @@ done
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
# 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/
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 --push $DOCKER_TAGS -f docker/ci/x86_64/Dockerfile dist/

View File

@@ -1,11 +1,6 @@
FROM golang:1.13.15
FROM golang:1.17
LABEL maintainer="stashappdev@gmail.com"
ENV PACKR2_VERSION=2.0.2
ENV PACKR2_SHA=f95ff4c96d7a28813220df030ad91700b8464fe292ab3e1dc9582305c2a338d2
ENV PACKR2_DOWNLOAD_FILE=packr_${PACKR2_VERSION}_linux_amd64.tar.gz
ENV PACKR2_DOWNLOAD_URL=https://github.com/gobuffalo/packr/releases/download/v${PACKR2_VERSION}/${PACKR2_DOWNLOAD_FILE}
LABEL maintainer="https://discord.gg/2TsNFKt"
# Install tools
RUN apt-get update && apt-get install -y apt-transport-https
@@ -18,10 +13,10 @@ RUN cat yarn.gpg | apt-key add - && \
rm yarn.gpg
RUN apt-get update && \
apt-get install -y automake autogen \
apt-get install -y automake autogen cmake \
libtool libxml2-dev uuid-dev libssl-dev bash \
patch make tar xz-utils bzip2 gzip sed cpio \
gcc-8-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
patch make tar xz-utils bzip2 gzip zlib1g-dev sed cpio \
gcc-10-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
gcc-arm-linux-gnueabi libc-dev-armel-cross linux-libc-dev-armel-cross \
gcc-arm-linux-gnueabihf libc-dev-armhf-cross \
gcc-aarch64-linux-gnu libc-dev-arm64-cross \
@@ -29,21 +24,22 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/*;
# Cross compile setup
ENV OSX_SDK_VERSION 10.11
ENV OSX_SDK_VERSION 11.3
ENV OSX_SDK_DOWNLOAD_FILE=MacOSX${OSX_SDK_VERSION}.sdk.tar.xz
ENV OSX_SDK_DOWNLOAD_URL=https://github.com/ndeloof/golang-cross/raw/113fix/${OSX_SDK_DOWNLOAD_FILE}
ENV OSX_SDK_SHA=98cdd56e0f6c1f9e1af25e11dd93d2e7d306a4aa50430a2bc6bc083ac67efbb8
ENV OSX_SDK_DOWNLOAD_URL=https://github.com/phracker/MacOSX-SDKs/releases/download/${OSX_SDK_VERSION}/${OSX_SDK_DOWNLOAD_FILE}
ENV OSX_SDK_SHA=cd4f08a75577145b8f05245a2975f7c81401d75e9535dcffbb879ee1deefcbf4
ENV OSX_SDK MacOSX$OSX_SDK_VERSION.sdk
ENV OSX_NDK_X86 /usr/local/osx-ndk-x86
RUN wget ${OSX_SDK_DOWNLOAD_URL}
RUN echo "$OSX_SDK_SHA $OSX_SDK_DOWNLOAD_FILE" | sha256sum -c - || exit 1; \
git clone https://github.com/tpoechtrager/osxcross.git && \
git -C osxcross checkout a9317c18a3a457ca0a657f08cc4d0d43c6cf8953 || exit 1; \
mv $OSX_SDK_DOWNLOAD_FILE osxcross/tarballs/ && \
UNATTENDED=yes SDK_VERSION=${OSX_SDK_VERSION} OSX_VERSION_MIN=10.9 osxcross/build.sh || exit 1; \
mv osxcross/target $OSX_NDK_X86; \
rm -rf osxcross;
git clone https://github.com/tpoechtrager/osxcross.git; \
mv $OSX_SDK_DOWNLOAD_FILE osxcross/tarballs/
RUN UNATTENDED=yes SDK_VERSION=${OSX_SDK_VERSION} OSX_VERSION_MIN=10.10 osxcross/build.sh || exit 1;
RUN cp osxcross/target/lib/* /usr/lib/ ; \
mv osxcross/target $OSX_NDK_X86; \
rm -rf osxcross;
ENV PATH $OSX_NDK_X86/bin:$PATH
@@ -51,19 +47,21 @@ RUN mkdir -p /root/.ssh; \
chmod 0700 /root/.ssh; \
ssh-keyscan github.com > /root/.ssh/known_hosts;
RUN wget ${PACKR2_DOWNLOAD_URL}; \
echo "$PACKR2_SHA $PACKR2_DOWNLOAD_FILE" | sha256sum -c - || exit 1; \
tar -xzf $PACKR2_DOWNLOAD_FILE -C /usr/bin/ packr2; \
rm $PACKR2_DOWNLOAD_FILE;
CMD ["packr2", "version"]
# Notes for self:
# To test locally:
# make generate
# make ui
# cd docker/compiler
# make build
# docker run -it -v /PATH_TO_STASH:/go/stash stashapp/compiler:latest /bin/bash
# cd stash
# make cross-compile-all
# # binaries will show up in /dist
# Windows:
# GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go build -ldflags "-extldflags '-static'" -tags extended
# Darwin
# CC=o64-clang CXX=o64-clang++ GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -tags extended
# env GO111MODULE=on goreleaser --config=goreleaser-extended.yml --skip-publish --skip-validate --rm-dist --release-notes=temp/0.48-relnotes-ready.md
# env goreleaser --config=goreleaser-extended.yml --skip-publish --skip-validate --rm-dist --release-notes=temp/0.48-relnotes-ready.md

View File

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

View File

@@ -1,3 +1,5 @@
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.
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.
A MacOS univeral binary can be created using `lipo -create -output stash-osx-universal stash-osx stash-osx-applesilicon`, available in the image.

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env bash
TMP=$(mktemp -d /tmp/XXXXXXXXXXX)
SDK="MacOSX10.11.sdk"
mkdir -p $TMP/$SDK/usr/include/c++
cp -rf /Applications/Xcode7.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/$SDK $TMP &>/dev/null || true
cp -rf /Applications/Xcode7.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1 $TMP/$SDK/usr/include/c++ || exit -1
tar -C $TMP -czf $SDK.tar.gz $SDK

View File

@@ -1,27 +0,0 @@
FROM ubuntu:20.04 as prep
LABEL MAINTAINER="https://discord.gg/Uz29ny"
RUN apt-get update && \
apt-get -y install curl xz-utils && \
apt-get autoclean -y && \
rm -rf /var/lib/apt/lists/*
WORKDIR /
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# added " to end of stash-linux clause so that it doesn't pick up the arm builds
RUN curl -L -o /stash $(curl -s https://api.github.com/repos/stashapp/stash/releases/tags/latest_develop | awk '/browser_download_url/ && /stash-linux"/' | sed -e 's/.*: "\(.*\)"/\1/') && \
chmod +x /stash
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: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,53 +1,37 @@
# Docker install on Ubuntu 18.04
Installing StashApp can likely work on others if your OS either has it's own package manager or comes shipped with Docker and docker-compose.
# Docker Installation (for most 64-bit GNU/Linux systems)
StashApp is supported on most systems that support Docker and docker-compose. Your OS likely ships with or makes available the necessary packages.
## Dependencies
The goal is to avoid as many dependencies as possible so for now the only pre-requisites you are required to have are `curl`, `docker`, and `docker-compose` for the most part your understanding of the technologies can be superficial so long as you can follow commands and are open to reading a bit you should be fine.
Only `docker` and `docker-compose` are required. For the most part your understanding of the technologies can be superficial. So long as you can follow commands and are open to reading a bit, you should be fine.
### Docker
Docker is effectively the cross-platform software package repository it allows you to ship an entire environment in what's referred to as a container. Containers are intended to hold everything that is needed to ship what's required to run an application from one place to another with a degree of a standard that makes it easy for everyone along the way to reproduce the environment for their step in the chain.
The other side of docker is it brings everything that we would typically have to teach you about the individual components of your soon to be installed StashApp and ffmpeg, docker-compose wraps it up nicely in a handful of easy to follow steps that should result in the same environment on everyone's host.
The installation method we recommend is via the `docker.com` website however if your specific operating system's repository versions are at the latest along with docker you should be good to launch with you using whatever instructions you wish. The version of Docker we used in our deployment for testing this process was `Docker version 17.05.0-ce, build 89658be` however any versions later than this will be sufficient. At the writing of this tutorial, this was not the latest version of Docker.
#### Just the link to installation instructions, please
Instructions for installing on Ubuntu are at the link that follows:
https://docs.docker.com/install/linux/docker-ce/ubuntu/
If you plan on using other versions of OS you should at least aim to be a Linux base with an x86_64 CPU and the appropriate minimum version of the dependencies.
### Docker-compose
Docker Compose's role in this deployment is to get you a fully working instance of StashApp exactly as you would need it to have a reasonable instance for testing / developing on, you could technically deploy a live instance with this, but without a reverse proxy, is not recommended. You are encouraged to learn how to use the Docker-Compose format, but it's not a required prerequisite for getting this running you need to have it installed successfully.
Install Docker Compose via this guide below, and it is essential if you're using an older version of Linux to use the official documentation from Docker.com because you require the more recent version of docker-compose at least version 3.4 aka 1.22.0 or newer.
#### Just the link to installation instructions, please
https://docs.docker.com/compose/install/
### Install curl
This one's easy, copy paste.
```
apt update -y && \
apt install -f curl
```
Installation instructions are available below, and if your distrobution's repository ships a current version of docker, you may use that.
https://docs.docker.com/engine/install/
### Get the docker-compose.yml file
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml) in the repository, OR you can make your Linux console do it for you with this.
Now you can either navigate to the [docker-compose.yml](https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml) in the repository, or if you have curl, you can make your Linux console do it for you:
```
curl -o ~/docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml
mkdir stashapp && cd stashapp
curl -o docker-compose.yml https://raw.githubusercontent.com/stashapp/stash/master/docker/production/docker-compose.yml
```
Once you have that file where you want it, you can either modify the settings as you please OR you can run the following to get it up and running instantly.
Once you have that file where you want it, modify the settings as you please, and then run:
```
cd ~ && docker-compose up -d
docker-compose up -d
```
Installing StashApp this way will by default bind stash to port 9999 or in web browser terms. http://YOURIP:9999 or if you're doing this on your machine locally which is the only recommended production version of this container as is with no security configurations set at all is http://localhost:9999
Installing StashApp this way will by default bind stash to port 9999. This is available in your web browser locally at http://localhost:9999 or on your network as http://YOUR-LOCAL-IP:9999
Good luck and have fun!
### Docker
Docker is effectively a cross-platform software package repository. It allows you to ship an entire environment in what's referred to as a container. Containers are intended to hold everything that is needed to run an application from one place to another, making it easy for everyone along the way to reproduce the environment.
The StashApp docker container ships with everything you need to automatically build and run stash, including ffmpeg.
### docker-compose
Docker Compose lets you specify how and where to run your containers, and to manage their environment. The docker-compose.yml file in this folder gets you a fully working instance of StashApp exactly as you would need it to have a reasonable instance for testing / developing on. If you are deploying a live instance for production, a reverse proxy (such as NGINX or Traefik) is recommended, but not required.
The latest version is always recommended.

View File

@@ -4,9 +4,13 @@ version: '3.4'
services:
stash:
image: stashapp/stash:latest
container_name: stash
restart: unless-stopped
## the container's port must be the same with the STASH_PORT in the environment section
ports:
- "9999:9999"
## If you intend to use stash's DLNA functionality uncomment the below network mode and comment out the above ports section
# network_mode: host
logging:
driver: "json-file"
options:
@@ -17,12 +21,14 @@ services:
- STASH_GENERATED=/generated/
- STASH_METADATA=/metadata/
- STASH_CACHE=/cache/
## Adjust below to change default port (9999)
- STASH_PORT=9999
volumes:
- /etc/localtime:/etc/localtime:ro
## Adjust below paths (the left part) to your liking.
## E.g. you can change ./config:/root/.stash to ./stash:/root/.stash
## Keep configs here.
## Keep configs, scrapers, and plugins here.
- ./config:/root/.stash
## Point this at your collection.
- ./data:/data

View File

@@ -1,27 +0,0 @@
FROM ubuntu:20.04 as prep
LABEL MAINTAINER="leopere [at] nixc [dot] us"
RUN apt-get update && \
apt-get -y install curl xz-utils && \
apt-get autoclean -y && \
rm -rf /var/lib/apt/lists/*
WORKDIR /
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# added " to end of stash-linux clause so that it doesn't pick up the arm builds
RUN curl -L -o /stash $(curl -s https://api.github.com/repos/stashapp/stash/releases/latest | awk '/browser_download_url/ && /stash-linux/"' | sed -e 's/.*: "\(.*\)"/\1/') && \
chmod +x /stash
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: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"]

68
docs/DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,68 @@
# Building from Source
## Pre-requisites
* [Go](https://golang.org/dl/)
* [GolangCI](https://golangci-lint.run/) - A meta-linter which runs several linters in parallel
* To install, follow the [local installation instructions](https://golangci-lint.run/usage/install/#local-installation)
* [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).
NOTE: You may need to run the `go get` commands outside the project directory to avoid modifying the projects module file.
## Environment
### Windows
1. Download and install [Go for Windows](https://golang.org/dl/)
2. Download and install [MingW](https://sourceforge.net/projects/mingw-w64/)
3. Search for "advanced system settings" and open the system properties dialog.
1. Click the `Environment Variables` button
2. Under system variables find the `Path`. Edit and add `C:\Program Files\mingw-w64\*\mingw64\bin` (replace * with the correct path).
NOTE: The `make` command in Windows will be `mingw32-make` with MingW.
### macOS
TODO
## Commands
* `make generate` - Generate Go and UI GraphQL files
* `make build` - Builds the binary (make sure to build the UI as well... see below)
* `make docker-build` - Locally builds and tags a complete 'stash/build' docker image
* `make pre-ui` - Installs the UI dependencies. Only needs to be run once before building the UI for the first time, or if the dependencies are updated
* `make fmt-ui` - Formats the UI source code
* `make ui` - Builds the frontend
* `make lint` - Run the linter on the backend
* `make fmt` - Run `go fmt`
* `make it` - Run the unit and integration tests
* `make validate` - Run all of the tests and checks required to submit a PR
* `make 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
1. Run `make generate` to create generated files
2. Run `make ui` to compile the frontend
3. Run `make build` to build the executable for your current platform
## Cross compiling
This project uses a modification of the [CI-GoReleaser](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment
where the app can be cross-compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following
command to open a bash shell to the container to poke around:
`docker run --rm --mount type=bind,source="$(pwd)",target=/stash -w /stash -i -t stashappdev/compiler:latest /bin/bash`
## Profiling
Stash can be profiled using the `--cpuprofile <output profile filename>` command line flag.
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>`

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="185px" viewBox="0 0 256 185" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M250.715745,70.4971666 C244.951102,66.4973277 231.740464,64.997388 221.412146,66.9973071 C220.211179,56.9977092 214.68673,48.2480609 205.078993,40.4983724 L199.554544,36.4985331 L195.711449,42.248302 C190.90758,49.7480006 188.505646,60.2475786 189.226226,70.2471769 C189.46642,73.7470364 190.667387,79.9967847 194.270289,85.496564 C190.90758,87.4964838 183.941971,89.996383 174.814621,89.996383 L1.15476998,89.996383 L0.674383104,91.9963028 C-1.00697093,101.9959 -1.00697093,133.244645 18.6888904,157.243681 C33.5808831,175.492947 55.6786788,184.742575 84.7420842,184.742575 C147.672763,184.742575 194.270289,154.493791 216.127891,99.7459909 C224.774854,99.9959813 243.269748,99.7459909 252.637292,80.996745 C252.877486,80.4967649 253.357872,79.4968046 255.039227,75.7469554 L256,73.7470364 L250.715745,70.4971666 L250.715745,70.4971666 Z M139.986573,0 L113.565295,0 L113.565295,24.9989952 L139.986573,24.9989952 L139.986573,0 L139.986573,0 Z M139.986573,29.9987943 L113.565295,29.9987943 L113.565295,54.9977896 L139.986573,54.9977896 L139.986573,29.9987943 L139.986573,29.9987943 Z M108.761427,29.9987943 L82.3401495,29.9987943 L82.3401495,54.9977896 L108.761427,54.9977896 L108.761427,29.9987943 L108.761427,29.9987943 Z M77.5362814,29.9987943 L51.1150037,29.9987943 L51.1150037,54.9977896 L77.5362814,54.9977896 L77.5362814,29.9987943 L77.5362814,29.9987943 Z M46.311135,59.9975886 L19.8898576,59.9975886 L19.8898576,84.9965839 L46.311135,84.9965839 L46.311135,59.9975886 L46.311135,59.9975886 Z M77.5362814,59.9975886 L51.1150037,59.9975886 L51.1150037,84.9965839 L77.5362814,84.9965839 L77.5362814,59.9975886 L77.5362814,59.9975886 Z M108.761427,59.9975886 L82.3401495,59.9975886 L82.3401495,84.9965839 L108.761427,84.9965839 L108.761427,59.9975886 L108.761427,59.9975886 Z M139.986573,59.9975886 L113.565295,59.9975886 L113.565295,84.9965839 L139.986573,84.9965839 L139.986573,59.9975886 L139.986573,59.9975886 Z M171.211719,59.9975886 L144.790441,59.9975886 L144.790441,84.9965839 L171.211719,84.9965839 L171.211719,59.9975886 L171.211719,59.9975886 Z" fill="#2396ED" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="295px" viewBox="0 0 256 295" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<defs>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="6.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
</filter>
<linearGradient x1="48.5477412%" y1="115.276174%" x2="51.0473804%" y2="41.3637237%" id="linearGradient-2">
<stop stop-color="#FFEED7" offset="0%"></stop>
<stop stop-color="#BDBFC2" offset="100%"></stop>
</linearGradient>
<linearGradient x1="54.4065463%" y1="2.40410545%" x2="46.1753957%" y2="90.5422349%" id="linearGradient-3">
<stop stop-color="#FFFFFF" stop-opacity="0.8" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="51.859653%" y1="88.2477484%" x2="47.9469396%" y2="9.74782136%" id="linearGradient-4">
<stop stop-color="#FFEED7" offset="0%"></stop>
<stop stop-color="#BDBFC2" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.9251097%" y1="85.4900173%" x2="49.9236843%" y2="13.8109272%" id="linearGradient-5">
<stop stop-color="#FFEED7" offset="0%"></stop>
<stop stop-color="#BDBFC2" offset="100%"></stop>
</linearGradient>
<linearGradient x1="53.9014071%" y1="3.10177585%" x2="45.9555354%" y2="93.8949571%" id="linearGradient-6">
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="45.5928761%" y1="5.47459052%" x2="54.811359%" y2="93.5235162%" id="linearGradient-7">
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.9844987%" y1="89.8452442%" x2="49.9844987%" y2="40.6316864%" id="linearGradient-8">
<stop stop-color="#FFEED7" offset="0%"></stop>
<stop stop-color="#BDBFC2" offset="100%"></stop>
</linearGradient>
<linearGradient x1="53.5047131%" y1="99.97524%" x2="42.7455968%" y2="23.5451715%" id="linearGradient-9">
<stop stop-color="#FFEED7" offset="0%"></stop>
<stop stop-color="#BDBFC2" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.8413363%" y1="13.2289558%" x2="50.2412612%" y2="94.6729694%" id="linearGradient-10">
<stop stop-color="#FFFFFF" stop-opacity="0.8" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.9272298%" y1="37.3270337%" x2="50.7270446%" y2="92.7824735%" id="linearGradient-11">
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.8755597%" y1="2.29900584%" x2="49.8755597%" y2="81.203617%" id="linearGradient-12">
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.8334391%" y1="2.27189065%" x2="49.8240398%" y2="71.7989617%" id="linearGradient-13">
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="53.4670683%" y1="48.9213861%" x2="38.9488708%" y2="98.0999776%" id="linearGradient-14">
<stop stop-color="#FFA63F" offset="0%"></stop>
<stop stop-color="#FFFF00" offset="100%"></stop>
</linearGradient>
<linearGradient x1="52.3731508%" y1="143.008909%" x2="47.57909%" y2="-64.6215389%" id="linearGradient-15">
<stop stop-color="#FFEED7" offset="0%"></stop>
<stop stop-color="#BDBFC2" offset="100%"></stop>
</linearGradient>
<linearGradient x1="30.580815%" y1="34.0241079%" x2="65.8867024%" y2="89.175349%" id="linearGradient-16">
<stop stop-color="#FFA63F" offset="0%"></stop>
<stop stop-color="#FFFF00" offset="100%"></stop>
</linearGradient>
<linearGradient x1="59.5715091%" y1="-17.2155207%" x2="48.3608522%" y2="66.1184465%" id="linearGradient-17">
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="47.7689553%" y1="1.56481301%" x2="51.3733028%" y2="104.312856%" id="linearGradient-18">
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="43.5495626%" y1="4.5334861%" x2="57.1143288%" y2="92.8267174%" id="linearGradient-19">
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="49.7328042%" y1="17.6085216%" x2="50.5582487%" y2="99.3854667%" id="linearGradient-20">
<stop stop-color="#FFA63F" offset="0%"></stop>
<stop stop-color="#FFFF00" offset="100%"></stop>
</linearGradient>
<linearGradient x1="50.1697217%" y1="2.89048531%" x2="49.6802359%" y2="94.1704279%" id="linearGradient-21">
<stop stop-color="#FFFFFF" stop-opacity="0.65" offset="0%"></stop>
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
</defs>
<g fill="none">
<g transform="translate(10.000000, 0.000000)">
<path d="M235.125423,249.358628 C235.125423,266.714271 182.507524,280.855905 117.584567,280.855905 C52.6616093,280.855905 0.0437105058,266.806099 0.0437105058,249.358628 L0.0437105058,249.358628 C0.0437105058,232.002986 52.6616093,217.861352 117.584567,217.861352 C182.507524,217.861352 235.033594,232.002986 235.125423,249.358628 L235.125423,249.358628 L235.125423,249.358628 Z" fill="#000" fill-opacity="0.2" filter="url(#filter-1)"></path>
<path d="M53.2125821,215.473804 C41.8258117,199.128278 39.6219206,145.867578 66.160442,113.084699 C79.2919595,97.3819748 82.6896249,86.4543483 83.6997416,71.6699125 C84.434372,54.8652433 71.8538272,4.81855066 119.237485,1.05357012 C167.263944,-2.80323922 164.600909,44.5804184 164.325423,69.6496791 C164.141765,90.7703016 179.844489,102.799874 190.680286,119.329056 C210.607135,149.632558 208.954216,201.791313 186.915306,230.074582 C158.999353,265.428667 135.123866,250.093259 119.237485,251.378862 C89.4849556,253.123609 88.4748389,268.918162 53.2125821,215.473804 L53.2125821,215.473804 Z" fill="#000000"></path>
<path d="M169.10052,122.451235 C177.365111,130.073025 198.76122,164.141508 164.876395,185.445788 C152.938652,192.88392 175.528535,221.167189 186.364333,207.484699 C205.556551,182.874582 193.343321,143.571858 181.772893,129.522053 C174.059275,119.604543 162.121532,115.747734 169.10052,122.451235 L169.10052,122.451235 Z" fill="url(#linearGradient-2)"></path>
<path d="M166.8048,117.859796 C180.395461,128.879251 205.097407,167.447344 169.008691,192.608434 C157.162777,200.413881 179.477174,225.115827 192.057718,212.535282 C235.676395,168.641119 190.955773,118.227111 175.528535,100.871469 C161.754216,85.719718 149.540987,104.360963 166.8048,117.859796 L166.8048,117.859796 Z" stroke="#000000" stroke-width="0.9773" fill="#000000"></path>
<path d="M147.245267,25.0208853 C146.786123,37.60143 132.919975,48.5290565 116.298963,49.5391732 C99.6779518,50.54929 86.638263,40.9990954 87.097407,28.4185507 L87.097407,28.4185507 C87.556551,15.8380059 101.422699,4.91037946 118.043711,3.90026272 C134.664722,2.98197479 147.704411,12.4403405 147.245267,25.0208853 L147.245267,25.0208853 L147.245267,25.0208853 Z" fill="url(#linearGradient-3)"></path>
<path d="M107.483399,54.9570721 C107.942543,63.1298347 104.085734,70.0169942 98.7596638,70.2924806 C93.4335938,70.567967 88.7503253,64.2317802 88.2911813,56.0590176 L88.2911813,56.0590176 C87.8320374,47.8862549 91.6888467,40.9990954 97.0149167,40.723609 C102.340987,40.4481226 107.024255,46.7843094 107.483399,54.9570721 L107.483399,54.9570721 L107.483399,54.9570721 Z" fill="url(#linearGradient-4)"></path>
<path d="M117.125423,55.5998736 C117.30908,65.0582394 123.461609,72.5882005 130.807913,72.4045429 C138.154216,72.2208853 143.93943,64.4154378 143.755773,54.8652433 L143.755773,54.8652433 C143.572115,45.4068775 137.419586,37.8769164 130.073282,38.060574 C122.726979,38.2442316 116.849936,46.1415079 117.125423,55.5998736 L117.125423,55.5998736 L117.125423,55.5998736 Z" fill="url(#linearGradient-5)"></path>
<path d="M123.186123,57.7119359 C123.094294,62.9461771 125.6655,67.1703016 129.063166,67.1703016 C132.369002,67.1703016 135.215695,62.9461771 135.307524,57.8037647 L135.307524,57.8037647 C135.399353,52.5695234 132.828146,48.3453989 129.430481,48.3453989 C126.032816,48.3453989 123.277952,52.5695234 123.186123,57.7119359 L123.186123,57.7119359 L123.186123,57.7119359 Z" fill="#000000"></path>
<path d="M101.973672,57.8037647 C102.432816,62.119718 100.779897,65.7928697 98.3923486,66.1601849 C96.0048,66.4356713 93.7090802,63.2216635 93.2499362,58.9057102 L93.2499362,58.9057102 C92.7907922,54.5897569 94.4437105,50.9166051 96.8312591,50.54929 C99.2188078,50.2738036 101.514528,53.4878114 101.973672,57.8037647 L101.973672,57.8037647 L101.973672,57.8037647 Z" fill="#000000"></path>
<path d="M124.563555,54.7734145 C124.288068,57.7119359 125.6655,60.0994845 127.593905,60.2831421 C129.52231,60.4667997 131.358886,58.1710798 131.634372,55.3243872 L131.634372,55.3243872 C131.909858,52.3858658 130.532426,49.9983172 128.604022,49.8146596 C126.675617,49.631002 124.839041,51.9267219 124.563555,54.7734145 L124.563555,54.7734145 L124.563555,54.7734145 Z" fill="url(#linearGradient-6)"></path>
<path d="M99.9534381,55.5080448 C100.228925,57.8955935 99.2188078,60.0076557 97.7495471,60.1913133 C96.2802864,60.3749709 94.9028545,58.538395 94.6273681,56.0590176 L94.6273681,56.0590176 C94.3518817,53.6714689 95.3619984,51.5594067 96.8312591,51.3757491 C98.3005198,51.1920915 99.6779518,53.1204962 99.9534381,55.5080448 L99.9534381,55.5080448 L99.9534381,55.5080448 Z" fill="url(#linearGradient-7)"></path>
<path d="M71.0273681,145.68392 C77.5472125,130.899485 91.4133603,104.911936 91.6888467,84.80143 C91.6888467,68.8232199 139.531648,64.9664106 143.388458,80.9446207 C147.245267,96.9228308 156.979119,120.798317 163.223477,132.368745 C169.467835,143.847344 187.558107,180.487033 168.274061,212.443453 C150.918419,240.726722 98.3005198,263.132948 70.2009089,208.586644 C60.6507144,189.669913 62.3954615,166.25357 71.0273681,145.68392 L71.0273681,145.68392 Z" fill="url(#linearGradient-8)"></path>
<path d="M65.1503253,134.664465 C59.5487689,145.224776 47.9783409,172.957072 76.2616093,188.108823 C106.65694,204.270691 106.565111,237.420885 70.0172514,221.626333 C36.5915704,207.39287 51.3760062,149.724387 60.7425432,135.950068 C66.8032436,126.308045 75.986123,114.46213 65.1503253,134.664465 L65.1503253,134.664465 Z" fill="url(#linearGradient-9)"></path>
<path d="M69.9254226,122.726722 C61.0180296,137.235671 39.7137494,171.395983 68.2725043,189.210769 C106.65694,212.810769 95.8211424,236.31894 60.7425432,215.106488 C11.3386521,185.537617 54.7736716,125.848901 74.5168623,103.07536 C97.1067455,77.5469553 78.8328156,107.758628 69.9254226,122.726722 L69.9254226,122.726722 Z" stroke="#000000" stroke-width="1.25" fill="#000000"></path>
<path d="M156.428146,151.285477 C156.428146,167.447344 140.90908,188.384309 114.27873,188.200652 C86.8219206,188.384309 75.1596638,167.447344 75.1596638,151.285477 C75.1596638,135.123609 93.341765,121.992092 115.747991,121.992092 C138.246045,122.08392 156.428146,135.123609 156.428146,151.285477 L156.428146,151.285477 Z" fill="url(#linearGradient-10)"></path>
<path d="M141.919197,100.504154 C141.643711,117.216994 130.716084,121.165632 116.941765,121.165632 C103.167446,121.165632 93.1581074,118.686255 91.9643331,100.504154 C91.9643331,89.1173833 103.167446,82.5057102 116.941765,82.5057102 C130.716084,82.4138814 141.919197,89.0255546 141.919197,100.504154 L141.919197,100.504154 Z" fill="url(#linearGradient-11)"></path>
<path d="M58.6304809,126.216216 C67.6297027,112.533726 86.638263,91.504932 62.2118039,129.154737 C42.3767844,160.19287 54.8655004,180.119718 61.293516,185.629446 C79.8429323,202.158628 79.1083019,213.269913 64.5075237,204.546177 C33.1939051,185.904932 39.7137494,154.499485 58.6304809,126.216216 L58.6304809,126.216216 Z" fill="url(#linearGradient-12)"></path>
<path d="M188.935539,131.817772 C181.130092,115.747734 156.336318,74.9757491 190.129314,122.359407 C220.89196,165.243453 199.312193,195.087811 195.455384,198.026333 C191.598574,200.964854 178.650714,206.933726 182.415695,196.557072 C186.272504,186.180418 205.372893,166.529056 188.935539,131.817772 L188.935539,131.817772 Z" fill="url(#linearGradient-13)"></path>
<path d="M51.8351502,258.541508 C31.2655004,247.613881 1.42114241,260.65357 12.2569401,231.084699 C14.4608311,224.381197 9.0429323,214.280029 12.5324265,207.760185 C16.6647222,199.77108 25.5721152,201.515827 30.8981852,196.189757 C36.1324265,190.680029 39.438263,181.129835 49.263944,182.599095 C58.9977961,184.068356 65.5176405,196.006099 72.3129712,210.698706 C77.3635549,221.167189 95.1783409,235.951625 93.9845665,247.70571 C92.5153058,265.704154 72.0374848,269.101819 51.8351502,258.541508 L51.8351502,258.541508 Z" stroke="#E68C3F" stroke-width="6.25" fill="url(#linearGradient-14)"></path>
<path d="M201.607913,189.11894 C198.485734,194.995983 185.446045,204.454348 176.72231,201.974971 C167.906746,199.587422 163.866279,186.180418 165.611026,175.987422 C167.263944,164.600652 176.72231,163.95785 188.660053,169.651235 C201.516084,175.987422 205.372893,181.313492 201.607913,189.11894 L201.607913,189.11894 Z" fill="url(#linearGradient-15)"></path>
<path d="M194.445267,253.490924 C209.505189,235.216994 243.022699,238.981975 220.432816,213.912714 C215.657718,208.494815 217.126979,196.924387 211.249936,191.965632 C204.362777,185.904932 196.740987,190.863687 189.761998,187.741508 C182.78301,184.343842 175.436707,177.823998 166.896629,182.415438 C158.356551,187.098706 157.438263,199.220107 156.611804,215.198317 C155.877174,226.676916 145.408691,245.869134 151.010247,256.429446 C159.091181,272.774971 180.119975,270.57108 194.445267,253.490924 L194.445267,253.490924 Z" stroke="#E68C3F" stroke-width="6.2507" fill="url(#linearGradient-16)"></path>
<path d="M187.925423,229.064465 C211.249936,194.628667 193.894294,194.904154 188.017251,192.241119 C182.140209,189.486255 175.987679,184.068356 169.10052,187.833337 C162.21336,191.690146 161.846045,201.607656 161.662388,214.647344 C161.386901,224.013881 153.581454,239.716605 158.264722,248.440341 C163.958107,258.633337 177.732426,243.848901 187.925423,229.064465 L187.925423,229.064465 Z" fill="url(#linearGradient-17)"></path>
<path d="M47.0600529,234.02322 C12.1651113,211.433337 28.5106366,203.719718 33.7448778,200.138395 C40.0810646,195.546955 40.1728934,186.731391 47.9783409,187.55785 C55.7837883,188.384309 60.375228,198.026333 65.6094693,209.964076 C69.4662786,218.504154 82.8732825,229.890924 81.8631658,239.716605 C80.5775626,251.287033 62.1199751,243.665243 47.0600529,234.02322 L47.0600529,234.02322 Z" fill="url(#linearGradient-18)"></path>
<path d="M199.587679,188.843453 C196.832816,193.618551 185.629703,201.148512 178.19157,199.128278 C170.569781,197.199874 167.080286,186.455905 168.641376,178.374971 C170.018808,169.192092 178.19157,168.732948 188.476395,173.324387 C199.404022,178.283142 202.801687,182.507267 199.587679,188.843453 L199.587679,188.843453 Z" fill="#000000"></path>
<path d="M192.057718,186.180418 C190.312971,189.486255 182.966668,194.720496 177.824255,193.343064 C172.681843,191.965632 170.110637,184.5275 170.937096,178.925944 C171.671726,172.589757 177.181454,172.222442 184.160442,175.344621 C191.690403,178.834115 194.077952,181.772636 192.057718,186.180418 L192.057718,186.180418 Z" fill="url(#linearGradient-19)"></path>
<path d="M97.1067455,66.3438425 C100.779897,62.9461771 109.68729,52.5695234 126.583788,63.4053211 C129.705967,65.4255546 132.277174,65.6092121 138.246045,68.1804184 C150.275617,73.1391732 144.582232,85.0769164 131.726201,89.1173833 C126.216473,90.8621304 121.257718,97.5656324 111.340209,96.9228308 C102.800131,96.4636868 100.59624,90.8621304 95.3619984,87.8317802 C86.0872903,82.597539 84.7098584,75.5267219 89.760442,71.7617413 C94.8110257,67.9967608 96.7394304,66.6193289 97.1067455,66.3438425 L97.1067455,66.3438425 Z" stroke="#E68C3F" stroke-width="3.75" fill="url(#linearGradient-20)"></path>
<path d="M138.429703,75.9858658 C133.379119,76.2613522 122.451493,87.1889787 110.972893,87.1889787 C99.4942942,87.1889787 92.6071346,76.5368386 90.8623875,76.5368386" stroke="#E68C3F" stroke-width="2.5"></path>
<path d="M102.800131,65.4255546 C104.636707,63.7726363 110.421921,59.2730254 118.043711,63.8644651 C119.696629,64.782753 121.349547,65.7928697 123.737096,67.1703016 C128.604022,70.0169942 126.216473,74.14929 120.33943,76.7204962 C117.676395,77.8224417 113.268613,80.2099904 109.962777,80.0263328 C106.289625,79.6590176 103.810247,77.2714689 101.422699,75.7103795 C96.9230879,72.7718581 97.1985743,70.2924806 99.3106366,68.364076 C100.871726,66.8948153 102.616473,65.5173833 102.800131,65.4255546 L102.800131,65.4255546 Z" fill="url(#linearGradient-21)"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 42 42" xmlns="http://www.w3.org/2000/svg"><path d="m23.091 14.018v-0.342l-1.063 0.073c-0.301 0.019-0.527 0.083-0.679 0.191-0.152 0.109-0.228 0.26-0.228 0.453 0 0.188 0.075 0.338 0.226 0.449 0.15 0.112 0.352 0.167 0.604 0.167 0.161 0 0.312-0.025 0.451-0.074s0.261-0.118 0.363-0.206c0.102-0.087 0.182-0.191 0.239-0.312 0.058-0.121 0.087-0.254 0.087-0.399zm-2.091-13.768c-11.579 0-20.75 9.171-20.75 20.75 0 11.58 9.171 20.75 20.75 20.75s20.75-9.17 20.75-20.75c0-11.579-9.17-20.75-20.75-20.75zm4.028 12.299c0.098-0.275 0.236-0.511 0.415-0.707s0.394-0.347 0.646-0.453 0.533-0.159 0.842-0.159c0.279 0 0.531 0.042 0.755 0.125 0.225 0.083 0.417 0.195 0.578 0.336s0.289 0.305 0.383 0.493 0.15 0.387 0.169 0.596h-0.833c-0.021-0.115-0.059-0.223-0.113-0.322s-0.125-0.185-0.213-0.258c-0.089-0.073-0.193-0.13-0.312-0.171-0.12-0.042-0.254-0.062-0.405-0.062-0.177 0-0.338 0.036-0.481 0.107-0.144 0.071-0.267 0.172-0.369 0.302s-0.181 0.289-0.237 0.475c-0.057 0.187-0.085 0.394-0.085 0.622 0 0.236 0.028 0.448 0.085 0.634 0.056 0.187 0.136 0.344 0.24 0.473 0.103 0.129 0.228 0.228 0.373 0.296s0.305 0.103 0.479 0.103c0.285 0 0.517-0.067 0.697-0.201s0.296-0.33 0.35-0.588h0.834c-0.024 0.228-0.087 0.436-0.189 0.624s-0.234 0.348-0.396 0.481c-0.163 0.133-0.354 0.236-0.574 0.308s-0.462 0.109-0.725 0.109c-0.312 0-0.593-0.052-0.846-0.155-0.252-0.103-0.469-0.252-0.649-0.445s-0.319-0.428-0.417-0.705-0.147-0.588-0.147-0.935c-2e-3 -0.339 0.047-0.647 0.145-0.923zm-11.853-1.262h0.834v0.741h0.016c0.051-0.123 0.118-0.234 0.2-0.33 0.082-0.097 0.176-0.179 0.284-0.248 0.107-0.069 0.226-0.121 0.354-0.157 0.129-0.036 0.265-0.054 0.407-0.054 0.306 0 0.565 0.073 0.775 0.219 0.211 0.146 0.361 0.356 0.449 0.63h0.021c0.056-0.132 0.13-0.25 0.221-0.354s0.196-0.194 0.314-0.268 0.248-0.13 0.389-0.169 0.289-0.058 0.445-0.058c0.215 0 0.41 0.034 0.586 0.103s0.326 0.165 0.451 0.29 0.221 0.277 0.288 0.455 0.101 0.376 0.101 0.594v2.981h-0.87v-2.772c0-0.287-0.074-0.51-0.222-0.667-0.147-0.157-0.358-0.236-0.632-0.236-0.134 0-0.257 0.024-0.369 0.071-0.111 0.047-0.208 0.113-0.288 0.198-0.081 0.084-0.144 0.186-0.189 0.304-0.046 0.118-0.069 0.247-0.069 0.387v2.715h-0.858v-2.844c0-0.126-0.02-0.24-0.059-0.342s-0.094-0.189-0.167-0.262c-0.072-0.073-0.161-0.128-0.264-0.167-0.104-0.039-0.22-0.059-0.349-0.059-0.134 0-0.258 0.025-0.373 0.075-0.114 0.05-0.212 0.119-0.294 0.207-0.082 0.089-0.146 0.193-0.191 0.314-0.044 0.12-0.116 0.252-0.116 0.394v2.683h-0.825v-4.374zm1.893 20.939c-3.825 0-6.224-2.658-6.224-6.9s2.399-6.909 6.224-6.909 6.215 2.667 6.215 6.909c0 4.241-2.39 6.9-6.215 6.9zm7.082-16.575c-0.141 0.036-0.285 0.054-0.433 0.054-0.218 0-0.417-0.031-0.598-0.093-0.182-0.062-0.337-0.149-0.467-0.262s-0.232-0.249-0.304-0.409c-0.073-0.16-0.109-0.338-0.109-0.534 0-0.384 0.143-0.684 0.429-0.9s0.7-0.342 1.243-0.377l1.18-0.068v-0.338c0-0.252-0.08-0.445-0.24-0.576s-0.386-0.197-0.679-0.197c-0.118 0-0.229 0.015-0.331 0.044-0.102 0.03-0.192 0.072-0.27 0.127s-0.143 0.121-0.193 0.198c-0.051 0.076-0.086 0.162-0.105 0.256h-0.818c5e-3 -0.193 0.053-0.372 0.143-0.536s0.212-0.306 0.367-0.427 0.336-0.215 0.546-0.282 0.438-0.101 0.685-0.101c0.266 0 0.507 0.033 0.723 0.101s0.401 0.163 0.554 0.288 0.271 0.275 0.354 0.451 0.125 0.373 0.125 0.59v3.001h-0.833v-0.729h-0.021c-0.062 0.118-0.14 0.225-0.235 0.32-0.096 0.095-0.203 0.177-0.322 0.244-0.12 0.067-0.25 0.119-0.391 0.155zm5.503 16.575c-2.917 0-4.9-1.528-5.038-3.927h1.899c0.148 1.371 1.473 2.279 3.288 2.279 1.741 0 2.992-0.908 2.992-2.149 0-1.074-0.76-1.723-2.519-2.167l-1.714-0.426c-2.464-0.611-3.584-1.732-3.584-3.575 0-2.269 1.982-3.844 4.807-3.844 2.76 0 4.686 1.584 4.76 3.862h-1.88c-0.13-1.371-1.25-2.214-2.918-2.214-1.658 0-2.806 0.852-2.806 2.084 0 0.972 0.722 1.547 2.482 1.991l1.445 0.361c2.751 0.667 3.881 1.751 3.881 3.696-1e-3 2.482-1.964 4.029-5.095 4.029zm-12.585-12.106c-2.621 0-4.26 2.01-4.26 5.205 0 3.186 1.639 5.196 4.26 5.196 2.612 0 4.26-2.01 4.26-5.196 1e-3 -3.195-1.648-5.205-4.26-5.205z"/></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="257px" viewBox="0 0 256 257" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M0,36.3573818 L104.619084,22.1093454 L104.664817,123.02292 L0.0955693151,123.618411 L0,36.3573818 Z M104.569248,134.650129 L104.650452,235.651651 L0.0812046021,221.274919 L0.0753414539,133.972642 L104.569248,134.650129 Z M117.25153,20.2454506 L255.967753,6.21724894e-15 L255.967753,121.739477 L117.25153,122.840723 L117.25153,20.2454506 Z M256,135.599959 L255.96746,256.791232 L117.251237,237.213007 L117.056874,135.373055 L256,135.599959 Z" fill="#00ADEF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 734 B

84
go.mod
View File

@@ -8,46 +8,92 @@ require (
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/logger v1.0.4 // indirect
github.com/gobuffalo/packr/v2 v2.8.1
github.com/golang-migrate/migrate/v4 v4.3.1
github.com/golang-jwt/jwt/v4 v4.0.0
github.com/golang-migrate/migrate/v4 v4.15.0-beta.1
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.2
github.com/h2non/filetype v1.0.8
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/karrick/godirwalk v1.16.1 // indirect
github.com/jmoiron/sqlx v1.3.1
github.com/json-iterator/go v1.1.11
github.com/mattn/go-sqlite3 v1.14.6
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/remeh/sizedwaitgroup v1.0.0
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
github.com/rs/cors v1.6.0
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
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.7.0
github.com/stretchr/testify v1.5.1
github.com/tidwall/gjson v1.8.1
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.9.0
github.com/stretchr/testify v1.7.0
github.com/tidwall/gjson v1.9.3
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/crypto v0.0.0-20210817164053-32db794688a5
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/net v0.0.0-20210520170846-37e1c6afe023
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/tools v0.0.0-20200915031644-64986481280e // indirect
golang.org/x/text v0.3.6
golang.org/x/tools v0.1.5 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v2 v2.4.0
)
require github.com/vektah/gqlparser/v2 v2.0.1
require (
github.com/agnivade/levenshtein v1.1.0 // indirect
github.com/antchfx/xpath v1.1.6 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0-rc.5 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/zerolog v1.18.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.0.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/urfave/cli/v2 v2.1.1 // indirect
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999
go 1.13
go 1.17

874
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -34,24 +34,8 @@ models:
model: github.com/stashapp/stash/pkg/models.Movie
Tag:
model: github.com/stashapp/stash/pkg/models.Tag
ScrapedPerformer:
model: github.com/stashapp/stash/pkg/models.ScrapedPerformer
ScrapedScene:
model: github.com/stashapp/stash/pkg/models.ScrapedScene
ScrapedScenePerformer:
model: github.com/stashapp/stash/pkg/models.ScrapedScenePerformer
ScrapedSceneStudio:
model: github.com/stashapp/stash/pkg/models.ScrapedSceneStudio
ScrapedSceneMovie:
model: github.com/stashapp/stash/pkg/models.ScrapedSceneMovie
ScrapedSceneTag:
model: github.com/stashapp/stash/pkg/models.ScrapedSceneTag
SceneFileType:
model: github.com/stashapp/stash/pkg/models.SceneFileType
ScrapedMovie:
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:

View File

@@ -6,6 +6,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
}
databasePath
generatedPath
metadataPath
cachePath
calculateMD5
videoFileNamingAlgorithm
@@ -18,10 +19,12 @@ fragment ConfigGeneralData on ConfigGeneralResult {
previewPreset
maxTranscodeSize
maxStreamingTranscodeSize
writeImageThumbnails
apiKey
username
password
maxSessionAge
trustedProxies
logFile
logOut
logLevel
@@ -49,12 +52,20 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
wallShowTitle
wallPlayback
maximumLoopDuration
noBrowser
autostartVideo
autostartVideoOnPlaySelected
continuePlaylistDefault
showStudioAsText
css
cssEnabled
language
slideshowDelay
disabledDropdownCreate {
performer
tag
studio
}
handyKey
funscriptOffset
}
@@ -73,6 +84,46 @@ fragment ConfigScrapingData on ConfigScrapingResult {
excludeTagPatterns
}
fragment IdentifyFieldOptionsData on IdentifyFieldOptions {
field
strategy
createMissing
}
fragment IdentifyMetadataOptionsData on IdentifyMetadataOptions {
fieldOptions {
...IdentifyFieldOptionsData
}
setCoverImage
setOrganized
includeMalePerformers
}
fragment ScraperSourceData on ScraperSource {
stash_box_index
stash_box_endpoint
scraper_id
}
fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
identify {
sources {
source {
...ScraperSourceData
}
options {
...IdentifyMetadataOptionsData
}
}
options {
...IdentifyMetadataOptionsData
}
}
deleteFile
deleteGenerated
}
fragment ConfigData on ConfigResult {
general {
...ConfigGeneralData
@@ -86,4 +137,7 @@ fragment ConfigData on ConfigResult {
scraping {
...ConfigScrapingData
}
defaults {
...ConfigDefaultSettingsData
}
}

View File

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

View File

@@ -17,4 +17,10 @@ fragment MovieData on Movie {
front_image_path
back_image_path
scene_count
scenes {
id
title
path
}
}

View File

@@ -22,6 +22,7 @@ fragment PerformerData on Performer {
scene_count
image_count
gallery_count
movie_count
tags {
...SlimTagData

View File

@@ -4,6 +4,7 @@ fragment SceneMarkerData on SceneMarker {
seconds
stream
preview
screenshot
scene {
id

View File

@@ -1,4 +1,5 @@
fragment ScrapedPerformerData on ScrapedPerformer {
stored_id
name
gender
url
@@ -18,7 +19,7 @@ fragment ScrapedPerformerData on ScrapedPerformer {
tags {
...ScrapedSceneTagData
}
image
images
details
death_date
hair_color
@@ -26,7 +27,7 @@ fragment ScrapedPerformerData on ScrapedPerformer {
remote_site_id
}
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
fragment ScrapedScenePerformerData on ScrapedPerformer {
stored_id
name
gender
@@ -55,8 +56,8 @@ fragment ScrapedScenePerformerData on ScrapedScenePerformer {
weight
}
fragment ScrapedMovieStudioData on ScrapedMovieStudio {
id
fragment ScrapedMovieStudioData on ScrapedStudio {
stored_id
name
url
}
@@ -78,7 +79,7 @@ fragment ScrapedMovieData on ScrapedMovie {
}
}
fragment ScrapedSceneMovieData on ScrapedSceneMovie {
fragment ScrapedSceneMovieData on ScrapedMovie {
stored_id
name
aliases
@@ -90,14 +91,14 @@ fragment ScrapedSceneMovieData on ScrapedSceneMovie {
synopsis
}
fragment ScrapedSceneStudioData on ScrapedSceneStudio {
fragment ScrapedSceneStudioData on ScrapedStudio {
stored_id
name
url
remote_site_id
}
fragment ScrapedSceneTagData on ScrapedSceneTag {
fragment ScrapedSceneTagData on ScrapedTag {
stored_id
name
}
@@ -108,6 +109,7 @@ fragment ScrapedSceneData on ScrapedScene {
url
date
image
remote_site_id
file {
size
@@ -135,6 +137,12 @@ fragment ScrapedSceneData on ScrapedScene {
movies {
...ScrapedSceneMovieData
}
fingerprints {
hash
algorithm
duration
}
}
fragment ScrapedGalleryData on ScrapedGallery {

View File

@@ -11,4 +11,5 @@ fragment SlimStudioData on Studio {
}
details
rating
aliases
}

View File

@@ -18,10 +18,12 @@ fragment StudioData on Studio {
scene_count
image_count
gallery_count
movie_count
stash_ids {
stash_id
endpoint
}
details
rating
aliases
}

View File

@@ -8,4 +8,12 @@ fragment TagData on Tag {
image_count
gallery_count
performer_count
parents {
...SlimTagData
}
children {
...SlimTagData
}
}

View File

@@ -30,6 +30,12 @@ mutation ConfigureScraping($input: ConfigScrapingInput!) {
}
}
mutation ConfigureDefaults($input: ConfigDefaultSettingsInput!) {
configureDefaults(input: $input) {
...ConfigDefaultSettingsData
}
}
mutation GenerateAPIKey($input: GenerateAPIKeyInput!) {
generateAPIKey(input: $input)
}

View File

@@ -26,6 +26,10 @@ mutation MetadataAutoTag($input: AutoTagMetadataInput!) {
metadataAutoTag(input: $input)
}
mutation MetadataIdentify($input: IdentifyMetadataInput!) {
metadataIdentify(input: $input)
}
mutation MetadataClean($input: CleanMetadataInput!) {
metadataClean(input: $input)
}

View File

@@ -1,6 +1,8 @@
query FindImages($filter: FindFilterType, $image_filter: ImageFilterType, $image_ids: [Int!]) {
findImages(filter: $filter, image_filter: $image_filter, image_ids: $image_ids) {
count
megapixels
filesize
images {
...SlimImageData
}

View File

@@ -1,6 +1,8 @@
query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene_ids: [Int!]) {
findScenes(filter: $filter, scene_filter: $scene_filter, scene_ids: $scene_ids) {
count
filesize
duration
scenes {
...SlimSceneData
}
@@ -10,6 +12,8 @@ query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene
query FindScenesByPathRegex($filter: FindFilterType) {
findScenesByPathRegex(filter: $filter) {
count
filesize
duration
scenes {
...SlimSceneData
}

View File

@@ -42,14 +42,14 @@ query ListMovieScrapers {
}
}
query ScrapePerformerList($scraper_id: ID!, $query: String!) {
scrapePerformerList(scraper_id: $scraper_id, query: $query) {
query ScrapeSinglePerformer($source: ScraperSourceInput!, $input: ScrapeSinglePerformerInput!) {
scrapeSinglePerformer(source: $source, input: $input) {
...ScrapedPerformerData
}
}
query ScrapePerformer($scraper_id: ID!, $scraped_performer: ScrapedPerformerInput!) {
scrapePerformer(scraper_id: $scraper_id, scraped_performer: $scraped_performer) {
query ScrapeMultiPerformers($source: ScraperSourceInput!, $input: ScrapeMultiPerformersInput!) {
scrapeMultiPerformers(source: $source, input: $input) {
...ScrapedPerformerData
}
}
@@ -60,8 +60,14 @@ query ScrapePerformerURL($url: String!) {
}
}
query ScrapeScene($scraper_id: ID!, $scene: SceneUpdateInput!) {
scrapeScene(scraper_id: $scraper_id, scene: $scene) {
query ScrapeSingleScene($source: ScraperSourceInput!, $input: ScrapeSingleSceneInput!) {
scrapeSingleScene(source: $source, input: $input) {
...ScrapedSceneData
}
}
query ScrapeMultiScenes($source: ScraperSourceInput!, $input: ScrapeMultiScenesInput!) {
scrapeMultiScenes(source: $source, input: $input) {
...ScrapedSceneData
}
}
@@ -72,8 +78,8 @@ query ScrapeSceneURL($url: String!) {
}
}
query ScrapeGallery($scraper_id: ID!, $gallery: GalleryUpdateInput!) {
scrapeGallery(scraper_id: $scraper_id, gallery: $gallery) {
query ScrapeSingleGallery($source: ScraperSourceInput!, $input: ScrapeSingleGalleryInput!) {
scrapeSingleGallery(source: $source, input: $input) {
...ScrapedGalleryData
}
}
@@ -89,15 +95,3 @@ query ScrapeMovieURL($url: String!) {
...ScrapedMovieData
}
}
query QueryStashBoxScene($input: StashBoxSceneQueryInput!) {
queryStashBoxScene(input: $input) {
...ScrapedStashBoxSceneData
}
}
query QueryStashBoxPerformer($input: StashBoxPerformerQueryInput!) {
queryStashBoxPerformer(input: $input) {
...ScrapedStashBoxPerformerData
}
}

View File

@@ -7,7 +7,7 @@ type Query {
"""Find a scene by ID or Checksum"""
findScene(id: ID, checksum: String): Scene
findSceneByHash(input: SceneHashInput!): Scene
"""A function which queries Scene objects"""
findScenes(scene_filter: SceneFilterType, scene_ids: [Int!], filter: FindFilterType): FindScenesResultType!
@@ -25,7 +25,7 @@ type Query {
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!
@@ -72,31 +72,50 @@ type Query {
listGalleryScrapers: [Scraper!]!
listMovieScrapers: [Scraper!]!
"""Scrape a list of performers based on name"""
scrapePerformerList(scraper_id: ID!, query: String!): [ScrapedPerformer!]!
"""Scrapes a complete performer record based on a scrapePerformerList result"""
scrapePerformer(scraper_id: ID!, scraped_performer: ScrapedPerformerInput!): ScrapedPerformer
"""Scrape for a single scene"""
scrapeSingleScene(source: ScraperSourceInput!, input: ScrapeSingleSceneInput!): [ScrapedScene!]!
"""Scrape for multiple scenes"""
scrapeMultiScenes(source: ScraperSourceInput!, input: ScrapeMultiScenesInput!): [[ScrapedScene!]!]!
"""Scrape for a single performer"""
scrapeSinglePerformer(source: ScraperSourceInput!, input: ScrapeSinglePerformerInput!): [ScrapedPerformer!]!
"""Scrape for multiple performers"""
scrapeMultiPerformers(source: ScraperSourceInput!, input: ScrapeMultiPerformersInput!): [[ScrapedPerformer!]!]!
"""Scrape for a single gallery"""
scrapeSingleGallery(source: ScraperSourceInput!, input: ScrapeSingleGalleryInput!): [ScrapedGallery!]!
"""Scrape for a single movie"""
scrapeSingleMovie(source: ScraperSourceInput!, input: ScrapeSingleMovieInput!): [ScrapedMovie!]!
"""Scrapes a complete performer record based on a URL"""
scrapePerformerURL(url: String!): ScrapedPerformer
"""Scrapes a complete scene record based on an existing scene"""
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
"""Scrape a list of performers based on name"""
scrapePerformerList(scraper_id: ID!, query: String!): [ScrapedPerformer!]! @deprecated(reason: "use scrapeSinglePerformer")
"""Scrapes a complete performer record based on a scrapePerformerList result"""
scrapePerformer(scraper_id: ID!, scraped_performer: ScrapedPerformerInput!): ScrapedPerformer @deprecated(reason: "use scrapeSinglePerformer")
"""Scrapes a complete scene record based on an existing scene"""
scrapeScene(scraper_id: ID!, scene: SceneUpdateInput!): ScrapedScene @deprecated(reason: "use scrapeSingleScene")
"""Scrapes a complete gallery record based on an existing gallery"""
scrapeGallery(scraper_id: ID!, gallery: GalleryUpdateInput!): ScrapedGallery @deprecated(reason: "use scrapeSingleGallery")
"""Scrape a performer using Freeones"""
scrapeFreeones(performer_name: String!): ScrapedPerformer
scrapeFreeones(performer_name: String!): ScrapedPerformer @deprecated(reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones")
"""Scrape a list of performers from a query"""
scrapeFreeonesPerformerList(query: String!): [String!]!
scrapeFreeonesPerformerList(query: String!): [String!]! @deprecated(reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones")
"""Query StashBox for scenes"""
queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]!
queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]!
queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]! @deprecated(reason: "use scrapeSingleScene or scrapeMultiScenes")
"""Query StashBox for performers"""
queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]! @deprecated(reason: "use scrapeSinglePerformer or scrapeMultiPerformers")
# === end deprecated methods ===
# Plugins
"""List loaded plugins"""
@@ -108,7 +127,12 @@ type Query {
"""Returns the current, complete configuration"""
configuration: ConfigResult!
"""Returns an array of paths for the given path"""
directory(path: String): Directory!
directory(
"The directory path to list"
path: String,
"Desired collation locale. Determines the order of the directory result. eg. 'en-US', 'pt-BR', ..."
locale: String = "en"
): Directory!
# System status
systemStatus: SystemStatus!
@@ -130,7 +154,7 @@ type Query {
# Version
version: Version!
# LatestVersion
latestversion: ShortVersion!
}
@@ -213,6 +237,7 @@ type Mutation {
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
configureDLNA(input: ConfigDLNAInput!): ConfigDLNAResult!
configureScraping(input: ConfigScrapingInput!): ConfigScrapingResult!
configureDefaults(input: ConfigDefaultSettingsInput!): ConfigDefaultSettingsResult!
"""Generate and set (or clear) API key"""
generateAPIKey(input: GenerateAPIKeyInput!): String!
@@ -235,6 +260,8 @@ type Mutation {
metadataAutoTag(input: AutoTagMetadataInput!): ID!
"""Clean metadata. Returns the job ID"""
metadataClean(input: CleanMetadataInput!): ID!
"""Identifies scenes using scrapers. Returns the job ID"""
metadataIdentify(input: IdentifyMetadataInput!): ID!
"""Migrate generated files for the current hash naming"""
migrateHashNaming: ID!
@@ -256,7 +283,7 @@ type Mutation {
"""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"""

View File

@@ -39,6 +39,8 @@ input ConfigGeneralInput {
databasePath: String
"""Path to generated files"""
generatedPath: String
"""Path to import/export files"""
metadataPath: String
"""Path to cache"""
cachePath: String
"""Whether to calculate MD5 checksums for scene video files"""
@@ -63,12 +65,16 @@ input ConfigGeneralInput {
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""Write image thumbnails to disk when generating on the fly"""
writeImageThumbnails: Boolean
"""Username"""
username: String
"""Password"""
password: String
"""Maximum session cookie age"""
maxSessionAge: Int
"""Comma separated list of proxies to allow traffic from"""
trustedProxies: [String!]
"""Name of the log file"""
logFile: String
"""Whether to also output to stderr"""
@@ -108,6 +114,8 @@ type ConfigGeneralResult {
databasePath: String!
"""Path to generated files"""
generatedPath: String!
"""Path to import/export files"""
metadataPath: String!
"""Path to the config file used"""
configFilePath: String!
"""Path to scrapers"""
@@ -136,6 +144,8 @@ type ConfigGeneralResult {
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""Write image thumbnails to disk when generating on the fly"""
writeImageThumbnails: Boolean!
"""API Key"""
apiKey: String!
"""Username"""
@@ -144,6 +154,8 @@ type ConfigGeneralResult {
password: String!
"""Maximum session cookie age"""
maxSessionAge: Int!
"""Comma separated list of proxies to allow traffic from"""
trustedProxies: [String!]!
"""Name of the log file"""
logFile: String
"""Whether to also output to stderr"""
@@ -176,56 +188,102 @@ type ConfigGeneralResult {
stashBoxes: [StashBox!]!
}
input ConfigDisableDropdownCreateInput {
performer: Boolean
tag: Boolean
studio: Boolean
}
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"""
wallShowTitle: Boolean
"""Wall playback type"""
wallPlayback: String
"""Maximum duration (in seconds) in which a scene video will loop in the scene player"""
maximumLoopDuration: Int
"""If true, video will autostart on load in the scene player"""
autostartVideo: Boolean
"""If true, video will autostart when loading from play random or play selected"""
autostartVideoOnPlaySelected: Boolean
"""If true, next scene in playlist will be played at video end by default"""
continuePlaylistDefault: Boolean
"""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
"""Set to true to disable creating new objects via the dropdown menus"""
disableDropdownCreate: ConfigDisableDropdownCreateInput
"""Handy Connection Key"""
handyKey: String
"""Funscript Time Offset"""
funscriptOffset: Int
"""True if we should not auto-open a browser window on startup"""
noBrowser: Boolean
}
type ConfigDisableDropdownCreate {
performer: Boolean!
tag: Boolean!
studio: Boolean!
}
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"""
wallShowTitle: Boolean
"""Wall playback type"""
wallPlayback: String
"""Maximum duration (in seconds) in which a scene video will loop in the scene player"""
maximumLoopDuration: Int
""""True if we should not auto-open a browser window on startup"""
noBrowser: Boolean
"""If true, video will autostart on load in the scene player"""
autostartVideo: Boolean
"""If true, video will autostart when loading from play random or play selected"""
autostartVideoOnPlaySelected: Boolean
"""If true, next scene in playlist will be played at video end by default"""
continuePlaylistDefault: Boolean
"""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
"""Fields are true if creating via dropdown menus are disabled"""
disabledDropdownCreate: ConfigDisableDropdownCreate!
"""Handy Connection Key"""
handyKey: String
"""Funscript Time Offset"""
@@ -274,12 +332,31 @@ type ConfigScrapingResult {
excludeTagPatterns: [String!]!
}
type ConfigDefaultSettingsResult {
identify: IdentifyMetadataTaskOptions
"""If true, delete file checkbox will be checked by default"""
deleteFile: Boolean
"""If true, delete generated supporting files checkbox will be checked by default"""
deleteGenerated: Boolean
}
input ConfigDefaultSettingsInput {
identify: IdentifyMetadataInput
"""If true, delete file checkbox will be checked by default"""
deleteFile: Boolean
"""If true, delete generated files checkbox will be checked by default"""
deleteGenerated: Boolean
}
"""All configuration settings"""
type ConfigResult {
general: ConfigGeneralResult!
interface: ConfigInterfaceResult!
dlna: ConfigDLNAResult!
scraping: ConfigScrapingResult!
defaults: ConfigDefaultSettingsResult!
}
"""Directory structure of a path"""

View File

@@ -72,7 +72,7 @@ input PerformerFilterType {
"""Filter to only include performers missing this property"""
is_missing: String
"""Filter to only include performers with these tags"""
tags: MultiCriterionInput
tags: HierarchicalMultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter by scene count"""
@@ -99,11 +99,11 @@ input PerformerFilterType {
input SceneMarkerFilterType {
"""Filter to only include scene markers with this tag"""
tag_id: ID
tag_id: ID @deprecated(reason: "use tags filter instead")
"""Filter to only include scene markers with these tags"""
tags: MultiCriterionInput
tags: HierarchicalMultiCriterionInput
"""Filter to only include scene markers attached to a scene with these tags"""
scene_tags: MultiCriterionInput
scene_tags: HierarchicalMultiCriterionInput
"""Filter to only include scene markers with these performers"""
performers: MultiCriterionInput
}
@@ -143,11 +143,11 @@ input SceneFilterType {
"""Filter to only include scenes with this movie"""
movies: MultiCriterionInput
"""Filter to only include scenes with these tags"""
tags: MultiCriterionInput
tags: HierarchicalMultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include scenes with performers with these tags"""
performer_tags: MultiCriterionInput
performer_tags: HierarchicalMultiCriterionInput
"""Filter to only include scenes with these performers"""
performers: MultiCriterionInput
"""Filter by performer count"""
@@ -176,9 +176,15 @@ input MovieFilterType {
is_missing: String
"""Filter by url"""
url: StringCriterionInput
"""Filter to only include movies where performer appears in a scene"""
performers: MultiCriterionInput
}
input StudioFilterType {
AND: StudioFilterType
OR: StudioFilterType
NOT: StudioFilterType
name: StringCriterionInput
details: StringCriterionInput
"""Filter to only include studios with this parent studio"""
@@ -197,6 +203,8 @@ input StudioFilterType {
gallery_count: IntCriterionInput
"""Filter by url"""
url: StringCriterionInput
"""Filter by studio aliases"""
aliases: StringCriterionInput
}
input GalleryFilterType {
@@ -224,11 +232,11 @@ input GalleryFilterType {
"""Filter to only include galleries with this studio"""
studios: HierarchicalMultiCriterionInput
"""Filter to only include galleries with these tags"""
tags: MultiCriterionInput
tags: HierarchicalMultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include galleries with performers with these tags"""
performer_tags: MultiCriterionInput
performer_tags: HierarchicalMultiCriterionInput
"""Filter to only include galleries with these performers"""
performers: MultiCriterionInput
"""Filter by performer count"""
@@ -267,6 +275,18 @@ input TagFilterType {
"""Filter by number of markers with this tag"""
marker_count: IntCriterionInput
"""Filter by parent tags"""
parents: HierarchicalMultiCriterionInput
"""Filter by child tags"""
children: HierarchicalMultiCriterionInput
"""Filter by number of parent tags the tag has"""
parent_count: IntCriterionInput
"""Filter by number f child tags the tag has"""
child_count: IntCriterionInput
}
input ImageFilterType {
@@ -293,11 +313,11 @@ input ImageFilterType {
"""Filter to only include images with this studio"""
studios: HierarchicalMultiCriterionInput
"""Filter to only include images with these tags"""
tags: MultiCriterionInput
tags: HierarchicalMultiCriterionInput
"""Filter by tag count"""
tag_count: IntCriterionInput
"""Filter to only include images with performers with these tags"""
performer_tags: MultiCriterionInput
performer_tags: HierarchicalMultiCriterionInput
"""Filter to only include images with these performers"""
performers: MultiCriterionInput
"""Filter by performer count"""
@@ -357,7 +377,7 @@ input GenderCriterionInput {
input HierarchicalMultiCriterionInput {
value: [ID!]
modifier: CriterionModifier!
depth: Int!
depth: Int
}
enum FilterMode {

View File

@@ -74,6 +74,11 @@ input BulkGalleryUpdateInput {
input GalleryDestroyInput {
ids: [ID!]!
"""
If true, then the zip file will be deleted if the gallery is zip-file-based.
If gallery is folder-based, then any files not associated with other
galleries will be deleted, along with the folder, if it is not empty.
"""
delete_file: Boolean
delete_generated: Boolean
}

View File

@@ -70,5 +70,9 @@ input ImagesDestroyInput {
type FindImagesResultType {
count: Int!
"""Total megapixels of the images"""
megapixels: Float!
"""Total file size in bytes"""
filesize: Float!
images: [Image!]!
}

View File

@@ -2,6 +2,7 @@
scalar Time
enum LogLevel {
Trace
Debug
Info
Progress

View File

@@ -1,13 +1,15 @@
scalar Upload
input GenerateMetadataInput {
sprites: Boolean!
previews: Boolean!
imagePreviews: Boolean!
sprites: Boolean
previews: Boolean
imagePreviews: Boolean
previewOptions: GeneratePreviewOptionsInput
markers: Boolean!
transcodes: Boolean!
phashes: Boolean!
markers: Boolean
markerImagePreviews: Boolean
markerScreenshots: Boolean
transcodes: Boolean
phashes: Boolean
"""scene ids to generate for"""
sceneIDs: [ID!]
@@ -45,6 +47,8 @@ input ScanMetadataInput {
scanGenerateSprites: Boolean
"""Generate phashes during scan"""
scanGeneratePhashes: Boolean
"""Generate image thumbnails during scan"""
scanGenerateThumbnails: Boolean
}
input CleanMetadataInput {
@@ -63,6 +67,88 @@ input AutoTagMetadataInput {
tags: [String!]
}
enum IdentifyFieldStrategy {
"""Never sets the field value"""
IGNORE
"""
For multi-value fields, merge with existing.
For single-value fields, ignore if already set
"""
MERGE
"""Always replaces the value if a value is found.
For multi-value fields, any existing values are removed and replaced with the
scraped values.
"""
OVERWRITE
}
input IdentifyFieldOptionsInput {
field: String!
strategy: IdentifyFieldStrategy!
"""creates missing objects if needed - only applicable for performers, tags and studios"""
createMissing: Boolean
}
input IdentifyMetadataOptionsInput {
"""any fields missing from here are defaulted to MERGE and createMissing false"""
fieldOptions: [IdentifyFieldOptionsInput!]
"""defaults to true if not provided"""
setCoverImage: Boolean
setOrganized: Boolean
"""defaults to true if not provided"""
includeMalePerformers: Boolean
}
input IdentifySourceInput {
source: ScraperSourceInput!
"""Options defined for a source override the defaults"""
options: IdentifyMetadataOptionsInput
}
input IdentifyMetadataInput {
"""An ordered list of sources to identify items with. Only the first source that finds a match is used."""
sources: [IdentifySourceInput!]!
"""Options defined here override the configured defaults"""
options: IdentifyMetadataOptionsInput
"""scene ids to identify"""
sceneIDs: [ID!]
"""paths of scenes to identify - ignored if scene ids are set"""
paths: [String!]
}
# types for default options
type IdentifyFieldOptions {
field: String!
strategy: IdentifyFieldStrategy!
"""creates missing objects if needed - only applicable for performers, tags and studios"""
createMissing: Boolean
}
type IdentifyMetadataOptions {
"""any fields missing from here are defaulted to MERGE and createMissing false"""
fieldOptions: [IdentifyFieldOptions!]
"""defaults to true if not provided"""
setCoverImage: Boolean
setOrganized: Boolean
"""defaults to true if not provided"""
includeMalePerformers: Boolean
}
type IdentifySource {
source: ScraperSource!
"""Options defined for a source override the defaults"""
options: IdentifyMetadataOptions
}
type IdentifyMetadataTaskOptions {
"""An ordered list of sources to identify items with. Only the first source that finds a match is used."""
sources: [IdentifySource!]!
"""Options defined here override the configured defaults"""
options: IdentifyMetadataOptions
}
input ExportObjectTypeInput {
ids: [String!]
all: Boolean

View File

@@ -17,6 +17,7 @@ type Movie {
front_image_path: String # Resolver
back_image_path: String # Resolver
scene_count: Int # Resolver
scenes: [Scene!]!
}
input MovieCreateInput {
@@ -60,4 +61,4 @@ input MovieDestroyInput {
type FindMoviesResultType {
count: Int!
movies: [Movie!]!
}
}

View File

@@ -42,6 +42,8 @@ type Performer {
weight: Int
created_at: Time!
updated_at: Time!
movie_count: Int
movies: [Movie!]!
}
input PerformerCreateInput {

View File

@@ -12,6 +12,8 @@ type SceneMarker {
stream: String! # Resolver
"""The path to the preview image for this marker"""
preview: String! # Resolver
"""The path to the screenshot image for this marker"""
screenshot: String! # Resolver
}
input SceneMarkerCreateInput {

View File

@@ -103,6 +103,7 @@ input BulkSceneUpdateInput {
gallery_ids: BulkUpdateIds
performer_ids: BulkUpdateIds
tag_ids: BulkUpdateIds
movie_ids: BulkUpdateIds
}
input SceneDestroyInput {
@@ -119,6 +120,10 @@ input ScenesDestroyInput {
type FindScenesResultType {
count: Int!
"""Total duration in seconds"""
duration: Float!
"""Total file size in bytes"""
filesize: Float!
scenes: [Scene!]!
}

View File

@@ -1,12 +1,6 @@
type ScrapedMovieStudio {
"""Set if studio matched"""
id: ID
name: String!
url: String
}
"""A movie from a scraping operation..."""
type ScrapedMovie {
stored_id: ID
name: String
aliases: String
duration: String
@@ -15,7 +9,7 @@ type ScrapedMovie {
director: String
url: String
synopsis: String
studio: ScrapedMovieStudio
studio: ScrapedStudio
"""This should be a base64 encoded data URL"""
front_image: String

View File

@@ -1,5 +1,7 @@
"""A performer from a scraping operation..."""
type ScrapedPerformer {
"""Set if performer matched"""
stored_id: ID
name: String
gender: String
url: String
@@ -16,11 +18,11 @@ type ScrapedPerformer {
tattoos: String
piercings: String
aliases: String
# Should be ScrapedPerformerTag - but would be identical types
tags: [ScrapedSceneTag!]
tags: [ScrapedTag!]
"""This should be a base64 encoded data URL"""
image: String
image: String @deprecated(reason: "use images instead")
images: [String!]
details: String
death_date: String
hair_color: String
@@ -29,6 +31,8 @@ type ScrapedPerformer {
}
input ScrapedPerformerInput {
"""Set if performer matched"""
stored_id: ID
name: String
gender: String
url: String

View File

@@ -26,58 +26,17 @@ type Scraper {
movie: ScraperSpec
}
type ScrapedScenePerformer {
"""Set if performer matched"""
stored_id: ID
name: String!
gender: String
url: String
twitter: String
instagram: String
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
tags: [ScrapedSceneTag!]
remote_site_id: String
images: [String!]
details: String
death_date: String
hair_color: String
weight: String
}
type ScrapedSceneMovie {
"""Set if movie matched"""
stored_id: ID
name: String!
aliases: String
duration: String
date: String
rating: String
director: String
synopsis: String
url: String
}
type ScrapedSceneStudio {
type ScrapedStudio {
"""Set if studio matched"""
stored_id: ID
name: String!
url: String
image: String
remote_site_id: String
}
type ScrapedSceneTag {
type ScrapedTag {
"""Set if tag matched"""
stored_id: ID
name: String!
@@ -94,25 +53,109 @@ type ScrapedScene {
file: SceneFileType # Resolver
studio: ScrapedSceneStudio
tags: [ScrapedSceneTag!]
performers: [ScrapedScenePerformer!]
movies: [ScrapedSceneMovie!]
studio: ScrapedStudio
tags: [ScrapedTag!]
performers: [ScrapedPerformer!]
movies: [ScrapedMovie!]
remote_site_id: String
duration: Int
fingerprints: [StashBoxFingerprint!]
}
input ScrapedSceneInput {
title: String
details: String
url: String
date: String
# no image, file, duration or relationships
remote_site_id: String
}
type ScrapedGallery {
title: String
details: String
url: String
date: String
studio: ScrapedSceneStudio
tags: [ScrapedSceneTag!]
performers: [ScrapedScenePerformer!]
studio: ScrapedStudio
tags: [ScrapedTag!]
performers: [ScrapedPerformer!]
}
input ScrapedGalleryInput {
title: String
details: String
url: String
date: String
# no studio, tags or performers
}
input ScraperSourceInput {
"""Index of the configured stash-box instance to use. Should be unset if scraper_id is set"""
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
"""Stash-box endpoint"""
stash_box_endpoint: String
"""Scraper ID to scrape with. Should be unset if stash_box_index is set"""
scraper_id: ID
}
type ScraperSource {
"""Index of the configured stash-box instance to use. Should be unset if scraper_id is set"""
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
"""Stash-box endpoint"""
stash_box_endpoint: String
"""Scraper ID to scrape with. Should be unset if stash_box_index is set"""
scraper_id: ID
}
input ScrapeSingleSceneInput {
"""Instructs to query by string"""
query: String
"""Instructs to query by scene fingerprints"""
scene_id: ID
"""Instructs to query by scene fragment"""
scene_input: ScrapedSceneInput
}
input ScrapeMultiScenesInput {
"""Instructs to query by scene fingerprints"""
scene_ids: [ID!]
}
input ScrapeSinglePerformerInput {
"""Instructs to query by string"""
query: String
"""Instructs to query by performer id"""
performer_id: ID
"""Instructs to query by performer fragment"""
performer_input: ScrapedPerformerInput
}
input ScrapeMultiPerformersInput {
"""Instructs to query by scene fingerprints"""
performer_ids: [ID!]
}
input ScrapeSingleGalleryInput {
"""Instructs to query by string"""
query: String
"""Instructs to query by gallery id"""
gallery_id: ID
"""Instructs to query by gallery fragment"""
gallery_input: ScrapedGalleryInput
}
input ScrapeSingleMovieInput {
"""Instructs to query by string"""
query: String
"""Instructs to query by movie id"""
movie_id: ID
"""Instructs to query by gallery fragment"""
movie_input: ScrapedMovieInput
}
input StashBoxSceneQueryInput {
@@ -135,7 +178,7 @@ input StashBoxPerformerQueryInput {
type StashBoxPerformerQueryResult {
query: String!
results: [ScrapedScenePerformer!]!
results: [ScrapedPerformer!]!
}
type StashBoxFingerprint {
@@ -144,10 +187,16 @@ type StashBoxFingerprint {
duration: Int!
}
"""If neither performer_ids nor performer_names are set, tag all performers"""
input StashBoxBatchPerformerTagInput {
"Stash endpoint to use for the performer tagging"
endpoint: Int!
"Fields to exclude when executing the performer tagging"
exclude_fields: [String!]
"Refresh performers already tagged by StashBox if true. Only tag performers with no StashBox tagging if false"
refresh: Boolean!
"If set, only tag these performer ids"
performer_ids: [ID!]
"If set, only tag these performer names"
performer_names: [String!]
}

View File

@@ -5,6 +5,7 @@ type Studio {
url: String
parent_studio: Studio
child_studios: [Studio!]!
aliases: [String!]!
image_path: String # Resolver
scene_count: Int # Resolver
@@ -15,6 +16,8 @@ type Studio {
details: String
created_at: Time!
updated_at: Time!
movie_count: Int
movies: [Movie!]!
}
input StudioCreateInput {
@@ -26,6 +29,7 @@ input StudioCreateInput {
stash_ids: [StashIDInput!]
rating: Int
details: String
aliases: [String!]
}
input StudioUpdateInput {
@@ -38,6 +42,7 @@ input StudioUpdateInput {
stash_ids: [StashIDInput!]
rating: Int
details: String
aliases: [String!]
}
input StudioDestroyInput {

View File

@@ -11,6 +11,9 @@ type Tag {
image_count: Int # Resolver
gallery_count: Int # Resolver
performer_count: Int
parents: [Tag!]!
children: [Tag!]!
}
input TagCreateInput {
@@ -19,6 +22,9 @@ input TagCreateInput {
"""This should be a URL or a base64 encoded data URL"""
image: String
parent_ids: [ID!]
child_ids: [ID!]
}
input TagUpdateInput {
@@ -28,6 +34,9 @@ input TagUpdateInput {
"""This should be a URL or a base64 encoded data URL"""
image: String
parent_ids: [ID!]
child_ids: [ID!]
}
input TagDestroyInput {

View File

@@ -49,6 +49,7 @@ fragment PerformerFragment on Performer {
disambiguation
aliases
gender
merged_ids
urls {
...URLFragment
}
@@ -75,11 +76,6 @@ fragment PerformerFragment on Performer {
piercings {
...BodyModificationFragment
}
details
death_date {
...FuzzyDateFragment
}
weight
}
fragment PerformerAppearanceFragment on PerformerAppearance {
@@ -127,8 +123,8 @@ query FindSceneByFingerprint($fingerprint: FingerprintQueryInput!) {
}
}
query FindScenesByFingerprints($fingerprints: [String!]!) {
findScenesByFingerprints(fingerprints: $fingerprints) {
query FindScenesByFullFingerprints($fingerprints: [FingerprintQueryInput!]!) {
findScenesByFullFingerprints(fingerprints: $fingerprints) {
...SceneFragment
}
}
@@ -151,6 +147,12 @@ query FindPerformerByID($id: ID!) {
}
}
query FindSceneByID($id: ID!) {
findScene(id: $id) {
...SceneFragment
}
}
mutation SubmitFingerprint($input: FingerprintSubmission!) {
submitFingerprint(input: $input)
}

15
main.go
View File

@@ -2,25 +2,38 @@
package main
import (
"embed"
"os"
"os/signal"
"runtime/pprof"
"syscall"
"github.com/stashapp/stash/pkg/api"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
//go:embed ui/v2.5/build
var uiBox embed.FS
//go:embed ui/login
var loginUIBox embed.FS
func main() {
manager.Initialize()
api.Start()
api.Start(uiBox, loginUIBox)
// stop any profiling at exit
defer pprof.StopCPUProfile()
blockForever()
err := manager.GetInstance().Shutdown()
if err != nil {
logger.Errorf("Error when closing: %s", err)
}
}
func blockForever() {

143
pkg/api/authentication.go Normal file
View File

@@ -0,0 +1,143 @@
package api
import (
"errors"
"net"
"net/http"
"net/url"
"strings"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/session"
)
const loginEndPoint = "/login"
const (
tripwireActivatedErrMsg = "Stash is exposed to the public internet without authentication, and is not serving any more content to protect your privacy. " +
"More information and fixes are available at https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet"
externalAccessErrMsg = "You have attempted to access Stash over the internet, and authentication is not enabled. " +
"This is extremely dangerous! The whole world can see your your stash page and browse your files! " +
"Stash is not answering any other requests to protect your privacy. " +
"Please read the log entry or visit https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet"
)
func allowUnauthenticated(r *http.Request) bool {
return strings.HasPrefix(r.URL.Path, loginEndPoint) || r.URL.Path == "/css"
}
func authenticateHandler() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c := config.GetInstance()
if !checkSecurityTripwireActivated(c, w) {
return
}
userID, err := manager.GetInstance().SessionStore.Authenticate(w, r)
if err != nil {
if errors.Is(err, session.ErrUnauthorized) {
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write([]byte(err.Error()))
if err != nil {
logger.Error(err)
}
return
}
// unauthorized error
w.Header().Add("WWW-Authenticate", `FormBased`)
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := session.CheckAllowPublicWithoutAuth(c, r); err != nil {
var externalAccess session.ExternalAccessError
var untrustedProxy session.UntrustedProxyError
switch {
case errors.As(err, &externalAccess):
securityActivateTripwireAccessedFromInternetWithoutAuth(c, externalAccess, w)
return
case errors.As(err, &untrustedProxy):
logger.Warnf("Rejected request from untrusted proxy: %v", net.IP(untrustedProxy))
w.WriteHeader(http.StatusForbidden)
return
default:
logger.Errorf("Error checking external access security: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
ctx := r.Context()
if c.HasCredentials() {
// authentication is required
if userID == "" && !allowUnauthenticated(r) {
// authentication was not received, redirect
// if graphql was requested, we just return a forbidden error
if r.URL.Path == "/graphql" {
w.Header().Add("WWW-Authenticate", `FormBased`)
w.WriteHeader(http.StatusUnauthorized)
return
}
prefix := getProxyPrefix(r.Header)
// otherwise redirect to the login page
u := url.URL{
Path: prefix + "/login",
}
q := u.Query()
q.Set(returnURLParam, prefix+r.URL.Path)
u.RawQuery = q.Encode()
http.Redirect(w, r, u.String(), http.StatusFound)
return
}
}
ctx = session.SetCurrentUserID(ctx, userID)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
func checkSecurityTripwireActivated(c *config.Instance, w http.ResponseWriter) bool {
if accessErr := session.CheckExternalAccessTripwire(c); accessErr != nil {
w.WriteHeader(http.StatusForbidden)
_, err := w.Write([]byte(tripwireActivatedErrMsg))
if err != nil {
logger.Error(err)
}
return false
}
return true
}
func securityActivateTripwireAccessedFromInternetWithoutAuth(c *config.Instance, accessErr session.ExternalAccessError, w http.ResponseWriter) {
session.LogExternalAccessError(accessErr)
err := c.ActivatePublicAccessTripwire(net.IP(accessErr).String())
if err != nil {
logger.Error(err)
}
w.WriteHeader(http.StatusForbidden)
_, err = w.Write([]byte(externalAccessErrMsg))
if err != nil {
logger.Error(err)
}
err = manager.GetInstance().Shutdown()
if err != nil {
logger.Error(err)
}
}

View File

@@ -20,7 +20,7 @@ func getArgumentMap(ctx context.Context) map[string]interface{} {
func getUpdateInputMap(ctx context.Context) map[string]interface{} {
args := getArgumentMap(ctx)
input, _ := args[updateInputField]
input := args[updateInputField]
var ret map[string]interface{}
if input != nil {
ret, _ = input.(map[string]interface{})
@@ -36,7 +36,7 @@ func getUpdateInputMap(ctx context.Context) map[string]interface{} {
func getUpdateInputMaps(ctx context.Context) []map[string]interface{} {
args := getArgumentMap(ctx)
input, _ := args[updateInputField]
input := args[updateInputField]
var ret []map[string]interface{}
if input != nil {
// convert []interface{} into []map[string]interface{}

View File

@@ -1,10 +1,11 @@
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"net/http"
"regexp"
"runtime"
@@ -15,7 +16,7 @@ import (
"github.com/stashapp/stash/pkg/logger"
)
//we use the github REST V3 API as no login is required
// we use the github REST V3 API as no login is required
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"
@@ -29,6 +30,7 @@ var ErrNoVersion = errors.New("no stash version")
var stashReleases = func() map[string]string {
return map[string]string{
"darwin/amd64": "stash-osx",
"darwin/arm64": "stash-osx-applesilicon",
"linux/amd64": "stash-linux",
"windows/amd64": "stash-win.exe",
"linux/arm": "stash-pi",
@@ -106,34 +108,37 @@ type githubTagResponse struct {
Node_id string
}
func makeGithubRequest(url string, output interface{}) error {
func makeGithubRequest(ctx context.Context, url string, output interface{}) error {
client := &http.Client{
Timeout: 3 * time.Second,
}
req, _ := http.NewRequest("GET", url, nil)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req.Header.Add("Accept", apiAcceptHeader) // gh api recommendation , send header with api version
response, err := client.Do(req)
if err != nil {
return fmt.Errorf("Github API request failed: %s", err)
//lint:ignore ST1005 Github is a proper capitalized noun
return fmt.Errorf("Github API request failed: %w", err)
}
if response.StatusCode != http.StatusOK {
//lint:ignore ST1005 Github is a proper capitalized noun
return fmt.Errorf("Github API request failed: %s", response.Status)
}
defer response.Body.Close()
data, err := ioutil.ReadAll(response.Body)
data, err := io.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("Github API read response failed: %s", err)
//lint:ignore ST1005 Github is a proper capitalized noun
return fmt.Errorf("Github API read response failed: %w", err)
}
err = json.Unmarshal(data, output)
if err != nil {
return fmt.Errorf("Unmarshalling Github API response failed: %s", err)
return fmt.Errorf("unmarshalling Github API response failed: %w", err)
}
return nil
@@ -143,7 +148,7 @@ func makeGithubRequest(url string, output interface{}) error {
// If running a build from the "master" branch, then the latest full release
// is used, otherwise it uses the release that is tagged with "latest_develop"
// which is the latest pre-release build.
func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease string, err error) {
func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string, latestRelease string, err error) {
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
@@ -176,14 +181,14 @@ func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease strin
}
release := githubReleasesResponse{}
err = makeGithubRequest(url, &release)
err = makeGithubRequest(ctx, url, &release)
if err != nil {
return "", "", err
}
if release.Prerelease == usePreRelease {
latestVersion = getReleaseHash(release, shortHash, usePreRelease)
latestVersion = getReleaseHash(ctx, release, shortHash, usePreRelease)
if wantedRelease != "" {
for _, asset := range release.Assets {
@@ -196,17 +201,17 @@ func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease strin
}
if latestVersion == "" {
return "", "", fmt.Errorf("No version found for \"%s\"", version)
return "", "", fmt.Errorf("no version found for \"%s\"", version)
}
return latestVersion, latestRelease, nil
}
func getReleaseHash(release githubReleasesResponse, shortHash bool, usePreRelease bool) string {
func getReleaseHash(ctx context.Context, release githubReleasesResponse, shortHash bool, usePreRelease bool) string {
shaLength := len(release.Target_commitish)
// the /latest API call doesn't return the hash in target_commitish
// also add sanity check in case Target_commitish is not 40 characters
if !usePreRelease || shaLength != 40 {
return getShaFromTags(shortHash, release.Tag_name)
return getShaFromTags(ctx, shortHash, release.Tag_name)
}
if shortHash {
@@ -221,9 +226,9 @@ func getReleaseHash(release githubReleasesResponse, shortHash bool, usePreReleas
return release.Target_commitish
}
func printLatestVersion() {
func printLatestVersion(ctx context.Context) {
_, githash, _ = GetVersion()
latest, _, err := GetLatestVersion(true)
latest, _, err := GetLatestVersion(ctx, true)
if err != nil {
logger.Errorf("Couldn't find latest version: %s", err)
} else {
@@ -237,13 +242,21 @@ func printLatestVersion() {
// get sha from the github api tags endpoint
// returns the sha1 hash/shorthash or "" if something's wrong
func getShaFromTags(shortHash bool, name string) string {
func getShaFromTags(ctx context.Context, shortHash bool, name string) string {
url := apiTags
tags := []githubTagResponse{}
err := makeGithubRequest(url, &tags)
err := makeGithubRequest(ctx, url, &tags)
if err != nil {
logger.Errorf("Github Tags Api %v", err)
// If the context is canceled, we don't want to log this as an error
// in the path. The function here just gives up and returns "" if
// something goes wrong. Hence, log the error at the info-level so
// it's still present, but don't treat this as an error.
if errors.Is(err, context.Canceled) {
logger.Infof("aborting sha request due to context cancellation")
} else {
logger.Errorf("Github Tags Api: %v", err)
}
return ""
}
_, gitShort, _ := GetVersion() // retrieve short hash to check actual length

View File

@@ -5,8 +5,8 @@ package api
type key int
const (
galleryKey key = iota
performerKey
// galleryKey key = 0
performerKey key = iota + 1
sceneKey
studioKey
movieKey

View File

@@ -1,24 +1,36 @@
package api
import (
"io"
"io/fs"
"os"
"strings"
"github.com/gobuffalo/packr/v2"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/static"
"github.com/stashapp/stash/pkg/utils"
)
type imageBox struct {
box *packr.Box
box fs.FS
files []string
}
func newImageBox(box *packr.Box) *imageBox {
return &imageBox{
box: box,
files: box.List(),
func newImageBox(box fs.FS) (*imageBox, error) {
ret := &imageBox{
box: box,
}
err := fs.WalkDir(box, ".", func(path string, d fs.DirEntry, err error) error {
if !d.IsDir() {
ret.files = append(ret.files, path)
}
return nil
})
return ret, err
}
var performerBox *imageBox
@@ -26,8 +38,15 @@ var performerBoxMale *imageBox
var performerBoxCustom *imageBox
func initialiseImages() {
performerBox = newImageBox(packr.New("Performer Box", "../../static/performer"))
performerBoxMale = newImageBox(packr.New("Male Performer Box", "../../static/performer_male"))
var err error
performerBox, err = newImageBox(&static.Performer)
if err != nil {
logger.Warnf("error loading performer images: %v", err)
}
performerBoxMale, err = newImageBox(&static.PerformerMale)
if err != nil {
logger.Warnf("error loading male performer images: %v", err)
}
initialiseCustomImages()
}
@@ -36,7 +55,11 @@ func initialiseCustomImages() {
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))
var err error
performerBoxCustom, err = newImageBox(os.DirFS(customPath))
if err != nil {
logger.Warnf("error loading custom performer from %s: %v", customPath, err)
}
} else {
performerBoxCustom = nil
}
@@ -63,5 +86,11 @@ func getRandomPerformerImageUsingName(name, gender, customPath string) ([]byte,
imageFiles := box.files
index := utils.IntFromString(name) % uint64(len(imageFiles))
return box.box.Find(imageFiles[index])
img, err := box.box.Open(imageFiles[index])
if err != nil {
return nil, err
}
defer img.Close()
return io.ReadAll(img)
}

34
pkg/api/locale.go Normal file
View File

@@ -0,0 +1,34 @@
package api
import (
"golang.org/x/text/collate"
"golang.org/x/text/language"
)
// matcher defines a matcher for the languages we support
var matcher = language.NewMatcher([]language.Tag{
language.MustParse("en-US"), // The first language is used as fallback.
language.MustParse("en-GB"),
language.MustParse("en-AU"),
language.MustParse("es-ES"),
language.MustParse("de-DE"),
language.MustParse("it-IT"),
language.MustParse("fr-FR"),
language.MustParse("pt-BR"),
language.MustParse("sv-SE"),
language.MustParse("zh-CN"),
language.MustParse("zh-TW"),
})
// newCollator parses a locale into a collator
// Go through the available matches and return a valid match, in practice the first is a fallback
// Optionally pass collation options through for creation.
// If passed a nil-locale string, return nil
func newCollator(locale *string, opts ...collate.Option) *collate.Collator {
if locale == nil {
return nil
}
tag, _ := language.MatchStrings(matcher, *locale)
return collate.New(tag, opts...)
}

View File

@@ -2,6 +2,7 @@ package api
import (
"context"
"errors"
"sort"
"strconv"
@@ -10,6 +11,11 @@ import (
"github.com/stashapp/stash/pkg/plugin"
)
var (
ErrNotImplemented = errors.New("not implemented")
ErrNotSupported = errors.New("not supported")
)
type hookExecutor interface {
ExecutePostHooks(ctx context.Context, id int, hookType plugin.HookTriggerEnum, input interface{}, inputFields []string)
}
@@ -53,22 +59,6 @@ func (r *Resolver) Tag() models.TagResolver {
return &tagResolver{r}
}
func (r *Resolver) ScrapedSceneTag() models.ScrapedSceneTagResolver {
return &scrapedSceneTagResolver{r}
}
func (r *Resolver) ScrapedSceneMovie() models.ScrapedSceneMovieResolver {
return &scrapedSceneMovieResolver{r}
}
func (r *Resolver) ScrapedScenePerformer() models.ScrapedScenePerformerResolver {
return &scrapedScenePerformerResolver{r}
}
func (r *Resolver) ScrapedSceneStudio() models.ScrapedSceneStudioResolver {
return &scrapedSceneStudioResolver{r}
}
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type subscriptionResolver struct{ *Resolver }
@@ -81,10 +71,6 @@ type imageResolver struct{ *Resolver }
type studioResolver struct{ *Resolver }
type movieResolver struct{ *Resolver }
type tagResolver struct{ *Resolver }
type scrapedSceneTagResolver struct{ *Resolver }
type scrapedSceneMovieResolver struct{ *Resolver }
type scrapedScenePerformerResolver struct{ *Resolver }
type scrapedSceneStudioResolver struct{ *Resolver }
func (r *Resolver) withTxn(ctx context.Context, fn func(r models.Repository) error) error {
return r.txnManager.WithTxn(ctx, fn)
@@ -178,9 +164,9 @@ func (r *queryResolver) Version(ctx context.Context) (*models.Version, error) {
}, nil
}
//Gets latest version (git shorthash commit for now)
// Latestversion returns the latest git shorthash commit.
func (r *queryResolver) Latestversion(ctx context.Context) (*models.ShortVersion, error) {
ver, url, err := GetLatestVersion(true)
ver, url, err := GetLatestVersion(ctx, true)
if err == nil {
logger.Infof("Retrieved latest hash: %s", ver)
} else {

View File

@@ -125,6 +125,18 @@ func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret
return &res, err
}
func (r *movieResolver) Scenes(ctx context.Context, obj *models.Movie) (ret []*models.Scene, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error
ret, err = repo.Scene().FindByMovieID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *movieResolver) CreatedAt(ctx context.Context, obj *models.Movie) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}

View File

@@ -254,3 +254,26 @@ func (r *performerResolver) CreatedAt(ctx context.Context, obj *models.Performer
func (r *performerResolver) UpdatedAt(ctx context.Context, obj *models.Performer) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Movie().FindByPerformerID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *performerResolver) MovieCount(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.Movie().CountByPerformerID(obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}

View File

@@ -140,7 +140,7 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod
}
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 {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Scene()
mqb := repo.Movie()

View File

@@ -58,6 +58,12 @@ func (r *sceneMarkerResolver) Preview(ctx context.Context, obj *models.SceneMark
return urlbuilders.NewSceneURLBuilder(baseURL, sceneID).GetSceneMarkerStreamPreviewURL(obj.ID), nil
}
func (r *sceneMarkerResolver) Screenshot(ctx context.Context, obj *models.SceneMarker) (string, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
sceneID := int(obj.SceneID.Int64)
return urlbuilders.NewSceneURLBuilder(baseURL, sceneID).GetSceneMarkerStreamScreenshotURL(obj.ID), nil
}
func (r *sceneMarkerResolver) CreatedAt(ctx context.Context, obj *models.SceneMarker) (*time.Time, error) {
return &obj.CreatedAt.Timestamp, nil
}

View File

@@ -1,23 +0,0 @@
package api
import (
"context"
"github.com/stashapp/stash/pkg/models"
)
func (r *scrapedSceneTagResolver) StoredID(ctx context.Context, obj *models.ScrapedSceneTag) (*string, error) {
return obj.ID, nil
}
func (r *scrapedSceneMovieResolver) StoredID(ctx context.Context, obj *models.ScrapedSceneMovie) (*string, error) {
return obj.ID, nil
}
func (r *scrapedScenePerformerResolver) StoredID(ctx context.Context, obj *models.ScrapedScenePerformer) (*string, error) {
return obj.ID, nil
}
func (r *scrapedSceneStudioResolver) StoredID(ctx context.Context, obj *models.ScrapedSceneStudio) (*string, error) {
return obj.ID, nil
}

View File

@@ -39,12 +39,23 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
// indicate that image is missing by setting default query param to true
if !hasImage {
imagePath = imagePath + "?default=true"
imagePath += "?default=true"
}
return &imagePath, nil
}
func (r *studioResolver) Aliases(ctx context.Context, obj *models.Studio) (ret []string, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Studio().GetAliases(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, err
}
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 {
@@ -140,3 +151,26 @@ func (r *studioResolver) CreatedAt(ctx context.Context, obj *models.Studio) (*ti
func (r *studioResolver) UpdatedAt(ctx context.Context, obj *models.Studio) (*time.Time, error) {
return &obj.UpdatedAt.Timestamp, nil
}
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Movie().FindByStudioID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *studioResolver) MovieCount(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.Movie().CountByStudioID(obj.ID)
return err
}); err != nil {
return nil, err
}
return &res, nil
}

View File

@@ -10,6 +10,28 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindByChildTagID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *tagResolver) Children(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Tag().FindByParentTagID(obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, 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)

View File

@@ -13,18 +13,21 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
var ErrOverriddenConfig = errors.New("cannot set overridden value")
func (r *mutationResolver) Setup(ctx context.Context, input models.SetupInput) (bool, error) {
err := manager.GetInstance().Setup(input)
err := manager.GetInstance().Setup(ctx, input)
return err == nil, err
}
func (r *mutationResolver) Migrate(ctx context.Context, input models.MigrateInput) (bool, error) {
err := manager.GetInstance().Migrate(input)
err := manager.GetInstance().Migrate(ctx, input)
return err == nil, err
}
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.ConfigGeneralInput) (*models.ConfigGeneralResult, error) {
c := config.GetInstance()
existingPaths := c.GetStashPaths()
if len(input.Stashes) > 0 {
for _, s := range input.Stashes {
@@ -46,7 +49,20 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
c.Set(config.Stash, input.Stashes)
}
if input.DatabasePath != nil {
checkConfigOverride := func(key string) error {
if c.HasOverride(key) {
return fmt.Errorf("%w: %s", ErrOverriddenConfig, key)
}
return nil
}
existingDBPath := c.GetDatabasePath()
if input.DatabasePath != nil && existingDBPath != *input.DatabasePath {
if err := checkConfigOverride(config.Database); err != nil {
return makeConfigGeneralResult(), err
}
ext := filepath.Ext(*input.DatabasePath)
if ext != ".db" && ext != ".sqlite" && ext != ".sqlite3" {
return makeConfigGeneralResult(), fmt.Errorf("invalid database path, use extension db, sqlite, or sqlite3")
@@ -54,14 +70,38 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
c.Set(config.Database, input.DatabasePath)
}
if input.GeneratedPath != nil {
existingGeneratedPath := c.GetGeneratedPath()
if input.GeneratedPath != nil && existingGeneratedPath != *input.GeneratedPath {
if err := checkConfigOverride(config.Generated); err != nil {
return makeConfigGeneralResult(), err
}
if err := utils.EnsureDir(*input.GeneratedPath); err != nil {
return makeConfigGeneralResult(), err
}
c.Set(config.Generated, input.GeneratedPath)
}
if input.CachePath != nil {
existingMetadataPath := c.GetMetadataPath()
if input.MetadataPath != nil && existingMetadataPath != *input.MetadataPath {
if err := checkConfigOverride(config.Metadata); err != nil {
return makeConfigGeneralResult(), err
}
if *input.MetadataPath != "" {
if err := utils.EnsureDir(*input.MetadataPath); err != nil {
return makeConfigGeneralResult(), err
}
}
c.Set(config.Metadata, input.MetadataPath)
}
existingCachePath := c.GetCachePath()
if input.CachePath != nil && existingCachePath != *input.CachePath {
if err := checkConfigOverride(config.Metadata); err != nil {
return makeConfigGeneralResult(), err
}
if *input.CachePath != "" {
if err := utils.EnsureDir(*input.CachePath); err != nil {
return makeConfigGeneralResult(), err
@@ -115,6 +155,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
c.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
}
if input.WriteImageThumbnails != nil {
c.Set(config.WriteImageThumbnails, *input.WriteImageThumbnails)
}
if input.Username != nil {
c.Set(config.Username, input.Username)
}
@@ -133,6 +177,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
c.Set(config.MaxSessionAge, *input.MaxSessionAge)
}
if input.TrustedProxies != nil {
c.Set(config.TrustedProxies, input.TrustedProxies)
}
if input.LogFile != nil {
c.Set(config.LogFile, input.LogFile)
}
@@ -208,17 +256,21 @@ 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()
setBool := func(key string, v *bool) {
if v != nil {
c.Set(key, *v)
}
}
if input.MenuItems != nil {
c.Set(config.MenuItems, input.MenuItems)
}
if input.SoundOnPreview != nil {
c.Set(config.SoundOnPreview, *input.SoundOnPreview)
}
setBool(config.SoundOnPreview, input.SoundOnPreview)
setBool(config.WallShowTitle, input.WallShowTitle)
if input.WallShowTitle != nil {
c.Set(config.WallShowTitle, *input.WallShowTitle)
}
setBool(config.NoBrowser, input.NoBrowser)
if input.WallPlayback != nil {
c.Set(config.WallPlayback, *input.WallPlayback)
@@ -228,13 +280,10 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
c.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
}
if input.AutostartVideo != nil {
c.Set(config.AutostartVideo, *input.AutostartVideo)
}
if input.ShowStudioAsText != nil {
c.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
}
setBool(config.AutostartVideo, input.AutostartVideo)
setBool(config.ShowStudioAsText, input.ShowStudioAsText)
setBool(config.AutostartVideoOnPlaySelected, input.AutostartVideoOnPlaySelected)
setBool(config.ContinuePlaylistDefault, input.ContinuePlaylistDefault)
if input.Language != nil {
c.Set(config.Language, *input.Language)
@@ -252,8 +301,13 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
c.SetCSS(css)
if input.CSSEnabled != nil {
c.Set(config.CSSEnabled, *input.CSSEnabled)
setBool(config.CSSEnabled, input.CSSEnabled)
if input.DisableDropdownCreate != nil {
ddc := input.DisableDropdownCreate
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
setBool(config.DisableDropdownCreateStudio, ddc.Studio)
setBool(config.DisableDropdownCreateTag, ddc.Tag)
}
if input.HandyKey != nil {
@@ -289,7 +343,9 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input models.Confi
if !*input.Enabled && dlnaService.IsRunning() {
dlnaService.Stop(nil)
} else if *input.Enabled && !dlnaService.IsRunning() {
dlnaService.Start(nil)
if err := dlnaService.Start(nil); err != nil {
logger.Warnf("error starting DLNA service: %v", err)
}
}
}
@@ -331,6 +387,28 @@ func (r *mutationResolver) ConfigureScraping(ctx context.Context, input models.C
return makeConfigScrapingResult(), nil
}
func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.ConfigDefaultSettingsInput) (*models.ConfigDefaultSettingsResult, error) {
c := config.GetInstance()
if input.Identify != nil {
c.Set(config.DefaultIdentifySettings, input.Identify)
}
if input.DeleteFile != nil {
c.Set(config.DeleteFileDefault, *input.DeleteFile)
}
if input.DeleteGenerated != nil {
c.Set(config.DeleteGeneratedDefault, *input.DeleteGenerated)
}
if err := c.Write(); err != nil {
return makeConfigDefaultsResult(), err
}
return makeConfigDefaultsResult(), nil
}
func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input models.GenerateAPIKeyInput) (string, error) {
c := config.GetInstance()

View File

@@ -441,7 +441,7 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
return err
}
if len(imgGalleries) == 0 {
if len(imgGalleries) == 1 {
if err := iqb.Destroy(img.ID); err != nil {
return err
}
@@ -465,13 +465,15 @@ func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.Gall
// if delete file is true, then delete the file as well
// if it fails, just log a message
if input.DeleteFile != nil && *input.DeleteFile {
for _, gallery := range galleries {
manager.DeleteGalleryFile(gallery)
}
// #1804 - delete the image files first, since they must be removed
// before deleting a folder
for _, img := range imgsToDelete {
manager.DeleteImageFile(img)
}
for _, gallery := range galleries {
manager.DeleteGalleryFile(gallery)
}
}
// if delete generated is true, then delete the generated files

View File

@@ -2,7 +2,8 @@ package api
import (
"context"
"io/ioutil"
"fmt"
"os"
"path/filepath"
"strconv"
"sync"
@@ -89,6 +90,13 @@ func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.Aut
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) MetadataIdentify(ctx context.Context, input models.IdentifyMetadataInput) (string, error) {
t := manager.CreateIdentifyJob(input)
jobID := manager.GetInstance().JobManager.Add(ctx, "Identifying...", t)
return strconv.Itoa(jobID), nil
}
func (r *mutationResolver) MetadataClean(ctx context.Context, input models.CleanMetadataInput) (string, error) {
jobID := manager.GetInstance().Clean(ctx, input)
return strconv.Itoa(jobID), nil
@@ -105,8 +113,10 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input models.Back
mgr := manager.GetInstance()
var backupPath string
if download {
utils.EnsureDir(mgr.Paths.Generated.Downloads)
f, err := ioutil.TempFile(mgr.Paths.Generated.Downloads, "backup*.sqlite")
if err := utils.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
}
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "backup*.sqlite")
if err != nil {
return nil, err
}

View File

@@ -38,7 +38,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
// Process the base 64 encoded image string
if input.FrontImage != nil {
frontimageData, err = utils.ProcessImageInput(*input.FrontImage)
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
if err != nil {
return nil, err
}
@@ -46,7 +46,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
// Process the base 64 encoded image string
if input.BackImage != nil {
backimageData, err = utils.ProcessImageInput(*input.BackImage)
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
if err != nil {
return nil, err
}
@@ -139,7 +139,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
var frontimageData []byte
frontImageIncluded := translator.hasField("front_image")
if input.FrontImage != nil {
frontimageData, err = utils.ProcessImageInput(*input.FrontImage)
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
if err != nil {
return nil, err
}
@@ -147,7 +147,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
backImageIncluded := translator.hasField("back_image")
var backimageData []byte
if input.BackImage != nil {
backimageData, err = utils.ProcessImageInput(*input.BackImage)
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
if err != nil {
return nil, err
}
@@ -202,7 +202,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
// HACK - if front image is null and back image is not null, then set the front image
// to the default image since we can't have a null front image and a non-null back image
if frontimageData == nil && backimageData != nil {
frontimageData, _ = utils.ProcessImageInput(models.DefaultMovieImage)
frontimageData, _ = utils.ProcessImageInput(ctx, models.DefaultMovieImage)
}
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {

View File

@@ -32,7 +32,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
var err error
if input.Image != nil {
imageData, err = utils.ProcessImageInput(*input.Image)
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
}
if err != nil {
@@ -178,7 +178,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
var err error
imageIncluded := translator.hasField("image")
if input.Image != nil {
imageData, err = utils.ProcessImageInput(*input.Image)
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
if err != nil {
return nil, err
}

View File

@@ -17,7 +17,7 @@ func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, t
func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) {
err := manager.GetInstance().PluginCache.LoadPlugins()
if err != nil {
logger.Errorf("Error reading plugin configs: %s", err.Error())
logger.Errorf("Error reading plugin configs: %v", err)
}
return true, nil

View File

@@ -11,6 +11,7 @@ import (
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/utils"
)
@@ -32,7 +33,7 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
// Start the transaction and save the scene
if err := r.withTxn(ctx, func(repo models.Repository) error {
ret, err = r.sceneUpdate(input, translator, repo)
ret, err = r.sceneUpdate(ctx, input, translator, repo)
return err
}); err != nil {
return nil, err
@@ -52,7 +53,7 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
inputMap: inputMaps[i],
}
thisScene, err := r.sceneUpdate(*scene, translator, repo)
thisScene, err := r.sceneUpdate(ctx, *scene, translator, repo)
ret = append(ret, thisScene)
if err != nil {
@@ -85,7 +86,7 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce
return newRet, nil
}
func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Scene, error) {
func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUpdateInput, translator changesetTranslator, repo models.Repository) (*models.Scene, error) {
// Populate scene from the input
sceneID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -110,7 +111,7 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator
if input.CoverImage != nil && *input.CoverImage != "" {
var err error
coverImageData, err = utils.ProcessImageInput(*input.CoverImage)
coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage)
if err != nil {
return nil, err
}
@@ -119,7 +120,7 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator
}
qb := repo.Scene()
scene, err := qb.Update(updatedScene)
s, err := qb.Update(updatedScene)
if err != nil {
return nil, err
}
@@ -169,13 +170,13 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator
// only update the cover image if provided and everything else was successful
if coverImageData != nil {
err = manager.SetSceneScreenshot(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
err = scene.SetScreenshot(manager.GetInstance().Paths, s.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), coverImageData)
if err != nil {
return nil, err
}
}
return scene, nil
return s, nil
}
func (r *mutationResolver) updateScenePerformers(qb models.SceneReaderWriter, sceneID int, performerIDs []string) error {
@@ -304,6 +305,18 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
return err
}
}
// Save the movies
if translator.hasField("movie_ids") {
movies, err := adjustSceneMovieIDs(qb, sceneID, *input.MovieIds)
if err != nil {
return err
}
if err := qb.UpdateMovies(sceneID, movies); err != nil {
return err
}
}
}
return nil
@@ -395,6 +408,48 @@ func adjustSceneGalleryIDs(qb models.SceneReader, sceneID int, ids models.BulkUp
return adjustIDs(ret, ids), nil
}
func adjustSceneMovieIDs(qb models.SceneReader, sceneID int, updateIDs models.BulkUpdateIds) ([]models.MoviesScenes, error) {
existingMovies, err := qb.GetMovies(sceneID)
if err != nil {
return nil, err
}
// if we are setting the ids, just return the ids
if updateIDs.Mode == models.BulkUpdateIDModeSet {
existingMovies = []models.MoviesScenes{}
for _, idStr := range updateIDs.Ids {
id, _ := strconv.Atoi(idStr)
existingMovies = append(existingMovies, models.MoviesScenes{MovieID: id})
}
return existingMovies, nil
}
for _, idStr := range updateIDs.Ids {
id, _ := strconv.Atoi(idStr)
// look for the id in the list
foundExisting := false
for idx, existingMovie := range existingMovies {
if existingMovie.MovieID == id {
if updateIDs.Mode == models.BulkUpdateIDModeRemove {
// remove from the list
existingMovies = append(existingMovies[:idx], existingMovies[idx+1:]...)
}
foundExisting = true
break
}
}
if !foundExisting && updateIDs.Mode != models.BulkUpdateIDModeRemove {
existingMovies = append(existingMovies, models.MoviesScenes{MovieID: id})
}
}
return existingMovies, err
}
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
sceneID, err := strconv.Atoi(input.ID)
if err != nil {

View File

@@ -6,6 +6,8 @@ import (
"strconv"
"time"
"github.com/stashapp/stash/pkg/studio"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
@@ -32,7 +34,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
// Process the base 64 encoded image string
if input.Image != nil {
imageData, err = utils.ProcessImageInput(*input.Image)
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
if err != nil {
return nil, err
}
@@ -64,19 +66,19 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
}
// Start the transaction and save the studio
var studio *models.Studio
var s *models.Studio
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Studio()
var err error
studio, err = qb.Create(newStudio)
s, err = qb.Create(newStudio)
if err != nil {
return err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(studio.ID, imageData); err != nil {
if err := qb.UpdateImage(s.ID, imageData); err != nil {
return err
}
}
@@ -84,7 +86,17 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
// Save the stash_ids
if input.StashIds != nil {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
if err := qb.UpdateStashIDs(studio.ID, stashIDJoins); err != nil {
if err := qb.UpdateStashIDs(s.ID, stashIDJoins); err != nil {
return err
}
}
if len(input.Aliases) > 0 {
if err := studio.EnsureAliasesUnique(s.ID, input.Aliases, qb); err != nil {
return err
}
if err := qb.UpdateAliases(s.ID, input.Aliases); err != nil {
return err
}
}
@@ -94,8 +106,8 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, studio.ID, plugin.StudioCreatePost, input, nil)
return r.getStudio(ctx, studio.ID)
r.hookExecutor.ExecutePostHooks(ctx, s.ID, plugin.StudioCreatePost, input, nil)
return r.getStudio(ctx, s.ID)
}
func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.StudioUpdateInput) (*models.Studio, error) {
@@ -118,7 +130,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
imageIncluded := translator.hasField("image")
if input.Image != nil {
var err error
imageData, err = utils.ProcessImageInput(*input.Image)
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
if err != nil {
return nil, err
}
@@ -136,7 +148,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
// Start the transaction and save the studio
var studio *models.Studio
var s *models.Studio
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Studio()
@@ -145,19 +157,19 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
}
var err error
studio, err = qb.Update(updatedStudio)
s, err = qb.Update(updatedStudio)
if err != nil {
return err
}
// update image table
if len(imageData) > 0 {
if err := qb.UpdateImage(studio.ID, imageData); err != nil {
if err := qb.UpdateImage(s.ID, imageData); err != nil {
return err
}
} else if imageIncluded {
// must be unsetting
if err := qb.DestroyImage(studio.ID); err != nil {
if err := qb.DestroyImage(s.ID); err != nil {
return err
}
}
@@ -170,13 +182,23 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
}
}
if translator.hasField("aliases") {
if err := studio.EnsureAliasesUnique(studioID, input.Aliases, qb); err != nil {
return err
}
if err := qb.UpdateAliases(studioID, input.Aliases); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
r.hookExecutor.ExecutePostHooks(ctx, studio.ID, plugin.StudioUpdatePost, input, translator.getFields())
return r.getStudio(ctx, studio.ID)
r.hookExecutor.ExecutePostHooks(ctx, s.ID, plugin.StudioUpdatePost, input, translator.getFields())
return r.getStudio(ctx, s.ID)
}
func (r *mutationResolver) StudioDestroy(ctx context.Context, input models.StudioDestroyInput) (bool, error) {

View File

@@ -6,6 +6,7 @@ import (
"strconv"
"time"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/tag"
@@ -36,14 +37,31 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
var err error
if input.Image != nil {
imageData, err = utils.ProcessImageInput(*input.Image)
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
if err != nil {
return nil, err
}
}
// Start the transaction and save the t
var parentIDs []int
var childIDs []int
if len(input.ParentIds) > 0 {
parentIDs, err = utils.StringSliceToIntSlice(input.ParentIds)
if err != nil {
return nil, err
}
}
if len(input.ChildIds) > 0 {
childIDs, err = utils.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return nil, err
}
}
// Start the transaction and save the tag
var t *models.Tag
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Tag()
@@ -75,6 +93,26 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
}
}
if len(parentIDs) > 0 {
if err := qb.UpdateParentTags(t.ID, parentIDs); err != nil {
return err
}
}
if len(childIDs) > 0 {
if err := qb.UpdateChildTags(t.ID, childIDs); err != nil {
return err
}
}
// FIXME: This should be called before any changes are made, but
// requires a rewrite of ValidateHierarchy.
if len(parentIDs) > 0 || len(childIDs) > 0 {
if err := tag.ValidateHierarchy(t, parentIDs, childIDs, qb); err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
@@ -99,13 +137,30 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
imageIncluded := translator.hasField("image")
if input.Image != nil {
imageData, err = utils.ProcessImageInput(*input.Image)
imageData, err = utils.ProcessImageInput(ctx, *input.Image)
if err != nil {
return nil, err
}
}
var parentIDs []int
var childIDs []int
if translator.hasField("parent_ids") {
parentIDs, err = utils.StringSliceToIntSlice(input.ParentIds)
if err != nil {
return nil, err
}
}
if translator.hasField("child_ids") {
childIDs, err = utils.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return nil, err
}
}
// Start the transaction and save the tag
var t *models.Tag
if err := r.withTxn(ctx, func(repo models.Repository) error {
@@ -161,6 +216,27 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
}
}
if parentIDs != nil {
if err := qb.UpdateParentTags(tagID, parentIDs); err != nil {
return err
}
}
if childIDs != nil {
if err := qb.UpdateChildTags(tagID, childIDs); err != nil {
return err
}
}
// FIXME: This should be called before any changes are made, but
// requires a rewrite of ValidateHierarchy.
if parentIDs != nil || childIDs != nil {
if err := tag.ValidateHierarchy(t, parentIDs, childIDs, qb); err != nil {
logger.Errorf("Error saving tag: %s", err)
return err
}
}
return nil
}); err != nil {
return nil, err
@@ -242,10 +318,30 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input models.TagsMerge
return fmt.Errorf("Tag with ID %d not found", destination)
}
parents, children, err := tag.MergeHierarchy(destination, source, qb)
if err != nil {
return err
}
if err = qb.Merge(source, destination); err != nil {
return err
}
err = qb.UpdateParentTags(destination, parents)
if err != nil {
return err
}
err = qb.UpdateChildTags(destination, children)
if err != nil {
return err
}
err = tag.ValidateHierarchy(t, parents, children, qb)
if err != nil {
logger.Errorf("Error merging tag: %s", err)
return err
}
return nil
}); err != nil {
return nil, err

View File

@@ -6,23 +6,26 @@ import (
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
"golang.org/x/text/collate"
)
func (r *queryResolver) Configuration(ctx context.Context) (*models.ConfigResult, error) {
return makeConfigResult(), nil
}
func (r *queryResolver) Directory(ctx context.Context, path *string) (*models.Directory, error) {
func (r *queryResolver) Directory(ctx context.Context, path, locale *string) (*models.Directory, error) {
directory := &models.Directory{}
var err error
col := newCollator(locale, collate.IgnoreCase, collate.Numeric)
var dirPath = ""
if path != nil {
dirPath = *path
}
currentDir := utils.GetDir(dirPath)
directories, err := utils.ListDir(currentDir)
directories, err := utils.ListDir(col, currentDir)
if err != nil {
return directory, err
}
@@ -40,6 +43,7 @@ func makeConfigResult() *models.ConfigResult {
Interface: makeConfigInterfaceResult(),
Dlna: makeConfigDLNAResult(),
Scraping: makeConfigScrapingResult(),
Defaults: makeConfigDefaultsResult(),
}
}
@@ -59,7 +63,8 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
Stashes: config.GetStashPaths(),
DatabasePath: config.GetDatabasePath(),
GeneratedPath: config.GetGeneratedPath(),
ConfigFilePath: config.GetConfigFilePath(),
MetadataPath: config.GetMetadataPath(),
ConfigFilePath: config.GetConfigFile(),
ScrapersPath: config.GetScrapersPath(),
CachePath: config.GetCachePath(),
CalculateMd5: config.IsCalculateMD5(),
@@ -73,10 +78,12 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
PreviewPreset: config.GetPreviewPreset(),
MaxTranscodeSize: &maxTranscodeSize,
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
WriteImageThumbnails: config.IsWriteImageThumbnails(),
APIKey: config.GetAPIKey(),
Username: config.GetUsername(),
Password: config.GetPasswordHash(),
MaxSessionAge: config.GetMaxSessionAge(),
TrustedProxies: config.GetTrustedProxies(),
LogFile: &logFile,
LogOut: config.GetLogOut(),
LogLevel: config.GetLogLevel(),
@@ -101,8 +108,11 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
soundOnPreview := config.GetSoundOnPreview()
wallShowTitle := config.GetWallShowTitle()
wallPlayback := config.GetWallPlayback()
noBrowser := config.GetNoBrowser()
maximumLoopDuration := config.GetMaximumLoopDuration()
autostartVideo := config.GetAutostartVideo()
autostartVideoOnPlaySelected := config.GetAutostartVideoOnPlaySelected()
continuePlaylistDefault := config.GetContinuePlaylistDefault()
showStudioAsText := config.GetShowStudioAsText()
css := config.GetCSS()
cssEnabled := config.GetCSSEnabled()
@@ -112,19 +122,23 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
scriptOffset := config.GetFunscriptOffset()
return &models.ConfigInterfaceResult{
MenuItems: menuItems,
SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle,
WallPlayback: &wallPlayback,
MaximumLoopDuration: &maximumLoopDuration,
AutostartVideo: &autostartVideo,
ShowStudioAsText: &showStudioAsText,
CSS: &css,
CSSEnabled: &cssEnabled,
Language: &language,
SlideshowDelay: &slideshowDelay,
HandyKey: &handyKey,
FunscriptOffset: &scriptOffset,
MenuItems: menuItems,
SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle,
WallPlayback: &wallPlayback,
MaximumLoopDuration: &maximumLoopDuration,
NoBrowser: &noBrowser,
AutostartVideo: &autostartVideo,
ShowStudioAsText: &showStudioAsText,
AutostartVideoOnPlaySelected: &autostartVideoOnPlaySelected,
ContinuePlaylistDefault: &continuePlaylistDefault,
CSS: &css,
CSSEnabled: &cssEnabled,
Language: &language,
SlideshowDelay: &slideshowDelay,
DisabledDropdownCreate: config.GetDisableDropdownCreate(),
HandyKey: &handyKey,
FunscriptOffset: &scriptOffset,
}
}
@@ -152,3 +166,15 @@ func makeConfigScrapingResult() *models.ConfigScrapingResult {
ExcludeTagPatterns: config.GetScraperExcludeTagPatterns(),
}
}
func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult {
config := config.GetInstance()
deleteFileDefault := config.GetDeleteFileDefault()
deleteGeneratedDefault := config.GetDeleteGeneratedDefault()
return &models.ConfigDefaultSettingsResult{
Identify: config.GetDefaultIdentifySettings(),
DeleteFile: &deleteFileDefault,
DeleteGenerated: &deleteGeneratedDefault,
}
}

View File

@@ -4,7 +4,9 @@ import (
"context"
"strconv"
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
@@ -39,14 +41,32 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *models.FindImagesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
qb := repo.Image()
images, total, err := qb.Query(imageFilter, filter)
fields := graphql.CollectAllFields(ctx)
result, err := qb.Query(models.ImageQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,
Count: utils.StrInclude(fields, "count"),
},
ImageFilter: imageFilter,
Megapixels: utils.StrInclude(fields, "megapixels"),
TotalSize: utils.StrInclude(fields, "filesize"),
})
if err != nil {
return err
}
images, err := result.Resolve()
if err != nil {
return err
}
ret = &models.FindImagesResultType{
Count: total,
Images: images,
Count: result.Count,
Images: images,
Megapixels: result.Megapixels,
Filesize: result.TotalSize,
}
return nil

View File

@@ -4,8 +4,10 @@ import (
"context"
"strconv"
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
@@ -65,16 +67,34 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input models.SceneH
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var scenes []*models.Scene
var total int
var err error
fields := graphql.CollectAllFields(ctx)
result := &models.SceneQueryResult{}
if len(sceneIDs) > 0 {
scenes, err = repo.Scene().FindMany(sceneIDs)
if err == nil {
total = len(scenes)
result.Count = len(scenes)
for _, s := range scenes {
result.TotalDuration += s.Duration.Float64
size, _ := strconv.ParseFloat(s.Size.String, 64)
result.TotalSize += size
}
}
} else {
scenes, total, err = repo.Scene().Query(sceneFilter, filter)
result, err = repo.Scene().Query(models.SceneQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,
Count: utils.StrInclude(fields, "count"),
},
SceneFilter: sceneFilter,
TotalDuration: utils.StrInclude(fields, "duration"),
TotalSize: utils.StrInclude(fields, "filesize"),
})
if err == nil {
scenes, err = result.Resolve()
}
}
if err != nil {
@@ -82,8 +102,10 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
}
ret = &models.FindScenesResultType{
Count: total,
Scenes: scenes,
Count: result.Count,
Scenes: scenes,
Duration: result.TotalDuration,
Filesize: result.TotalSize,
}
return nil
@@ -114,14 +136,31 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
queryFilter.Q = nil
}
scenes, total, err := repo.Scene().Query(sceneFilter, queryFilter)
fields := graphql.CollectAllFields(ctx)
result, err := repo.Scene().Query(models.SceneQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: queryFilter,
Count: utils.StrInclude(fields, "count"),
},
SceneFilter: sceneFilter,
TotalDuration: utils.StrInclude(fields, "duration"),
TotalSize: utils.StrInclude(fields, "filesize"),
})
if err != nil {
return err
}
scenes, err := result.Resolve()
if err != nil {
return err
}
ret = &models.FindScenesResultType{
Count: total,
Scenes: scenes,
Count: result.Count,
Scenes: scenes,
Duration: result.TotalDuration,
Filesize: result.TotalSize,
}
return nil

View File

@@ -2,7 +2,9 @@ package api
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
@@ -29,8 +31,9 @@ func (r *queryResolver) ScrapeFreeonesPerformerList(ctx context.Context, query s
var ret []string
for _, v := range scrapedPerformers {
name := v.Name
ret = append(ret, *name)
if v.Name != nil {
ret = append(ret, *v.Name)
}
}
return ret, nil
@@ -68,8 +71,21 @@ func (r *queryResolver) ScrapePerformerURL(ctx context.Context, url string) (*mo
return manager.GetInstance().ScraperCache.ScrapePerformerURL(url)
}
func (r *queryResolver) ScrapeSceneQuery(ctx context.Context, scraperID string, query string) ([]*models.ScrapedScene, error) {
if query == "" {
return nil, nil
}
return manager.GetInstance().ScraperCache.ScrapeSceneQuery(scraperID, query)
}
func (r *queryResolver) ScrapeScene(ctx context.Context, scraperID string, scene models.SceneUpdateInput) (*models.ScrapedScene, error) {
return manager.GetInstance().ScraperCache.ScrapeScene(scraperID, scene)
id, err := strconv.Atoi(scene.ID)
if err != nil {
return nil, err
}
return manager.GetInstance().ScraperCache.ScrapeScene(scraperID, id)
}
func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*models.ScrapedScene, error) {
@@ -77,7 +93,12 @@ func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*models
}
func (r *queryResolver) ScrapeGallery(ctx context.Context, scraperID string, gallery models.GalleryUpdateInput) (*models.ScrapedGallery, error) {
return manager.GetInstance().ScraperCache.ScrapeGallery(scraperID, gallery)
id, err := strconv.Atoi(gallery.ID)
if err != nil {
return nil, err
}
return manager.GetInstance().ScraperCache.ScrapeGallery(scraperID, id)
}
func (r *queryResolver) ScrapeGalleryURL(ctx context.Context, url string) (*models.ScrapedGallery, error) {
@@ -98,11 +119,11 @@ func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.Sta
client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager)
if len(input.SceneIds) > 0 {
return client.FindStashBoxScenesByFingerprints(input.SceneIds)
return client.FindStashBoxScenesByFingerprintsFlat(input.SceneIds)
}
if input.Q != nil {
return client.QueryStashBoxScene(*input.Q)
return client.QueryStashBoxScene(ctx, *input.Q)
}
return nil, nil
@@ -127,3 +148,180 @@ func (r *queryResolver) QueryStashBoxPerformer(ctx context.Context, input models
return nil, nil
}
func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) {
boxes := config.GetInstance().GetStashBoxes()
if index < 0 || index >= len(boxes) {
return nil, fmt.Errorf("invalid stash_box_index %d", index)
}
return stashbox.NewClient(*boxes[index], r.txnManager), nil
}
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeSingleSceneInput) ([]*models.ScrapedScene, error) {
if source.ScraperID != nil {
var singleScene *models.ScrapedScene
var err error
switch {
case input.SceneID != nil:
var sceneID int
sceneID, err = strconv.Atoi(*input.SceneID)
if err != nil {
return nil, err
}
singleScene, err = manager.GetInstance().ScraperCache.ScrapeScene(*source.ScraperID, sceneID)
case input.SceneInput != nil:
singleScene, err = manager.GetInstance().ScraperCache.ScrapeSceneFragment(*source.ScraperID, *input.SceneInput)
case input.Query != nil:
return manager.GetInstance().ScraperCache.ScrapeSceneQuery(*source.ScraperID, *input.Query)
default:
err = errors.New("scene_id, scene_input or query must be set")
}
if err != nil {
return nil, err
}
if singleScene != nil {
return []*models.ScrapedScene{singleScene}, nil
}
return nil, nil
} else if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if err != nil {
return nil, err
}
if input.SceneID != nil {
return client.FindStashBoxScenesByFingerprintsFlat([]string{*input.SceneID})
} else if input.Query != nil {
return client.QueryStashBoxScene(ctx, *input.Query)
}
return nil, errors.New("scene_id or query must be set")
}
return nil, errors.New("scraper_id or stash_box_index must be set")
}
func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeMultiScenesInput) ([][]*models.ScrapedScene, error) {
if source.ScraperID != nil {
return nil, ErrNotImplemented
} else if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if err != nil {
return nil, err
}
return client.FindStashBoxScenesByFingerprints(input.SceneIds)
}
return nil, errors.New("scraper_id or stash_box_index must be set")
}
func (r *queryResolver) ScrapeSinglePerformer(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeSinglePerformerInput) ([]*models.ScrapedPerformer, error) {
if source.ScraperID != nil {
if input.PerformerInput != nil {
singlePerformer, err := manager.GetInstance().ScraperCache.ScrapePerformer(*source.ScraperID, *input.PerformerInput)
if err != nil {
return nil, err
}
if singlePerformer != nil {
return []*models.ScrapedPerformer{singlePerformer}, nil
}
return nil, nil
}
if input.Query != nil {
return manager.GetInstance().ScraperCache.ScrapePerformerList(*source.ScraperID, *input.Query)
}
return nil, ErrNotImplemented
} else if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if err != nil {
return nil, err
}
var ret []*models.StashBoxPerformerQueryResult
switch {
case input.PerformerID != nil:
ret, err = client.FindStashBoxPerformersByNames([]string{*input.PerformerID})
case input.Query != nil:
ret, err = client.QueryStashBoxPerformer(*input.Query)
default:
return nil, ErrNotImplemented
}
if err != nil {
return nil, err
}
if len(ret) > 0 {
return ret[0].Results, nil
}
return nil, nil
}
return nil, errors.New("scraper_id or stash_box_index must be set")
}
func (r *queryResolver) ScrapeMultiPerformers(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeMultiPerformersInput) ([][]*models.ScrapedPerformer, error) {
if source.ScraperID != nil {
return nil, ErrNotImplemented
} else if source.StashBoxIndex != nil {
client, err := r.getStashBoxClient(*source.StashBoxIndex)
if err != nil {
return nil, err
}
return client.FindStashBoxPerformersByPerformerNames(input.PerformerIds)
}
return nil, errors.New("scraper_id or stash_box_index must be set")
}
func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeSingleGalleryInput) ([]*models.ScrapedGallery, error) {
if source.ScraperID != nil {
var singleGallery *models.ScrapedGallery
var err error
switch {
case input.GalleryID != nil:
var galleryID int
galleryID, err = strconv.Atoi(*input.GalleryID)
if err != nil {
return nil, err
}
singleGallery, err = manager.GetInstance().ScraperCache.ScrapeGallery(*source.ScraperID, galleryID)
case input.GalleryInput != nil:
singleGallery, err = manager.GetInstance().ScraperCache.ScrapeGalleryFragment(*source.ScraperID, *input.GalleryInput)
default:
return nil, ErrNotImplemented
}
if err != nil {
return nil, err
}
if singleGallery != nil {
return []*models.ScrapedGallery{singleGallery}, nil
}
return nil, nil
} else if source.StashBoxIndex != nil {
return nil, ErrNotSupported
}
return nil, errors.New("scraper_id must be set")
}
func (r *queryResolver) ScrapeSingleMovie(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeSingleMovieInput) ([]*models.ScrapedMovie, error) {
return nil, ErrNotSupported
}

View File

@@ -2,32 +2,12 @@ package api
import (
"context"
"time"
"github.com/stashapp/stash/pkg/job"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
)
type throttledUpdate struct {
id int
pendingUpdate *job.Job
lastUpdate time.Time
broadcastTimer *time.Timer
killTimer *time.Timer
}
func (tu *throttledUpdate) broadcast(output chan *models.JobStatusUpdate) {
tu.lastUpdate = time.Now()
output <- &models.JobStatusUpdate{
Type: models.JobStatusUpdateTypeUpdate,
Job: jobToJobModel(*tu.pendingUpdate),
}
tu.broadcastTimer = nil
tu.pendingUpdate = nil
}
func makeJobStatusUpdate(t models.JobStatusUpdateType, j job.Job) *models.JobStatusUpdate {
return &models.JobStatusUpdate{
Type: t,

View File

@@ -8,20 +8,22 @@ import (
)
func getLogLevel(logType string) models.LogLevel {
if logType == "progress" {
switch logType {
case "progress":
return models.LogLevelProgress
} else if logType == "debug" {
case "trace":
return models.LogLevelTrace
case "debug":
return models.LogLevelDebug
} else if logType == "info" {
case "info":
return models.LogLevelInfo
} else if logType == "warn" {
case "warn":
return models.LogLevelWarning
} else if logType == "error" {
case "error":
return models.LogLevelError
default:
return models.LogLevelDebug
}
// default to debug
return models.LogLevelDebug
}
func logEntriesFromLogItems(logItems []logger.LogItem) []*models.LogEntry {

View File

@@ -7,6 +7,7 @@ import (
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
@@ -32,15 +33,35 @@ func (rs imageRoutes) Routes() chi.Router {
// region Handlers
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
image := r.Context().Value(imageKey).(*models.Image)
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(image.Checksum, models.DefaultGthumbWidth)
img := r.Context().Value(imageKey).(*models.Image)
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
// if the thumbnail doesn't exist, fall back to the original file
w.Header().Add("Cache-Control", "max-age=604800000")
// if the thumbnail doesn't exist, encode on the fly
exists, _ := utils.FileExists(filepath)
if exists {
http.ServeFile(w, r, filepath)
} else {
rs.Image(w, r)
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMPEG)
data, err := encoder.GetThumbnail(img, models.DefaultGthumbWidth)
if err != nil {
logger.Errorf("error generating thumbnail for image: %s", err.Error())
// backwards compatibility - fallback to original image instead
rs.Image(w, r)
return
}
// write the generated thumbnail to disk if enabled
if manager.GetInstance().Config.IsWriteImageThumbnails() {
if err := utils.WriteFile(filepath, data); err != nil {
logger.Errorf("error writing thumbnail for image %s: %s", img.Path, err)
}
}
if n, err := w.Write(data); err != nil {
logger.Errorf("error writing thumbnail response. Wrote %v bytes: %v", n, err)
}
}
}
@@ -59,7 +80,7 @@ func ImageCtx(next http.Handler) http.Handler {
imageID, _ := strconv.Atoi(imageIdentifierQueryParam)
var image *models.Image
manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
readTxnErr := manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
qb := repo.Image()
if imageID == 0 {
image, _ = qb.FindByChecksum(imageIdentifierQueryParam)
@@ -69,6 +90,9 @@ func ImageCtx(next http.Handler) http.Handler {
return nil
})
if readTxnErr != nil {
logger.Warnf("read transaction failure while trying to read image by id: %v", readTxnErr)
}
if image == nil {
http.Error(w, http.StatusText(404), 404)

View File

@@ -6,6 +6,7 @@ import (
"strconv"
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
@@ -32,17 +33,22 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
image, _ = repo.Movie().GetFrontImage(movie.ID)
return nil
})
if err != nil {
logger.Warnf("read transaction error while getting front image: %v", err)
}
}
if len(image) == 0 {
_, image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
}
utils.ServeImage(image, w, r)
if err := utils.ServeImage(image, w, r); err != nil {
logger.Warnf("error serving front image: %v", err)
}
}
func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
@@ -50,17 +56,22 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
defaultParam := r.URL.Query().Get("default")
var image []byte
if defaultParam != "true" {
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
image, _ = repo.Movie().GetBackImage(movie.ID)
return nil
})
if err != nil {
logger.Warnf("read transaction error on fetch back image: %v", err)
}
}
if len(image) == 0 {
_, image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
}
utils.ServeImage(image, w, r)
if err := utils.ServeImage(image, w, r); err != nil {
logger.Warnf("error while serving image: %v", err)
}
}
func MovieCtx(next http.Handler) http.Handler {

View File

@@ -6,6 +6,7 @@ import (
"strconv"
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
@@ -33,17 +34,22 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
var image []byte
if defaultParam != "true" {
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
readTxnErr := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
image, _ = repo.Performer().GetImage(performer.ID)
return nil
})
if readTxnErr != nil {
logger.Warnf("couldn't execute getting a performer image from read transaction: %v", readTxnErr)
}
}
if len(image) == 0 || defaultParam == "true" {
image, _ = getRandomPerformerImageUsingName(performer.Name.String, performer.Gender.String, config.GetInstance().GetCustomPerformerImageLocation())
}
utils.ServeImage(image, w, r)
if err := utils.ServeImage(image, w, r); err != nil {
logger.Warnf("error serving image: %v", err)
}
}
func PerformerCtx(next http.Handler) http.Handler {

View File

@@ -16,8 +16,7 @@ import (
)
type sceneRoutes struct {
txnManager models.TransactionManager
sceneServer manager.SceneServer
txnManager models.TransactionManager
}
func (rs sceneRoutes) Routes() chi.Router {
@@ -42,6 +41,7 @@ func (rs sceneRoutes) Routes() chi.Router {
r.Get("/scene_marker/{sceneMarkerId}/stream", rs.SceneMarkerStream)
r.Get("/scene_marker/{sceneMarkerId}/preview", rs.SceneMarkerPreview)
r.Get("/scene_marker/{sceneMarkerId}/screenshot", rs.SceneMarkerScreenshot)
})
r.With(SceneCtx).Get("/{sceneId}_thumbs.vtt", rs.VttThumbs)
r.With(SceneCtx).Get("/{sceneId}_sprite.jpg", rs.VttSprite)
@@ -57,9 +57,10 @@ func getSceneFileContainer(scene *models.Scene) ffmpeg.Container {
container = ffmpeg.Container(scene.Format.String)
} else { // container isn't in the DB
// shouldn't happen, fallback to ffprobe
tmpVideoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
ffprobe := manager.GetInstance().FFProbe
tmpVideoFile, err := ffprobe.NewVideoFile(scene.Path, false)
if err != nil {
logger.Errorf("[transcode] error reading video file: %s", err.Error())
logger.Errorf("[transcode] error reading video file: %v", err)
return ffmpeg.Container("")
}
@@ -85,7 +86,9 @@ func (rs sceneRoutes) StreamMKV(w http.ResponseWriter, r *http.Request) {
container := getSceneFileContainer(scene)
if container != ffmpeg.Matroska {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("not an mkv file"))
if _, err := w.Write([]byte("not an mkv file")); err != nil {
logger.Warnf("[stream] error writing to stream: %v", err)
}
return
}
@@ -103,9 +106,10 @@ func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) {
func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
ffprobe := manager.GetInstance().FFProbe
videoFile, err := ffprobe.NewVideoFile(scene.Path, false)
if err != nil {
logger.Errorf("[stream] error reading video file: %s", err.Error())
logger.Errorf("[stream] error reading video file: %v", err)
return
}
@@ -126,7 +130,9 @@ func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
rangeStr := requestByteRange.ToHeaderValue(int64(str.Len()))
w.Header().Set("Content-Range", rangeStr)
w.Write(ret)
if n, err := w.Write(ret); err != nil {
logger.Warnf("[stream] error writing stream (wrote %v bytes): %v", n, err)
}
}
func (rs sceneRoutes) StreamTS(w http.ResponseWriter, r *http.Request) {
@@ -138,15 +144,18 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
scene := r.Context().Value(sceneKey).(*models.Scene)
// needs to be transcoded
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path, false)
ffprobe := manager.GetInstance().FFProbe
videoFile, err := ffprobe.NewVideoFile(scene.Path, false)
if err != nil {
logger.Errorf("[stream] error reading video file: %s", err.Error())
logger.Errorf("[stream] error reading video file: %v", err)
return
}
// start stream based on query param, if provided
r.ParseForm()
if err = r.ParseForm(); err != nil {
logger.Warnf("[stream] error parsing query form: %v", err)
}
startTime := r.Form.Get("start")
requestedSize := r.Form.Get("resolution")
@@ -164,13 +173,15 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
options.MaxTranscodeSize = models.StreamingResolutionEnum(requestedSize)
}
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
encoder := manager.GetInstance().FFMPEG
stream, err = encoder.GetTranscodeStream(options)
if err != nil {
logger.Errorf("[stream] error transcoding video file: %s", err.Error())
logger.Errorf("[stream] error transcoding video file: %v", err)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
if _, err := w.Write([]byte(err.Error())); err != nil {
logger.Warnf("[stream] error writing response: %v", err)
}
return
}
@@ -289,6 +300,12 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request)
http.Error(w, http.StatusText(500), 500)
return
}
if sceneMarker == nil {
http.Error(w, http.StatusText(404), 404)
return
}
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
http.ServeFile(w, r, filepath)
}
@@ -306,6 +323,12 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
http.Error(w, http.StatusText(500), 500)
return
}
if sceneMarker == nil {
http.Error(w, http.StatusText(404), 404)
return
}
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamPreviewImagePath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
// If the image doesn't exist, send the placeholder
@@ -320,6 +343,39 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneMarkerID, _ := strconv.Atoi(chi.URLParam(r, "sceneMarkerId"))
var sceneMarker *models.SceneMarker
if err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
var err error
sceneMarker, err = repo.SceneMarker().Find(sceneMarkerID)
return err
}); err != nil {
logger.Warnf("Error when getting scene marker for stream: %s", err.Error())
http.Error(w, http.StatusText(500), 500)
return
}
if sceneMarker == nil {
http.Error(w, http.StatusText(404), 404)
return
}
filepath := manager.GetInstance().Paths.SceneMarkers.GetStreamScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()), int(sceneMarker.Seconds))
// If the image doesn't exist, send the placeholder
exists, _ := utils.FileExists(filepath)
if !exists {
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(utils.PendingGenerateResource)
return
}
http.ServeFile(w, r, filepath)
}
// endregion
func SceneCtx(next http.Handler) http.Handler {
@@ -328,7 +384,7 @@ func SceneCtx(next http.Handler) http.Handler {
sceneID, _ := strconv.Atoi(sceneIdentifierQueryParam)
var scene *models.Scene
manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
readTxnErr := manager.GetInstance().TxnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
qb := repo.Scene()
if sceneID == 0 {
// determine checksum/os by the length of the query param
@@ -343,6 +399,9 @@ func SceneCtx(next http.Handler) http.Handler {
return nil
})
if readTxnErr != nil {
logger.Warnf("error executing SceneCtx transaction: %v", readTxnErr)
}
if scene == nil {
http.Error(w, http.StatusText(404), 404)

View File

@@ -2,10 +2,13 @@ package api
import (
"context"
"errors"
"net/http"
"strconv"
"syscall"
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
@@ -32,17 +35,27 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
var image []byte
if defaultParam != "true" {
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
image, _ = repo.Studio().GetImage(studio.ID)
return nil
})
if err != nil {
logger.Warnf("read transaction error while fetching studio image: %v", err)
}
}
if len(image) == 0 {
_, image, _ = utils.ProcessBase64Image(models.DefaultStudioImage)
}
utils.ServeImage(image, w, r)
if err := utils.ServeImage(image, w, r); err != nil {
// Broken pipe errors are common when serving images and the remote
// connection closes the connection. Filter them out of the error
// messages, as they are benign.
if !errors.Is(err, syscall.EPIPE) {
logger.Warnf("cannot serve studio image: %v", err)
}
}
}
func StudioCtx(next http.Handler) http.Handler {

View File

@@ -6,6 +6,7 @@ import (
"strconv"
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
@@ -32,17 +33,22 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) {
var image []byte
if defaultParam != "true" {
rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
err := rs.txnManager.WithReadTxn(r.Context(), func(repo models.ReaderRepository) error {
image, _ = repo.Tag().GetImage(tag.ID)
return nil
})
if err != nil {
logger.Warnf("read transaction error while getting tag image: %v", err)
}
}
if len(image) == 0 {
image = models.DefaultTagImage
}
utils.ServeImage(image, w, r)
if err := utils.ServeImage(image, w, r); err != nil {
logger.Warnf("error serving tag image: %v", err)
}
}
func TagCtx(next http.Handler) http.Handler {

View File

@@ -3,11 +3,12 @@ package api
import (
"context"
"crypto/tls"
"embed"
"errors"
"fmt"
"io/ioutil"
"io/fs"
"net/http"
"net/url"
"os"
"path"
"runtime/debug"
"strconv"
@@ -21,100 +22,22 @@ import (
gqlPlayground "github.com/99designs/gqlgen/graphql/playground"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/gobuffalo/packr/v2"
"github.com/gorilla/websocket"
"github.com/pkg/browser"
"github.com/rs/cors"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/session"
"github.com/stashapp/stash/pkg/utils"
)
var version string
var buildstamp string
var githash string
var officialBuild string
var uiBox *packr.Box
//var legacyUiBox *packr.Box
var loginUIBox *packr.Box
func allowUnauthenticated(r *http.Request) bool {
return strings.HasPrefix(r.URL.Path, "/login") || r.URL.Path == "/css"
}
func authenticateHandler() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, err := manager.GetInstance().SessionStore.Authenticate(w, r)
if err != nil {
if err != session.ErrUnauthorized {
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write([]byte(err.Error()))
if err != nil {
logger.Error(err)
}
return
}
// unauthorized error
w.Header().Add("WWW-Authenticate", `FormBased`)
w.WriteHeader(http.StatusUnauthorized)
return
}
c := config.GetInstance()
ctx := r.Context()
// handle redirect if no user and user is required
if userID == "" && c.HasCredentials() && !allowUnauthenticated(r) {
// if we don't have a userID, then redirect
// if graphql was requested, we just return a forbidden error
if r.URL.Path == "/graphql" {
w.Header().Add("WWW-Authenticate", `FormBased`)
w.WriteHeader(http.StatusUnauthorized)
return
}
// otherwise redirect to the login page
u := url.URL{
Path: "/login",
}
q := u.Query()
q.Set(returnURLParam, r.URL.Path)
u.RawQuery = q.Encode()
http.Redirect(w, r, u.String(), http.StatusFound)
return
}
ctx = session.SetCurrentUserID(ctx, userID)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
func visitedPluginHandler() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// get the visited plugins and set them in the context
next.ServeHTTP(w, r)
})
}
}
const loginEndPoint = "/login"
func Start() {
uiBox = packr.New("UI Box", "../../ui/v2.5/build")
//legacyUiBox = packr.New("UI Box", "../../ui/v1/dist/stash-frontend")
loginUIBox = packr.New("Login UI Box", "../../ui/login")
func Start(uiBox embed.FS, loginUIBox embed.FS) {
initialiseImages()
r := chi.NewRouter()
@@ -182,10 +105,10 @@ func Start() {
r.HandleFunc("/playground", gqlPlayground.Handler("GraphQL playground", "/graphql"))
// session handlers
r.Post(loginEndPoint, handleLogin)
r.Get("/logout", handleLogout)
r.Post(loginEndPoint, handleLogin(loginUIBox))
r.Get("/logout", handleLogout(loginUIBox))
r.Get(loginEndPoint, getLoginHandler)
r.Get(loginEndPoint, getLoginHandler(loginUIBox))
r.Mount("/performer", performerRoutes{
txnManager: txnManager,
@@ -226,11 +149,18 @@ func Start() {
r.HandleFunc("/login*", func(w http.ResponseWriter, r *http.Request) {
ext := path.Ext(r.URL.Path)
if ext == ".html" || ext == "" {
data, _ := loginUIBox.Find("login.html")
_, _ = w.Write(data)
prefix := getProxyPrefix(r.Header)
data := getLoginPage(loginUIBox)
baseURLIndex := strings.Replace(string(data), "%BASE_URL%", prefix+"/", 2)
_, _ = w.Write([]byte(baseURLIndex))
} else {
r.URL.Path = strings.Replace(r.URL.Path, loginEndPoint, "", 1)
http.FileServer(loginUIBox).ServeHTTP(w, r)
loginRoot, err := fs.Sub(loginUIBox, loginRootDir)
if err != nil {
panic(err)
}
http.FileServer(http.FS(loginRoot)).ServeHTTP(w, r)
}
})
@@ -255,6 +185,8 @@ func Start() {
// Serve the web app
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
const uiRootDir = "ui/v2.5/build"
ext := path.Ext(r.URL.Path)
if customUILocation != "" {
@@ -267,14 +199,25 @@ func Start() {
}
if ext == ".html" || ext == "" {
data, _ := uiBox.Find("index.html")
_, _ = w.Write(data)
data, err := uiBox.ReadFile(uiRootDir + "/index.html")
if err != nil {
panic(err)
}
prefix := getProxyPrefix(r.Header)
baseURLIndex := strings.Replace(string(data), "%BASE_URL%", prefix+"/", 2)
baseURLIndex = strings.Replace(baseURLIndex, "base href=\"/\"", fmt.Sprintf("base href=\"%s\"", prefix+"/"), 2)
_, _ = w.Write([]byte(baseURLIndex))
} else {
isStatic, _ := path.Match("/static/*/*", r.URL.Path)
if isStatic {
w.Header().Add("Cache-Control", "max-age=604800000")
}
http.FileServer(uiBox).ServeHTTP(w, r)
uiRoot, err := fs.Sub(uiBox, uiRootDir)
if err != nil {
panic(err)
}
http.FileServer(http.FS(uiRoot)).ServeHTTP(w, r)
}
})
@@ -288,7 +231,7 @@ func Start() {
tlsConfig, err := makeTLSConfig(c)
if err != nil {
// assume we don't want to start with a broken TLS configuration
panic(fmt.Errorf("error loading TLS config: %s", err.Error()))
panic(fmt.Errorf("error loading TLS config: %v", err))
}
server := &http.Server{
@@ -299,14 +242,28 @@ func Start() {
go func() {
printVersion()
printLatestVersion()
printLatestVersion(context.TODO())
logger.Infof("stash is listening on " + address)
if tlsConfig != nil {
displayAddress = "https://" + displayAddress + "/"
} else {
displayAddress = "http://" + displayAddress + "/"
}
// This can be done before actually starting the server, as modern browsers will
// automatically reload the page if a local port is closed at page load and then opened.
if !c.GetNoBrowser() && manager.GetInstance().IsDesktop() {
err = browser.OpenURL(displayAddress)
if err != nil {
logger.Error("Could not open browser: " + err.Error())
}
}
if tlsConfig != nil {
logger.Infof("stash is running at https://" + displayAddress + "/")
logger.Infof("stash is running at " + displayAddress)
logger.Error(server.ListenAndServeTLS("", ""))
} else {
logger.Infof("stash is running at http://" + displayAddress + "/")
logger.Infof("stash is running at " + displayAddress)
logger.Error(server.ListenAndServe())
}
}()
@@ -314,12 +271,21 @@ func Start() {
func printVersion() {
versionString := githash
if IsOfficialBuild() {
versionString += " - Official Build"
} else {
versionString += " - Unofficial Build"
}
if version != "" {
versionString = version + " (" + versionString + ")"
}
fmt.Printf("stash version: %s - %s\n", versionString, buildstamp)
}
func IsOfficialBuild() bool {
return officialBuild == "true"
}
func GetVersion() (string, string, string) {
return version, githash, buildstamp
}
@@ -342,12 +308,12 @@ func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
return nil, errors.New("SSL key file must be present if certificate file is present")
}
cert, err := ioutil.ReadFile(certFile)
cert, err := os.ReadFile(certFile)
if err != nil {
return nil, fmt.Errorf("error reading SSL certificate file %s: %s", certFile, err.Error())
}
key, err := ioutil.ReadFile(keyFile)
key, err := os.ReadFile(keyFile)
if err != nil {
return nil, fmt.Errorf("error reading SSL key file %s: %s", keyFile, err.Error())
}
@@ -355,7 +321,7 @@ func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
certs := make([]tls.Certificate, 1)
certs[0], err = tls.X509KeyPair(cert, key)
if err != nil {
return nil, fmt.Errorf("error parsing key pair: %s", err.Error())
return nil, fmt.Errorf("error parsing key pair: %v", err)
}
tlsConfig := &tls.Config{
Certificates: certs,
@@ -382,11 +348,19 @@ func BaseURLMiddleware(next http.Handler) http.Handler {
} else {
scheme = "http"
}
baseURL := scheme + "://" + r.Host
prefix := getProxyPrefix(r.Header)
port := ""
forwardedPort := r.Header.Get("X-Forwarded-Port")
if forwardedPort != "" && forwardedPort != "80" && forwardedPort != "8080" && forwardedPort != "443" && !strings.Contains(r.Host, ":") {
port = ":" + forwardedPort
}
baseURL := scheme + "://" + r.Host + port + prefix
externalHost := config.GetInstance().GetExternalHost()
if externalHost != "" {
baseURL = externalHost
baseURL = externalHost + prefix
}
r = r.WithContext(context.WithValue(ctx, BaseURLCtxKey, baseURL))
@@ -395,3 +369,12 @@ func BaseURLMiddleware(next http.Handler) http.Handler {
}
return http.HandlerFunc(fn)
}
func getProxyPrefix(headers http.Header) string {
prefix := ""
if headers.Get("X-Forwarded-Prefix") != "" {
prefix = strings.TrimRight(headers.Get("X-Forwarded-Prefix"), "/")
}
return prefix
}

View File

@@ -1,6 +1,8 @@
package api
import (
"embed"
"errors"
"fmt"
"html/template"
"net/http"
@@ -10,20 +12,24 @@ import (
"github.com/stashapp/stash/pkg/session"
)
const cookieName = "session"
const usernameFormKey = "username"
const passwordFormKey = "password"
const userIDKey = "userID"
const loginRootDir = "ui/login"
const returnURLParam = "returnURL"
func getLoginPage(loginUIBox embed.FS) []byte {
data, err := loginUIBox.ReadFile(loginRootDir + "/login.html")
if err != nil {
panic(err)
}
return data
}
type loginTemplateData struct {
URL string
Error string
}
func redirectToLogin(w http.ResponseWriter, returnURL string, loginError string) {
data, _ := loginUIBox.Find("login.html")
func redirectToLogin(loginUIBox embed.FS, w http.ResponseWriter, returnURL string, loginError string) {
data := getLoginPage(loginUIBox)
templ, err := template.New("Login").Parse(string(data))
if err != nil {
http.Error(w, fmt.Sprintf("error: %s", err), http.StatusInternalServerError)
@@ -36,42 +42,48 @@ func redirectToLogin(w http.ResponseWriter, returnURL string, loginError string)
}
}
func getLoginHandler(w http.ResponseWriter, r *http.Request) {
if !config.GetInstance().HasCredentials() {
http.Redirect(w, r, "/", http.StatusFound)
return
}
func getLoginHandler(loginUIBox embed.FS) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !config.GetInstance().HasCredentials() {
http.Redirect(w, r, "/", http.StatusFound)
return
}
redirectToLogin(w, r.URL.Query().Get(returnURLParam), "")
redirectToLogin(loginUIBox, w, r.URL.Query().Get(returnURLParam), "")
}
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
url := r.FormValue(returnURLParam)
if url == "" {
url = "/"
}
func handleLogin(loginUIBox embed.FS) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
url := r.FormValue(returnURLParam)
if url == "" {
url = "/"
}
err := manager.GetInstance().SessionStore.Login(w, r)
if err == session.ErrInvalidCredentials {
// redirect back to the login page with an error
redirectToLogin(w, url, "Username or password is invalid")
return
}
err := manager.GetInstance().SessionStore.Login(w, r)
if errors.Is(err, session.ErrInvalidCredentials) {
// redirect back to the login page with an error
redirectToLogin(loginUIBox, w, url, "Username or password is invalid")
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, url, http.StatusFound)
http.Redirect(w, r, url, http.StatusFound)
}
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
if err := manager.GetInstance().SessionStore.Logout(w, r); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
func handleLogout(loginUIBox embed.FS) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := manager.GetInstance().SessionStore.Logout(w, r); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// redirect to the login page if credentials are required
getLoginHandler(w, r)
// redirect to the login page if credentials are required
getLoginHandler(loginUIBox)(w, r)
}
}

View File

@@ -59,6 +59,10 @@ func (b SceneURLBuilder) GetSceneMarkerStreamPreviewURL(sceneMarkerID int) strin
return b.BaseURL + "/scene/" + b.SceneID + "/scene_marker/" + strconv.Itoa(sceneMarkerID) + "/preview"
}
func (b SceneURLBuilder) GetSceneMarkerStreamScreenshotURL(sceneMarkerID int) string {
return b.BaseURL + "/scene/" + b.SceneID + "/scene_marker/" + strconv.Itoa(sceneMarkerID) + "/screenshot"
}
func (b SceneURLBuilder) GetFunscriptURL() string {
return b.BaseURL + "/scene/" + b.SceneID + "/funscript"
}

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