From 8ed3c5f71d847e870ea5a5e915f98fccc416cba7 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Mon, 29 Jul 2019 13:42:00 +1000 Subject: [PATCH 01/48] Add seeking for live transcodes via video.js --- pkg/api/routes_scene.go | 21 +- pkg/ffmpeg/encoder_transcode.go | 13 +- ui/v2/package.json | 8 +- .../scenes/ScenePlayer/ScenePlayer.tsx | 110 +++++++++- ui/v2/src/index.scss | 5 + ui/v2/yarn.lock | 201 +++++++++++++++++- 6 files changed, 336 insertions(+), 22 deletions(-) diff --git a/pkg/api/routes_scene.go b/pkg/api/routes_scene.go index 965a6cb1a..ebc36b7fc 100644 --- a/pkg/api/routes_scene.go +++ b/pkg/api/routes_scene.go @@ -1,17 +1,18 @@ package api import ( - "io" "context" + "io" + "net/http" + "strconv" + "strings" + "github.com/go-chi/chi" + "github.com/stashapp/stash/pkg/ffmpeg" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/manager" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" - "github.com/stashapp/stash/pkg/ffmpeg" - "net/http" - "strconv" - "strings" ) type sceneRoutes struct{} @@ -41,7 +42,7 @@ func (rs sceneRoutes) Routes() chi.Router { func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) - + // detect if not a streamable file and try to transcode it instead filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.Checksum) @@ -58,10 +59,14 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) { logger.Errorf("[stream] error reading video file: %s", err.Error()) return } - + + // start stream based on query param, if provided + r.ParseForm() + startTime := r.Form.Get("start") + encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath) - stream, process, err := encoder.StreamTranscode(*videoFile) + stream, process, err := encoder.StreamTranscode(*videoFile, startTime) if err != nil { logger.Errorf("[stream] error transcoding video file: %s", err.Error()) return diff --git a/pkg/ffmpeg/encoder_transcode.go b/pkg/ffmpeg/encoder_transcode.go index d8942b36b..32f8d1cca 100644 --- a/pkg/ffmpeg/encoder_transcode.go +++ b/pkg/ffmpeg/encoder_transcode.go @@ -26,8 +26,14 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) { _, _ = e.run(probeResult, args) } -func (e *Encoder) StreamTranscode(probeResult VideoFile) (io.ReadCloser, *os.Process, error) { - args := []string{ +func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.ReadCloser, *os.Process, error) { + args := []string{} + + if startTime != "" { + args = append(args, "-ss", startTime) + } + + args = append(args, "-i", probeResult.Path, "-c:v", "libvpx-vp9", "-vf", "scale=iw:-2", @@ -37,6 +43,7 @@ func (e *Encoder) StreamTranscode(probeResult VideoFile) (io.ReadCloser, *os.Pro "-b:v", "0", "-f", "webm", "pipe:", - } + ) + return e.stream(probeResult, args) } diff --git a/ui/v2/package.json b/ui/v2/package.json index b2309d2e1..4e5c65601 100644 --- a/ui/v2/package.json +++ b/ui/v2/package.json @@ -12,6 +12,7 @@ "@types/react": "16.8.18", "@types/react-dom": "16.8.4", "@types/react-router-dom": "4.3.3", + "@types/video.js": "^7.2.11", "apollo-boost": "0.4.0", "axios": "0.18.0", "bulma": "0.7.5", @@ -30,7 +31,8 @@ "react-photo-gallery": "7.0.2", "react-router-dom": "5.0.0", "react-scripts": "3.0.1", - "react-use": "9.1.2" + "react-use": "9.1.2", + "video.js": "^7.6.0" }, "scripts": { "start": "react-scripts start", @@ -53,12 +55,12 @@ "devDependencies": { "graphql-code-generator": "0.18.2", "graphql-codegen-add": "0.18.2", + "graphql-codegen-time": "0.18.2", "graphql-codegen-typescript-client": "0.18.2", "graphql-codegen-typescript-common": "0.18.2", "graphql-codegen-typescript-react-apollo": "0.18.2", - "graphql-codegen-time": "0.18.2", "tslint": "5.16.0", "tslint-react": "4.0.0", "typescript": "3.4.5" } -} \ No newline at end of file +} diff --git a/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx b/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx index e1ce9a4b6..f05a3537a 100644 --- a/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx @@ -4,15 +4,95 @@ import ReactJWPlayer from "react-jw-player"; import * as GQL from "../../../core/generated-graphql"; import { SceneHelpers } from "../helpers"; import { ScenePlayerScrubber } from "./ScenePlayerScrubber"; +import videojs from "video.js"; +import "video.js/dist/video-js.css"; interface IScenePlayerProps { scene: GQL.SceneDataFragment; timestamp: number; + onReady?: any; + onSeeked?: any; + onTime?: any; } interface IScenePlayerState { scrubberPosition: number; } +export class VideoJSPlayer extends React.Component { + private player: any; + private videoNode: any; + + constructor(props: IScenePlayerProps) { + super(props); + } + + componentDidMount() { + this.player = videojs(this.videoNode); + + this.player.src(this.props.scene.paths.stream); + + // hack duration + this.player.duration = () => { return this.props.scene.file.duration; }; + this.player.start = 0; + this.player.oldCurrentTime = this.player.currentTime; + this.player.currentTime = (time: any) => { + if( time == undefined ) + { + return this.player.oldCurrentTime() + this.player.start; + } + this.player.start = time; + this.player.oldCurrentTime(0); + this.player.src(this.props.scene.paths.stream + "?start=" + time); + this.player.play(); + + return this; + }; + + this.player.ready(() => { + // dirty hack - make this player look like JWPlayer + this.player.seek = this.player.currentTime; + this.player.getPosition = this.player.currentTime; + + // hook it into the window function + (window as any).jwplayer = () => { + return this.player; + } + + this.player.on("timeupdate", () => { + this.props.onTime(); + }); + + this.player.on("seeked", () => { + this.props.onSeeked(); + }); + + this.props.onReady(); + }); + } + + componentWillUnmount() { + if (this.player) { + this.player.dispose(); + } + } + + render() { + return ( +
+
+ +
+
+ ); + } +} + @HotkeysTarget export class ScenePlayer extends React.Component { private player: any; @@ -36,12 +116,11 @@ export class ScenePlayer extends React.Component -
- + ); + } else { + return ( + + + ) + } + } + + public render() { + return ( + <> +
+ {this.renderPlayer()} Date: Wed, 31 Jul 2019 01:42:46 +0300 Subject: [PATCH 02/48] Remove main-packr.go which breaks go test --- main-packr.go | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 main-packr.go diff --git a/main-packr.go b/main-packr.go deleted file mode 100644 index d09a2acf8..000000000 --- a/main-packr.go +++ /dev/null @@ -1,7 +0,0 @@ -// Code generated by github.com/gobuffalo/packr/v2. DO NOT EDIT. - -// You can use the "packr clean" command to clean up this, -// and any other packr generated files. -package main - -import _ "github.com/stashapp/stash/packrd" From 46c295778783873b30157215b29c0053bca729f1 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 1 Aug 2019 11:27:53 +1000 Subject: [PATCH 03/48] Fix viewing jwplayer after non-jwplayer video --- .../scenes/SceneDetails/SceneMarkersPanel.tsx | 2 +- .../scenes/ScenePlayer/ScenePlayer.tsx | 18 ++++++++---------- ui/v2/src/components/scenes/helpers.tsx | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx b/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx index 63f4e71b4..bf74b6281 100644 --- a/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx +++ b/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx @@ -40,7 +40,7 @@ export const SceneMarkersPanel: FunctionComponent = (pr const sceneMarkerUpdate = StashService.useSceneMarkerUpdate(); const sceneMarkerDestroy = StashService.useSceneMarkerDestroy(); - const jwplayer = SceneHelpers.getJWPlayer(); + const jwplayer = SceneHelpers.getPlayer(); function onOpenEditor(marker: GQL.SceneMarkerDataFragment | null = null) { setIsEditorOpen(true); diff --git a/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx b/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx index f05a3537a..1b33408d5 100644 --- a/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx @@ -29,6 +29,12 @@ export class VideoJSPlayer extends React.Component { componentDidMount() { this.player = videojs(this.videoNode); + // dirty hack - make this player look like JWPlayer + this.player.seek = this.player.currentTime; + this.player.getPosition = this.player.currentTime; + + SceneHelpers.registerJSPlayer(this.player); + this.player.src(this.props.scene.paths.stream); // hack duration @@ -49,15 +55,6 @@ export class VideoJSPlayer extends React.Component { }; this.player.ready(() => { - // dirty hack - make this player look like JWPlayer - this.player.seek = this.player.currentTime; - this.player.getPosition = this.player.currentTime; - - // hook it into the window function - (window as any).jwplayer = () => { - return this.player; - } - this.player.on("timeupdate", () => { this.props.onTime(); }); @@ -73,6 +70,7 @@ export class VideoJSPlayer extends React.Component { componentWillUnmount() { if (this.player) { this.player.dispose(); + SceneHelpers.deregisterJSPlayer(); } } @@ -225,7 +223,7 @@ export class ScenePlayer extends React.Component 0) { this.player.seek(this.props.timestamp); } diff --git a/ui/v2/src/components/scenes/helpers.tsx b/ui/v2/src/components/scenes/helpers.tsx index 6749be137..3d7b0e3af 100644 --- a/ui/v2/src/components/scenes/helpers.tsx +++ b/ui/v2/src/components/scenes/helpers.tsx @@ -3,9 +3,12 @@ import { } from "@blueprintjs/core"; import React, { } from "react"; import { Link } from "react-router-dom"; +import videojs from "video.js"; import * as GQL from "../../core/generated-graphql"; export class SceneHelpers { + private static videoJSPlayer: videojs.Player | null; + public static maybeRenderStudio( scene: GQL.SceneDataFragment | GQL.SlimSceneDataFragment, height: number, @@ -33,8 +36,21 @@ export class SceneHelpers { ); } + public static registerJSPlayer(player : videojs.Player) { + this.videoJSPlayer = player; + } + + public static deregisterJSPlayer() { + this.videoJSPlayer = null; + } + public static getJWPlayerId(): string { return "main-jwplayer"; } - public static getJWPlayer(): any { + public static getPlayer(): any { + // return videoJSPlayer if it is set, otherwise use jwplayer() + if (this.videoJSPlayer) { + return this.videoJSPlayer; + } + return (window as any).jwplayer("main-jwplayer"); } } From aeef01a64c8482ff50372828d2e20b1c31009e5d Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 1 Aug 2019 11:36:29 +1000 Subject: [PATCH 04/48] Add row-based multithreading for live transcodes --- pkg/ffmpeg/encoder_transcode.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/ffmpeg/encoder_transcode.go b/pkg/ffmpeg/encoder_transcode.go index 32f8d1cca..5f336d439 100644 --- a/pkg/ffmpeg/encoder_transcode.go +++ b/pkg/ffmpeg/encoder_transcode.go @@ -39,6 +39,7 @@ func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.R "-vf", "scale=iw:-2", "-deadline", "realtime", "-cpu-used", "5", + "-row-mt", "1", "-crf", "30", "-b:v", "0", "-f", "webm", From 19525dafecad23b9083d1aae6a67e6d0748892e4 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 2 Aug 2019 15:57:11 +1000 Subject: [PATCH 05/48] Update doublestar to fix #38 --- go.mod | 2 +- go.sum | 2 + .../github.com/bmatcuk/doublestar/.gitignore | 3 + .../github.com/bmatcuk/doublestar/.travis.yml | 6 +- .../bmatcuk/doublestar/doublestar.go | 97 +++++++++++-------- vendor/github.com/bmatcuk/doublestar/go.mod | 2 + vendor/modules.txt | 2 +- 7 files changed, 70 insertions(+), 44 deletions(-) diff --git a/go.mod b/go.mod index e865c77a8..ff733f97b 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/stashapp/stash require ( github.com/99designs/gqlgen v0.9.0 github.com/PuerkitoBio/goquery v1.5.0 - github.com/bmatcuk/doublestar v1.1.1 + github.com/bmatcuk/doublestar v1.1.5 github.com/disintegration/imaging v1.6.0 github.com/fsnotify/fsnotify v1.4.7 github.com/go-chi/chi v4.0.2+incompatible diff --git a/go.sum b/go.sum index f24b92937..ddb5a70ee 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= diff --git a/vendor/github.com/bmatcuk/doublestar/.gitignore b/vendor/github.com/bmatcuk/doublestar/.gitignore index 76d92ba4b..af212ecc2 100644 --- a/vendor/github.com/bmatcuk/doublestar/.gitignore +++ b/vendor/github.com/bmatcuk/doublestar/.gitignore @@ -27,3 +27,6 @@ _testmain.go *.exe *.test *.prof + +# test directory +test/ diff --git a/vendor/github.com/bmatcuk/doublestar/.travis.yml b/vendor/github.com/bmatcuk/doublestar/.travis.yml index cf3c884ad..ec4fee889 100644 --- a/vendor/github.com/bmatcuk/doublestar/.travis.yml +++ b/vendor/github.com/bmatcuk/doublestar/.travis.yml @@ -1,10 +1,8 @@ language: go go: - - 1.3 - - 1.4 - - 1.5 - - 1.6 + - 1.11 + - 1.12 before_install: - go get -t -v ./... diff --git a/vendor/github.com/bmatcuk/doublestar/doublestar.go b/vendor/github.com/bmatcuk/doublestar/doublestar.go index ceab4e35b..0044dfa83 100644 --- a/vendor/github.com/bmatcuk/doublestar/doublestar.go +++ b/vendor/github.com/bmatcuk/doublestar/doublestar.go @@ -9,35 +9,49 @@ import ( "unicode/utf8" ) +// ErrBadPattern indicates a pattern was malformed. var ErrBadPattern = path.ErrBadPattern // Split a path on the given separator, respecting escaping. -func splitPathOnSeparator(path string, separator rune) []string { - // if the separator is '\\', then we can just split... +func splitPathOnSeparator(path string, separator rune) (ret []string) { + idx := 0 if separator == '\\' { - return strings.Split(path, string(separator)) + // if the separator is '\\', then we can just split... + ret = strings.Split(path, string(separator)) + idx = len(ret) + } else { + // otherwise, we need to be careful of situations where the separator was escaped + cnt := strings.Count(path, string(separator)) + if cnt == 0 { + return []string{path} + } + + ret = make([]string, cnt+1) + pathlen := len(path) + separatorLen := utf8.RuneLen(separator) + emptyEnd := false + for start := 0; start < pathlen; { + end := indexRuneWithEscaping(path[start:], separator) + if end == -1 { + emptyEnd = false + end = pathlen + } else { + emptyEnd = true + end += start + } + ret[idx] = path[start:end] + start = end + separatorLen + idx++ + } + + // If the last rune is a path separator, we need to append an empty string to + // represent the last, empty path component. By default, the strings from + // make([]string, ...) will be empty, so we just need to icrement the count + if emptyEnd { + idx++ + } } - // otherwise, we need to be careful of situations where the separator was escaped - cnt := strings.Count(path, string(separator)) - if cnt == 0 { - return []string{path} - } - ret := make([]string, cnt+1) - pathlen := len(path) - separatorLen := utf8.RuneLen(separator) - idx := 0 - for start := 0; start < pathlen; { - end := indexRuneWithEscaping(path[start:], separator) - if end == -1 { - end = pathlen - } else { - end += start - } - ret[idx] = path[start:end] - start = end + separatorLen - idx++ - } return ret[:idx] } @@ -65,8 +79,8 @@ func indexRuneWithEscaping(s string, r rune) int { // { term } // term: // '*' matches any sequence of non-path-separators -// '**' matches any sequence of characters, including -// path separators. +// '**' matches any sequence of characters, including +// path separators. // '?' matches any single non-path-separator character // '[' [ '^' ] { character-range } ']' // character class (must be non-empty) @@ -160,13 +174,14 @@ func doMatching(patternComponents, nameComponents []string) (matched bool, err e } } return false, nil - } else { - // try matching components - matched, err = matchComponent(patternComponents[patIdx], nameComponents[nameIdx]) - if !matched || err != nil { - return - } } + + // try matching components + matched, err = matchComponent(patternComponents[patIdx], nameComponents[nameIdx]) + if !matched || err != nil { + return + } + patIdx++ nameIdx++ } @@ -194,14 +209,20 @@ func Glob(pattern string) (matches []string, err error) { return nil, nil } - // On Windows systems, this will return the drive name ('C:'), on others, - // it will return an empty string. + // On Windows systems, this will return the drive name ('C:') for filesystem + // paths, or \\\ for UNC paths. On other systems, it will + // return an empty string. Since absolute paths on non-Windows systems start + // with a slash, patternComponent[0] == volumeName will return true for both + // absolute Windows paths and absolute non-Windows paths, but we need a + // separate check for UNC paths. volumeName := filepath.VolumeName(pattern) - - // If the first pattern component is equal to the volume name, then the - // pattern is an absolute path. - if patternComponents[0] == volumeName { - return doGlob(fmt.Sprintf("%s%s", volumeName, string(os.PathSeparator)), patternComponents[1:], matches) + isWindowsUNC := strings.HasPrefix(pattern, `\\`) + if isWindowsUNC || patternComponents[0] == volumeName { + startComponentIndex := 1 + if isWindowsUNC { + startComponentIndex = 4 + } + return doGlob(fmt.Sprintf("%s%s", volumeName, string(os.PathSeparator)), patternComponents[startComponentIndex:], matches) } // otherwise, it's a relative pattern diff --git a/vendor/github.com/bmatcuk/doublestar/go.mod b/vendor/github.com/bmatcuk/doublestar/go.mod index 1d0378b15..ce1688f73 100644 --- a/vendor/github.com/bmatcuk/doublestar/go.mod +++ b/vendor/github.com/bmatcuk/doublestar/go.mod @@ -1 +1,3 @@ module github.com/bmatcuk/doublestar + +go 1.12 diff --git a/vendor/modules.txt b/vendor/modules.txt index baac2df3e..ca3ec59b7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -11,7 +11,7 @@ github.com/PuerkitoBio/goquery github.com/agnivade/levenshtein # github.com/andybalholm/cascadia v1.0.0 github.com/andybalholm/cascadia -# github.com/bmatcuk/doublestar v1.1.1 +# github.com/bmatcuk/doublestar v1.1.5 github.com/bmatcuk/doublestar # github.com/disintegration/imaging v1.6.0 github.com/disintegration/imaging From b6bea6d30dccaaaf5fa93e9bb60e805f667e7636 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Sun, 28 Jul 2019 13:19:54 +1000 Subject: [PATCH 06/48] Fix build target for Windows. Add UI target --- Makefile | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 08914d55c..b3c2e12e1 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,10 @@ +ifeq ($(OS),Windows_NT) + SEPARATOR := && + SET := set +endif + build: - CGO_ENABLED=1 packr2 build -mod=vendor -v + $(SET) CGO_ENABLED=1 $(SEPARATOR) packr2 build -mod=vendor -v install: packr2 install @@ -26,3 +31,7 @@ vet: .PHONY: lint lint: revive -config revive.toml -exclude ./vendor/... ./... + +.PHONY: ui +ui: + cd ui/v2 && yarn build From 1ddbc8fa01d9c999a717d364a71f1011bc46778f Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Sun, 28 Jul 2019 13:54:27 +1000 Subject: [PATCH 07/48] Simplify fmt and vet targets --- Makefile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index b3c2e12e1..36fec732e 100644 --- a/Makefile +++ b/Makefile @@ -20,13 +20,12 @@ gqlgen: # Runs gofmt -w on the project's source code, modifying any files that do not match its style. .PHONY: fmt fmt: - go list ./... | grep -v vendor | xargs go fmt + go fmt ./... # Runs go vet on the project's source code. -# https://stackoverflow.com/questions/40531874/how-to-make-go-linters-ignore-vendor .PHONY: vet vet: - go list ./... | grep -v vendor | xargs go vet + go vet ./... .PHONY: lint lint: From 44e7ed17728ca61ed06ec0050d78f2c3142e7dfe Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 1 Aug 2019 11:46:33 +1000 Subject: [PATCH 08/48] Run gqlgen for UI in gqlgen target --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 36fec732e..ef6a0ca91 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ clean: .PHONY: gqlgen gqlgen: go run scripts/gqlgen.go + cd ui/v2 && yarn run gqlgen # Runs gofmt -w on the project's source code, modifying any files that do not match its style. .PHONY: fmt From 0f17af5902cb254f38f9f29520b89f4911a45b53 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Sat, 3 Aug 2019 19:02:43 +1000 Subject: [PATCH 09/48] Allow tag creation within multiselect --- .../components/select/FilterMultiSelect.tsx | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/ui/v2/src/components/select/FilterMultiSelect.tsx b/ui/v2/src/components/select/FilterMultiSelect.tsx index c876d3bef..99434568b 100644 --- a/ui/v2/src/components/select/FilterMultiSelect.tsx +++ b/ui/v2/src/components/select/FilterMultiSelect.tsx @@ -5,6 +5,8 @@ import { IMultiSelectProps, ItemPredicate, ItemRenderer, MultiSelect } from "@bl import * as GQL from "../../core/generated-graphql"; import { StashService } from "../../core/StashService"; import { HTMLInputProps } from "../../models"; +import { ErrorUtils } from "../../utils/errors"; +import { ToastUtils } from "../../utils/toasts"; const InternalPerformerMultiSelect = MultiSelect.ofType(); const InternalTagMultiSelect = MultiSelect.ofType(); @@ -24,6 +26,56 @@ interface IProps extends HTMLInputProps, Partial> export const FilterMultiSelect: React.FunctionComponent = (props: IProps) => { let items: ValidTypes[]; let InternalMultiSelect: new (props: IMultiSelectProps) => MultiSelect; + var createNewFunc = undefined; + + const [newTagName, setNewTagName] = React.useState(""); + const createTag = StashService.useTagCreate(getTagInput() as GQL.TagCreateInput); + + function getTagInput() { + const tagInput: Partial = { name: newTagName }; + return tagInput; + } + + async function onCreateNewObject(item: ValidTypes) { + var created : any; + if (props.type === "tags") { + try { + created = await createTag(); + + addSelectedItem(created.data.tagCreate); + + ToastUtils.success("Created tag"); + } catch (e) { + ErrorUtils.handle(e); + } + } + } + + function createNewTag(query : string) { + setNewTagName(query); + return { + name : query + }; + } + + function createNewRenderer(query: string, active: boolean, handleClick: React.MouseEventHandler) { + // if tag already exists with that name, then don't return anything + if (items.find((item) => { + return item.name === query; + })) { + return undefined; + } + + return ( + + ); + } + switch (props.type) { case "performers": { const { data } = StashService.useAllPerformersForFilter(); @@ -41,6 +93,7 @@ export const FilterMultiSelect: React.FunctionComponent = (props: IProps const { data } = StashService.useAllTagsForFilter(); items = !!data && !!data.allTags ? data.allTags : []; InternalMultiSelect = InternalTagMultiSelect; + createNewFunc = createNewTag; break; } default: { @@ -80,12 +133,21 @@ export const FilterMultiSelect: React.FunctionComponent = (props: IProps return item.name!.toLowerCase().indexOf(query.toLowerCase()) >= 0; }; - function onItemSelect(item: ValidTypes) { + function addSelectedItem(item: ValidTypes) { selectedItems.push(item); setSelectedItems(selectedItems); props.onUpdate(selectedItems); } + function onItemSelect(item: ValidTypes) { + if (item.id === undefined) { + // create the new item, if applicable + onCreateNewObject(item); + } else { + addSelectedItem(item); + } + } + function onItemRemove(value: string, index: number) { const newSelectedItems = selectedItems.filter((_, i) => i !== index); setSelectedItems(newSelectedItems); @@ -103,6 +165,8 @@ export const FilterMultiSelect: React.FunctionComponent = (props: IProps onItemSelect={onItemSelect} resetOnSelect={true} popoverProps={{position: "bottom"}} + createNewItemFromQuery={createNewFunc} + createNewItemRenderer={createNewRenderer} {...props} /> ); From 051e50b386edebd08f637f1a3013da113d3ba8a0 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 8 Aug 2019 18:31:10 +1000 Subject: [PATCH 10/48] Add path to scene sort by options --- ui/v2/src/models/list-filter/filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/v2/src/models/list-filter/filter.ts b/ui/v2/src/models/list-filter/filter.ts index 07b6021df..093df15d5 100644 --- a/ui/v2/src/models/list-filter/filter.ts +++ b/ui/v2/src/models/list-filter/filter.ts @@ -51,7 +51,7 @@ export class ListFilterModel { switch (filterMode) { case FilterMode.Scenes: if (!!this.sortBy === false) { this.sortBy = "date"; } - this.sortByOptions = ["title", "rating", "date", "filesize", "duration", "framerate", "bitrate", "random"]; + this.sortByOptions = ["title", "path", "rating", "date", "filesize", "duration", "framerate", "bitrate", "random"]; this.displayModeOptions = [ DisplayMode.Grid, DisplayMode.List, From 1fdb00fa0e768cd14e62dd42ac6e860be80f8e58 Mon Sep 17 00:00:00 2001 From: bill <48220860+bnkai@users.noreply.github.com> Date: Tue, 13 Aug 2019 16:41:56 +0300 Subject: [PATCH 11/48] Don't add duplicate scenes,galleries to the DB --- pkg/manager/task_scan.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pkg/manager/task_scan.go b/pkg/manager/task_scan.go index 19a016e97..dfdc879ab 100644 --- a/pkg/manager/task_scan.go +++ b/pkg/manager/task_scan.go @@ -46,9 +46,15 @@ func (t *ScanTask) scanGallery() { tx := database.DB.MustBeginTx(ctx, nil) gallery, _ = qb.FindByChecksum(checksum, tx) if gallery != nil { - logger.Infof("%s already exists. Updating path...", t.FilePath) - gallery.Path = t.FilePath - _, err = qb.Update(*gallery, tx) + exists, _ := utils.FileExists(t.FilePath) + if exists { + logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, gallery.Path) + } else { + + logger.Infof("%s already exists. Updating path...", t.FilePath) + gallery.Path = t.FilePath + _, err = qb.Update(*gallery, tx) + } } else { logger.Infof("%s doesn't exist. Creating new item...", t.FilePath) currentTime := time.Now() @@ -95,9 +101,14 @@ func (t *ScanTask) scanScene() { ctx := context.TODO() tx := database.DB.MustBeginTx(ctx, nil) if scene != nil { - logger.Infof("%s already exists. Updating path...", t.FilePath) - scene.Path = t.FilePath - _, err = qb.Update(*scene, tx) + exists, _ := utils.FileExists(t.FilePath) + if exists { + logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, scene.Path) + } else { + logger.Infof("%s already exists. Updating path...", t.FilePath) + scene.Path = t.FilePath + _, err = qb.Update(*scene, tx) + } } else { logger.Infof("%s doesn't exist. Creating new item...", t.FilePath) currentTime := time.Now() From 35bcf97a7af64ebc29f3c700868e94b6a4b649ec Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 15 Aug 2019 07:40:51 +1000 Subject: [PATCH 12/48] Add delete for performers and studios --- graphql/documents/mutations/performer.graphql | 4 + graphql/documents/mutations/studio.graphql | 4 + graphql/schema/schema.graphql | 2 + graphql/schema/types/performer.graphql | 4 + graphql/schema/types/studio.graphql | 4 + pkg/api/resolver_mutation_performer.go | 14 ++ pkg/api/resolver_mutation_studio.go | 18 +- pkg/models/generated_exec.go | 188 ++++++++++++++++++ pkg/models/generated_models.go | 8 + pkg/models/querybuilder_performer.go | 10 + pkg/models/querybuilder_studio.go | 17 ++ .../components/Shared/DetailsEditNavbar.tsx | 7 + .../Studios/StudioDetails/Studio.tsx | 15 ++ .../performers/PerformerDetails/Performer.tsx | 15 ++ ui/v2/src/core/StashService.ts | 6 + ui/v2/src/core/generated-graphql.tsx | 62 +++++- 16 files changed, 375 insertions(+), 3 deletions(-) diff --git a/graphql/documents/mutations/performer.graphql b/graphql/documents/mutations/performer.graphql index d1a8489dd..a74a3a5f7 100644 --- a/graphql/documents/mutations/performer.graphql +++ b/graphql/documents/mutations/performer.graphql @@ -82,4 +82,8 @@ mutation PerformerUpdate( }) { ...PerformerData } +} + +mutation PerformerDestroy($id: ID!) { + performerDestroy(input: { id: $id }) } \ No newline at end of file diff --git a/graphql/documents/mutations/studio.graphql b/graphql/documents/mutations/studio.graphql index b9ad5cded..d0032c8e2 100644 --- a/graphql/documents/mutations/studio.graphql +++ b/graphql/documents/mutations/studio.graphql @@ -17,4 +17,8 @@ mutation StudioUpdate( studioUpdate(input: { id: $id, name: $name, url: $url, image: $image }) { ...StudioData } +} + +mutation StudioDestroy($id: ID!) { + studioDestroy(input: { id: $id }) } \ No newline at end of file diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 862da0847..e756cd285 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -79,9 +79,11 @@ type Mutation { performerCreate(input: PerformerCreateInput!): Performer performerUpdate(input: PerformerUpdateInput!): Performer + performerDestroy(input: PerformerDestroyInput!): Boolean! studioCreate(input: StudioCreateInput!): Studio studioUpdate(input: StudioUpdateInput!): Studio + studioDestroy(input: StudioDestroyInput!): Boolean! tagCreate(input: TagCreateInput!): Tag tagUpdate(input: TagUpdateInput!): Tag diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index 38f8de897..af52b704c 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -66,6 +66,10 @@ input PerformerUpdateInput { image: String } +input PerformerDestroyInput { + id: ID! +} + type FindPerformersResultType { count: Int! performers: [Performer!]! diff --git a/graphql/schema/types/studio.graphql b/graphql/schema/types/studio.graphql index 294ea53ed..bda41b914 100644 --- a/graphql/schema/types/studio.graphql +++ b/graphql/schema/types/studio.graphql @@ -23,6 +23,10 @@ input StudioUpdateInput { image: String } +input StudioDestroyInput { + id: ID! +} + type FindStudiosResultType { count: Int! studios: [Studio!]! diff --git a/pkg/api/resolver_mutation_performer.go b/pkg/api/resolver_mutation_performer.go index 37222b2d3..54a7693e4 100644 --- a/pkg/api/resolver_mutation_performer.go +++ b/pkg/api/resolver_mutation_performer.go @@ -175,3 +175,17 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per return performer, nil } + +func (r *mutationResolver) PerformerDestroy(ctx context.Context, input models.PerformerDestroyInput) (bool, error) { + qb := models.NewPerformerQueryBuilder() + tx := database.DB.MustBeginTx(ctx, nil) + if err := qb.Destroy(input.ID, tx); err != nil { + _ = tx.Rollback() + return false, err + } + if err := tx.Commit(); err != nil { + return false, err + } + return true, nil +} + diff --git a/pkg/api/resolver_mutation_studio.go b/pkg/api/resolver_mutation_studio.go index 52745eb0b..c79622448 100644 --- a/pkg/api/resolver_mutation_studio.go +++ b/pkg/api/resolver_mutation_studio.go @@ -3,11 +3,12 @@ package api import ( "context" "database/sql" + "strconv" + "time" + "github.com/stashapp/stash/pkg/database" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" - "strconv" - "time" ) func (r *mutationResolver) StudioCreate(ctx context.Context, input models.StudioCreateInput) (*models.Studio, error) { @@ -85,3 +86,16 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio return studio, nil } + +func (r *mutationResolver) StudioDestroy(ctx context.Context, input models.StudioDestroyInput) (bool, error) { + qb := models.NewStudioQueryBuilder() + tx := database.DB.MustBeginTx(ctx, nil) + if err := qb.Destroy(input.ID, tx); err != nil { + _ = tx.Rollback() + return false, err + } + if err := tx.Commit(); err != nil { + return false, err + } + return true, nil +} diff --git a/pkg/models/generated_exec.go b/pkg/models/generated_exec.go index 3964dd1c9..48627edc4 100644 --- a/pkg/models/generated_exec.go +++ b/pkg/models/generated_exec.go @@ -108,12 +108,14 @@ type ComplexityRoot struct { Mutation struct { ConfigureGeneral func(childComplexity int, input ConfigGeneralInput) int PerformerCreate func(childComplexity int, input PerformerCreateInput) int + PerformerDestroy func(childComplexity int, input PerformerDestroyInput) int PerformerUpdate func(childComplexity int, input PerformerUpdateInput) int SceneMarkerCreate func(childComplexity int, input SceneMarkerCreateInput) int SceneMarkerDestroy func(childComplexity int, id string) int SceneMarkerUpdate func(childComplexity int, input SceneMarkerUpdateInput) int SceneUpdate func(childComplexity int, input SceneUpdateInput) int StudioCreate func(childComplexity int, input StudioCreateInput) int + StudioDestroy func(childComplexity int, input StudioDestroyInput) int StudioUpdate func(childComplexity int, input StudioUpdateInput) int TagCreate func(childComplexity int, input TagCreateInput) int TagDestroy func(childComplexity int, input TagDestroyInput) int @@ -288,8 +290,10 @@ type MutationResolver interface { SceneMarkerDestroy(ctx context.Context, id string) (bool, error) PerformerCreate(ctx context.Context, input PerformerCreateInput) (*Performer, error) PerformerUpdate(ctx context.Context, input PerformerUpdateInput) (*Performer, error) + PerformerDestroy(ctx context.Context, input PerformerDestroyInput) (bool, error) StudioCreate(ctx context.Context, input StudioCreateInput) (*Studio, error) StudioUpdate(ctx context.Context, input StudioUpdateInput) (*Studio, error) + StudioDestroy(ctx context.Context, input StudioDestroyInput) (bool, error) TagCreate(ctx context.Context, input TagCreateInput) (*Tag, error) TagUpdate(ctx context.Context, input TagUpdateInput) (*Tag, error) TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error) @@ -598,6 +602,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.PerformerCreate(childComplexity, args["input"].(PerformerCreateInput)), true + case "Mutation.performerDestroy": + if e.complexity.Mutation.PerformerDestroy == nil { + break + } + + args, err := ec.field_Mutation_performerDestroy_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.PerformerDestroy(childComplexity, args["input"].(PerformerDestroyInput)), true + case "Mutation.performerUpdate": if e.complexity.Mutation.PerformerUpdate == nil { break @@ -670,6 +686,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.StudioCreate(childComplexity, args["input"].(StudioCreateInput)), true + case "Mutation.studioDestroy": + if e.complexity.Mutation.StudioDestroy == nil { + break + } + + args, err := ec.field_Mutation_studioDestroy_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.StudioDestroy(childComplexity, args["input"].(StudioDestroyInput)), true + case "Mutation.studioUpdate": if e.complexity.Mutation.StudioUpdate == nil { break @@ -1840,9 +1868,11 @@ type Mutation { performerCreate(input: PerformerCreateInput!): Performer performerUpdate(input: PerformerUpdateInput!): Performer + performerDestroy(input: PerformerDestroyInput!): Boolean! studioCreate(input: StudioCreateInput!): Studio studioUpdate(input: StudioUpdateInput!): Studio + studioDestroy(input: StudioDestroyInput!): Boolean! tagCreate(input: TagCreateInput!): Tag tagUpdate(input: TagUpdateInput!): Tag @@ -2054,6 +2084,10 @@ input PerformerUpdateInput { image: String } +input PerformerDestroyInput { + id: ID! +} + type FindPerformersResultType { count: Int! performers: [Performer!]! @@ -2212,6 +2246,10 @@ input StudioUpdateInput { image: String } +input StudioDestroyInput { + id: ID! +} + type FindStudiosResultType { count: Int! studios: [Studio!]! @@ -2270,6 +2308,20 @@ func (ec *executionContext) field_Mutation_performerCreate_args(ctx context.Cont return args, nil } +func (ec *executionContext) field_Mutation_performerDestroy_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 PerformerDestroyInput + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalNPerformerDestroyInput2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐPerformerDestroyInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_performerUpdate_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -2354,6 +2406,20 @@ func (ec *executionContext) field_Mutation_studioCreate_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation_studioDestroy_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 StudioDestroyInput + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalNStudioDestroyInput2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐStudioDestroyInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_studioUpdate_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3625,6 +3691,40 @@ func (ec *executionContext) _Mutation_performerUpdate(ctx context.Context, field return ec.marshalOPerformer2ᚖgithubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐPerformer(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_performerDestroy(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_performerDestroy_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().PerformerDestroy(rctx, args["input"].(PerformerDestroyInput)) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_studioCreate(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() @@ -3687,6 +3787,40 @@ func (ec *executionContext) _Mutation_studioUpdate(ctx context.Context, field gr return ec.marshalOStudio2ᚖgithubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐStudio(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_studioDestroy(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_studioDestroy_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().StudioDestroy(rctx, args["input"].(StudioDestroyInput)) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_tagCreate(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() @@ -8131,6 +8265,24 @@ func (ec *executionContext) unmarshalInputPerformerCreateInput(ctx context.Conte return it, nil } +func (ec *executionContext) unmarshalInputPerformerDestroyInput(ctx context.Context, v interface{}) (PerformerDestroyInput, error) { + var it PerformerDestroyInput + var asMap = v.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "id": + var err error + it.ID, err = ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputPerformerFilterType(ctx context.Context, v interface{}) (PerformerFilterType, error) { var it PerformerFilterType var asMap = v.(map[string]interface{}) @@ -8557,6 +8709,24 @@ func (ec *executionContext) unmarshalInputStudioCreateInput(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputStudioDestroyInput(ctx context.Context, v interface{}) (StudioDestroyInput, error) { + var it StudioDestroyInput + var asMap = v.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "id": + var err error + it.ID, err = ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputStudioUpdateInput(ctx context.Context, v interface{}) (StudioUpdateInput, error) { var it StudioUpdateInput var asMap = v.(map[string]interface{}) @@ -9045,10 +9215,20 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec._Mutation_performerCreate(ctx, field) case "performerUpdate": out.Values[i] = ec._Mutation_performerUpdate(ctx, field) + case "performerDestroy": + out.Values[i] = ec._Mutation_performerDestroy(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "studioCreate": out.Values[i] = ec._Mutation_studioCreate(ctx, field) case "studioUpdate": out.Values[i] = ec._Mutation_studioUpdate(ctx, field) + case "studioDestroy": + out.Values[i] = ec._Mutation_studioDestroy(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "tagCreate": out.Values[i] = ec._Mutation_tagCreate(ctx, field) case "tagUpdate": @@ -11010,6 +11190,10 @@ func (ec *executionContext) unmarshalNPerformerCreateInput2githubᚗcomᚋstasha return ec.unmarshalInputPerformerCreateInput(ctx, v) } +func (ec *executionContext) unmarshalNPerformerDestroyInput2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐPerformerDestroyInput(ctx context.Context, v interface{}) (PerformerDestroyInput, error) { + return ec.unmarshalInputPerformerDestroyInput(ctx, v) +} + func (ec *executionContext) unmarshalNPerformerUpdateInput2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐPerformerUpdateInput(ctx context.Context, v interface{}) (PerformerUpdateInput, error) { return ec.unmarshalInputPerformerUpdateInput(ctx, v) } @@ -11319,6 +11503,10 @@ func (ec *executionContext) unmarshalNStudioCreateInput2githubᚗcomᚋstashapp return ec.unmarshalInputStudioCreateInput(ctx, v) } +func (ec *executionContext) unmarshalNStudioDestroyInput2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐStudioDestroyInput(ctx context.Context, v interface{}) (StudioDestroyInput, error) { + return ec.unmarshalInputStudioDestroyInput(ctx, v) +} + func (ec *executionContext) unmarshalNStudioUpdateInput2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐStudioUpdateInput(ctx context.Context, v interface{}) (StudioUpdateInput, error) { return ec.unmarshalInputStudioUpdateInput(ctx, v) } diff --git a/pkg/models/generated_models.go b/pkg/models/generated_models.go index 4c1a780a8..351270d16 100644 --- a/pkg/models/generated_models.go +++ b/pkg/models/generated_models.go @@ -109,6 +109,10 @@ type PerformerCreateInput struct { Image string `json:"image"` } +type PerformerDestroyInput struct { + ID string `json:"id"` +} + type PerformerFilterType struct { // Filter by favorite FilterFavorites *bool `json:"filter_favorites"` @@ -254,6 +258,10 @@ type StudioCreateInput struct { Image string `json:"image"` } +type StudioDestroyInput struct { + ID string `json:"id"` +} + type StudioUpdateInput struct { ID string `json:"id"` Name *string `json:"name"` diff --git a/pkg/models/querybuilder_performer.go b/pkg/models/querybuilder_performer.go index ebbe8604b..d19965bb2 100644 --- a/pkg/models/querybuilder_performer.go +++ b/pkg/models/querybuilder_performer.go @@ -2,6 +2,7 @@ package models import ( "database/sql" + "github.com/jmoiron/sqlx" "github.com/stashapp/stash/pkg/database" ) @@ -54,6 +55,15 @@ func (qb *PerformerQueryBuilder) Update(updatedPerformer Performer, tx *sqlx.Tx) return &updatedPerformer, nil } +func (qb *PerformerQueryBuilder) Destroy(id string, tx *sqlx.Tx) error { + _, err := tx.Exec("DELETE FROM performers_scenes WHERE performer_id = ?", id) + if err != nil { + return err + } + + return executeDeleteQuery("performers", id, tx) +} + func (qb *PerformerQueryBuilder) Find(id int) (*Performer, error) { query := "SELECT * FROM performers WHERE id = ? LIMIT 1" args := []interface{}{id} diff --git a/pkg/models/querybuilder_studio.go b/pkg/models/querybuilder_studio.go index 10071775c..c7a46afb4 100644 --- a/pkg/models/querybuilder_studio.go +++ b/pkg/models/querybuilder_studio.go @@ -2,6 +2,7 @@ package models import ( "database/sql" + "github.com/jmoiron/sqlx" "github.com/stashapp/stash/pkg/database" ) @@ -50,6 +51,22 @@ func (qb *StudioQueryBuilder) Update(updatedStudio Studio, tx *sqlx.Tx) (*Studio return &updatedStudio, nil } +func (qb *StudioQueryBuilder) Destroy(id string, tx *sqlx.Tx) error { + // remove studio from scenes + _, err := tx.Exec("UPDATE scenes SET studio_id = null WHERE studio_id = ?", id) + if err != nil { + return err + } + + // remove studio from scraped items + _, err = tx.Exec("UPDATE scraped_items SET studio_id = null WHERE studio_id = ?", id) + if err != nil { + return err + } + + return executeDeleteQuery("studios", id, tx) +} + func (qb *StudioQueryBuilder) Find(id int, tx *sqlx.Tx) (*Studio, error) { query := "SELECT * FROM studios WHERE id = ? LIMIT 1" args := []interface{}{id} diff --git a/ui/v2/src/components/Shared/DetailsEditNavbar.tsx b/ui/v2/src/components/Shared/DetailsEditNavbar.tsx index 47555cd50..0e3eea5b8 100644 --- a/ui/v2/src/components/Shared/DetailsEditNavbar.tsx +++ b/ui/v2/src/components/Shared/DetailsEditNavbar.tsx @@ -20,6 +20,7 @@ interface IProps { isEditing: boolean; onToggleEdit: () => void; onSave: () => void; + onDelete: () => void; onImageChange: (event: React.FormEvent) => void; // TODO: only for performers. make generic @@ -43,6 +44,11 @@ export const DetailsEditNavbar: FunctionComponent = (props: IProps) => { return + ); }; diff --git a/ui/v2/src/core/StashService.ts b/ui/v2/src/core/StashService.ts index 13c83c0dd..5475f3b2b 100644 --- a/ui/v2/src/core/StashService.ts +++ b/ui/v2/src/core/StashService.ts @@ -162,6 +162,10 @@ export class StashService { return GQL.useConfigureGeneral({ variables: { input }, refetchQueries: ["Configuration"] }); } + public static useConfigureInterface(input: GQL.ConfigInterfaceInput) { + return GQL.useConfigureInterface({ variables: { input }, refetchQueries: ["Configuration"] }); + } + public static queryScrapeFreeones(performerName: string) { return StashService.client.query({ query: GQL.ScrapeFreeonesDocument, diff --git a/ui/v2/src/core/generated-graphql.tsx b/ui/v2/src/core/generated-graphql.tsx index d0239e0b2..64085ad90 100644 --- a/ui/v2/src/core/generated-graphql.tsx +++ b/ui/v2/src/core/generated-graphql.tsx @@ -1,6 +1,6 @@ /* tslint:disable */ /* eslint-disable */ -// Generated in 2019-08-14T07:29:27+10:00 +// Generated in 2019-08-23T07:28:41+10:00 export type Maybe = T | undefined; export interface SceneFilterType { @@ -237,6 +237,13 @@ export interface ConfigGeneralInput { generatedPath?: Maybe; } +export interface ConfigInterfaceInput { + /** Custom CSS */ + css?: Maybe; + + cssEnabled?: Maybe; +} + export enum CriterionModifier { Equals = "EQUALS", NotEquals = "NOT_EQUALS", @@ -277,6 +284,18 @@ export type ConfigureGeneralMutation = { export type ConfigureGeneralConfigureGeneral = ConfigGeneralDataFragment; +export type ConfigureInterfaceVariables = { + input: ConfigInterfaceInput; +}; + +export type ConfigureInterfaceMutation = { + __typename?: "Mutation"; + + configureInterface: ConfigureInterfaceConfigureInterface; +}; + +export type ConfigureInterfaceConfigureInterface = ConfigInterfaceDataFragment; + export type PerformerCreateVariables = { name?: Maybe; url?: Maybe; @@ -932,14 +951,26 @@ export type ConfigGeneralDataFragment = { generatedPath: string; }; +export type ConfigInterfaceDataFragment = { + __typename?: "ConfigInterfaceResult"; + + css: Maybe; + + cssEnabled: Maybe; +}; + export type ConfigDataFragment = { __typename?: "ConfigResult"; general: ConfigDataGeneral; + + interface: ConfigDataInterface; }; export type ConfigDataGeneral = ConfigGeneralDataFragment; +export type ConfigDataInterface = ConfigInterfaceDataFragment; + export type GalleryDataFragment = { __typename?: "Gallery"; @@ -1315,14 +1346,25 @@ export const ConfigGeneralDataFragmentDoc = gql` } `; +export const ConfigInterfaceDataFragmentDoc = gql` + fragment ConfigInterfaceData on ConfigInterfaceResult { + css + cssEnabled + } +`; + export const ConfigDataFragmentDoc = gql` fragment ConfigData on ConfigResult { general { ...ConfigGeneralData } + interface { + ...ConfigInterfaceData + } } ${ConfigGeneralDataFragmentDoc} + ${ConfigInterfaceDataFragmentDoc} `; export const SlimPerformerDataFragmentDoc = gql` @@ -1554,6 +1596,26 @@ export function useConfigureGeneral( ConfigureGeneralVariables >(ConfigureGeneralDocument, baseOptions); } +export const ConfigureInterfaceDocument = gql` + mutation ConfigureInterface($input: ConfigInterfaceInput!) { + configureInterface(input: $input) { + ...ConfigInterfaceData + } + } + + ${ConfigInterfaceDataFragmentDoc} +`; +export function useConfigureInterface( + baseOptions?: ReactApolloHooks.MutationHookOptions< + ConfigureInterfaceMutation, + ConfigureInterfaceVariables + > +) { + return ReactApolloHooks.useMutation< + ConfigureInterfaceMutation, + ConfigureInterfaceVariables + >(ConfigureInterfaceDocument, baseOptions); +} export const PerformerCreateDocument = gql` mutation PerformerCreate( $name: String diff --git a/ui/v2/src/index.tsx b/ui/v2/src/index.tsx index bde16b201..28d0db3ac 100755 --- a/ui/v2/src/index.tsx +++ b/ui/v2/src/index.tsx @@ -8,11 +8,14 @@ import "./index.scss"; import * as serviceWorker from "./serviceWorker"; ReactDOM.render(( + <> + + ), document.getElementById("root")); // If you want your app to work offline and load faster, you can change From 949117bb69af59bba06d0022c794e8012c8f2ddd Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 23 Aug 2019 13:17:48 +1000 Subject: [PATCH 19/48] Use screenshot as wall fallback image --- ui/v2/src/components/Wall/WallItem.tsx | 40 +++++++++++++++++--------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/ui/v2/src/components/Wall/WallItem.tsx b/ui/v2/src/components/Wall/WallItem.tsx index cc1a7fc5b..a4d800d73 100644 --- a/ui/v2/src/components/Wall/WallItem.tsx +++ b/ui/v2/src/components/Wall/WallItem.tsx @@ -1,5 +1,5 @@ import _ from "lodash"; -import React, { FunctionComponent, useRef, useState } from "react"; +import React, { FunctionComponent, useRef, useState, useEffect } from "react"; import { Link } from "react-router-dom"; import * as GQL from "../../core/generated-graphql"; import { useInterfaceLocalForage } from "../../hooks/LocalForage"; @@ -17,6 +17,10 @@ interface IWallItemProps { export const WallItem: FunctionComponent = (props: IWallItemProps) => { const [videoPath, setVideoPath] = useState(undefined); + const [previewPath, setPreviewPath] = useState(""); + const [screenshotPath, setScreenshotPath] = useState(""); + const [title, setTitle] = useState(""); + const [tags, setTags] = useState([]); const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: true}); const interfaceSettings = useInterfaceLocalForage(); const showTextContainer = !!interfaceSettings.data ? interfaceSettings.data.wall.textContainerEnabled : true; @@ -68,18 +72,25 @@ export const WallItem: FunctionComponent = (props: IWallItemProp } } - let previewSrc: string = ""; - let title: string = ""; - let tags: JSX.Element[] = []; - if (!!props.sceneMarker) { - previewSrc = props.sceneMarker.preview; - title = `${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`; - tags = props.sceneMarker.tags.map((tag) => ({tag.name})); - tags.unshift({props.sceneMarker.primary_tag.name}); - } else if (!!props.scene) { - previewSrc = props.scene.paths.webp || ""; - title = props.scene.title || ""; - // tags = props.scene.tags.map((tag) => ({tag.name})); + useEffect(() => { + if (!!props.sceneMarker) { + setPreviewPath(props.sceneMarker.preview); + setTitle(`${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`); + const thisTags = props.sceneMarker.tags.map((tag) => ({tag.name})); + thisTags.unshift({props.sceneMarker.primary_tag.name}); + setTags(thisTags); + } else if (!!props.scene) { + setPreviewPath(props.scene.paths.webp || ""); + setScreenshotPath(props.scene.paths.screenshot || ""); + setTitle(props.scene.title || ""); + // tags = props.scene.tags.map((tag) => ({tag.name})); + } + }, [props.sceneMarker, props.scene]); + + function previewNotFound() { + if (previewPath !== screenshotPath) { + setPreviewPath(screenshotPath); + } } const className = ["scene-wall-item-container"]; @@ -99,12 +110,13 @@ export const WallItem: FunctionComponent = (props: IWallItemProp onClick()} to={linkSrc}>