mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add plugin tasks (#651)
This commit is contained in:
9
pkg/plugin/examples/README.md
Normal file
9
pkg/plugin/examples/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Building
|
||||
|
||||
From the base stash source directory:
|
||||
```
|
||||
go build -tags=plugin_example -o plugin_goraw.exe ./pkg/plugin/examples/goraw/...
|
||||
go build -tags=plugin_example -o plugin_gorpc.exe ./pkg/plugin/examples/gorpc/...
|
||||
```
|
||||
|
||||
Place the resulting binaries together with the yml files in the `plugins` subdirectory of your stash directory.
|
||||
231
pkg/plugin/examples/common/graphql.go
Normal file
231
pkg/plugin/examples/common/graphql.go
Normal file
@@ -0,0 +1,231 @@
|
||||
// +build plugin_example
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/shurcooL/graphql"
|
||||
"github.com/stashapp/stash/pkg/plugin/common/log"
|
||||
)
|
||||
|
||||
const tagName = "Hawwwwt"
|
||||
|
||||
// graphql inputs and returns
|
||||
type TagCreate struct {
|
||||
ID graphql.ID `graphql:"id"`
|
||||
}
|
||||
|
||||
type TagCreateInput struct {
|
||||
Name graphql.String `graphql:"name" json:"name"`
|
||||
}
|
||||
|
||||
type TagDestroyInput struct {
|
||||
ID graphql.ID `graphql:"id" json:"id"`
|
||||
}
|
||||
|
||||
type FindScenesResultType struct {
|
||||
Count graphql.Int
|
||||
Scenes []Scene
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
ID graphql.ID `graphql:"id"`
|
||||
Name graphql.String `graphql:"name"`
|
||||
}
|
||||
|
||||
type Scene struct {
|
||||
ID graphql.ID
|
||||
Tags []Tag
|
||||
}
|
||||
|
||||
func (s Scene) getTagIds() []graphql.ID {
|
||||
ret := []graphql.ID{}
|
||||
|
||||
for _, t := range s.Tags {
|
||||
ret = append(ret, t.ID)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type FindFilterType struct {
|
||||
PerPage *graphql.Int `graphql:"per_page" json:"per_page"`
|
||||
Sort *graphql.String `graphql:"sort" json:"sort"`
|
||||
}
|
||||
|
||||
type SceneUpdate struct {
|
||||
ID graphql.ID `graphql:"id"`
|
||||
}
|
||||
|
||||
type SceneUpdateInput struct {
|
||||
ID graphql.ID `graphql:"id" json:"id"`
|
||||
TagIds []graphql.ID `graphql:"tag_ids" json:"tag_ids"`
|
||||
}
|
||||
|
||||
func getTagID(client *graphql.Client, create bool) (*graphql.ID, error) {
|
||||
log.Info("Checking if tag exists already")
|
||||
|
||||
// see if tag exists already
|
||||
var q struct {
|
||||
AllTags []Tag `graphql:"allTags"`
|
||||
}
|
||||
|
||||
err := client.Query(context.Background(), &q, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error getting tags: %s\n", err.Error())
|
||||
}
|
||||
|
||||
for _, t := range q.AllTags {
|
||||
if t.Name == tagName {
|
||||
id := t.ID
|
||||
return &id, nil
|
||||
}
|
||||
}
|
||||
|
||||
if !create {
|
||||
log.Info("Not found and not creating")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// create the tag
|
||||
var m struct {
|
||||
TagCreate TagCreate `graphql:"tagCreate(input: $s)"`
|
||||
}
|
||||
|
||||
input := TagCreateInput{
|
||||
Name: tagName,
|
||||
}
|
||||
|
||||
vars := map[string]interface{}{
|
||||
"s": input,
|
||||
}
|
||||
|
||||
log.Info("Creating new tag")
|
||||
|
||||
err = client.Mutate(context.Background(), &m, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error mutating scene: %s\n", err.Error())
|
||||
}
|
||||
|
||||
return &m.TagCreate.ID, nil
|
||||
}
|
||||
|
||||
func findRandomScene(client *graphql.Client) (*Scene, error) {
|
||||
// get a random scene
|
||||
var q struct {
|
||||
FindScenes FindScenesResultType `graphql:"findScenes(filter: $c)"`
|
||||
}
|
||||
|
||||
pp := graphql.Int(1)
|
||||
sort := graphql.String("random")
|
||||
filterInput := &FindFilterType{
|
||||
PerPage: &pp,
|
||||
Sort: &sort,
|
||||
}
|
||||
|
||||
vars := map[string]interface{}{
|
||||
"c": filterInput,
|
||||
}
|
||||
|
||||
log.Info("Finding a random scene")
|
||||
err := client.Query(context.Background(), &q, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error getting random scene: %s\n", err.Error())
|
||||
}
|
||||
|
||||
if q.FindScenes.Count == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &q.FindScenes.Scenes[0], nil
|
||||
}
|
||||
|
||||
func addTagId(tagIds []graphql.ID, tagId graphql.ID) []graphql.ID {
|
||||
for _, t := range tagIds {
|
||||
if t == tagId {
|
||||
return tagIds
|
||||
}
|
||||
}
|
||||
|
||||
tagIds = append(tagIds, tagId)
|
||||
return tagIds
|
||||
}
|
||||
|
||||
func AddTag(client *graphql.Client) error {
|
||||
tagID, err := getTagID(client, true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scene, err := findRandomScene(client)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if scene == nil {
|
||||
return errors.New("no scenes to add tag to")
|
||||
}
|
||||
|
||||
var m struct {
|
||||
SceneUpdate SceneUpdate `graphql:"sceneUpdate(input: $s)"`
|
||||
}
|
||||
|
||||
input := SceneUpdateInput{
|
||||
ID: scene.ID,
|
||||
TagIds: scene.getTagIds(),
|
||||
}
|
||||
|
||||
input.TagIds = addTagId(input.TagIds, *tagID)
|
||||
|
||||
vars := map[string]interface{}{
|
||||
"s": input,
|
||||
}
|
||||
|
||||
log.Infof("Adding tag to scene %v", scene.ID)
|
||||
err = client.Mutate(context.Background(), &m, vars)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error mutating scene: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveTag(client *graphql.Client) error {
|
||||
tagID, err := getTagID(client, false)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tagID == nil {
|
||||
log.Info("Tag does not exist. Nothing to remove")
|
||||
return nil
|
||||
}
|
||||
|
||||
// destroy the tag
|
||||
var m struct {
|
||||
TagDestroy bool `graphql:"tagDestroy(input: $s)"`
|
||||
}
|
||||
|
||||
input := TagDestroyInput{
|
||||
ID: *tagID,
|
||||
}
|
||||
|
||||
vars := map[string]interface{}{
|
||||
"s": input,
|
||||
}
|
||||
|
||||
log.Info("Destroying tag")
|
||||
|
||||
err = client.Mutate(context.Background(), &m, vars)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error destroying tag: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
28
pkg/plugin/examples/goraw/goraw.yml
Normal file
28
pkg/plugin/examples/goraw/goraw.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
# example plugin config
|
||||
name: Hawwwwt Tagger (Raw edition)
|
||||
description: Ultimate Hawwwwt tagging utility (using raw interface).
|
||||
version: 1.0
|
||||
url: http://www.github.com/stashapp/stash
|
||||
exec:
|
||||
- plugin_goraw
|
||||
interface: raw
|
||||
tasks:
|
||||
- name: Add hawwwwt tag to random scene
|
||||
description: Creates a "Hawwwwt" tag if not present and adds to a random scene.
|
||||
defaultArgs:
|
||||
mode: add
|
||||
- name: Remove hawwwwt tag from system
|
||||
description: Removes the "Hawwwwt" tag from all scenes and deletes the tag.
|
||||
defaultArgs:
|
||||
mode: remove
|
||||
- name: Indefinite task
|
||||
description: Sleeps indefinitely - interruptable
|
||||
# we'll try command-line argument for this one
|
||||
execArgs:
|
||||
- indef
|
||||
- "{pluginDir}"
|
||||
- name: Long task
|
||||
description: Sleeps for 100 seconds - interruptable
|
||||
defaultArgs:
|
||||
mode: long
|
||||
|
||||
106
pkg/plugin/examples/goraw/main.go
Normal file
106
pkg/plugin/examples/goraw/main.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// +build plugin_example
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
exampleCommon "github.com/stashapp/stash/pkg/plugin/examples/common"
|
||||
|
||||
"github.com/stashapp/stash/pkg/plugin/common"
|
||||
"github.com/stashapp/stash/pkg/plugin/common/log"
|
||||
"github.com/stashapp/stash/pkg/plugin/util"
|
||||
)
|
||||
|
||||
// raw plugins may accept the plugin input from stdin, or they can elect
|
||||
// to ignore it entirely. In this case it optionally reads from the
|
||||
// command-line parameters.
|
||||
func main() {
|
||||
input := common.PluginInput{}
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
inData, _ := ioutil.ReadAll(os.Stdin)
|
||||
log.Debugf("Raw input: %s", string(inData))
|
||||
decodeErr := json.Unmarshal(inData, &input)
|
||||
|
||||
if decodeErr != nil {
|
||||
panic("missing mode argument")
|
||||
}
|
||||
} else {
|
||||
log.Debug("Using command line inputs")
|
||||
mode := os.Args[1]
|
||||
log.Debugf("Command line inputs: %v", os.Args[1:])
|
||||
input.Args = common.ArgsMap{
|
||||
"mode": mode,
|
||||
}
|
||||
|
||||
// just some hard-coded values
|
||||
input.ServerConnection = common.StashServerConnection{
|
||||
Scheme: "http",
|
||||
Port: 9999,
|
||||
}
|
||||
}
|
||||
|
||||
output := common.PluginOutput{}
|
||||
Run(input, &output)
|
||||
|
||||
out, _ := json.Marshal(output)
|
||||
os.Stdout.WriteString(string(out))
|
||||
}
|
||||
|
||||
func Run(input common.PluginInput, output *common.PluginOutput) error {
|
||||
modeArg := input.Args.String("mode")
|
||||
|
||||
var err error
|
||||
if modeArg == "" || modeArg == "add" {
|
||||
client := util.NewClient(input.ServerConnection)
|
||||
err = exampleCommon.AddTag(client)
|
||||
} else if modeArg == "remove" {
|
||||
client := util.NewClient(input.ServerConnection)
|
||||
err = exampleCommon.RemoveTag(client)
|
||||
} else if modeArg == "long" {
|
||||
err = doLongTask()
|
||||
} else if modeArg == "indef" {
|
||||
err = doIndefiniteTask()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
*output = common.PluginOutput{
|
||||
Error: &errStr,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
outputStr := "ok"
|
||||
*output = common.PluginOutput{
|
||||
Output: &outputStr,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func doLongTask() error {
|
||||
const total = 100
|
||||
upTo := 0
|
||||
|
||||
log.Info("Doing long task")
|
||||
for upTo < total {
|
||||
time.Sleep(time.Second)
|
||||
|
||||
log.Progress(float64(upTo) / float64(total))
|
||||
upTo++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func doIndefiniteTask() error {
|
||||
log.Warn("Sleeping indefinitely")
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
26
pkg/plugin/examples/gorpc/gorpc.yml
Normal file
26
pkg/plugin/examples/gorpc/gorpc.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
# example plugin config
|
||||
name: Hawwwwt Tagger
|
||||
description: Ultimate Hawwwwt tagging utility.
|
||||
version: 1.0
|
||||
url: http://www.github.com/stashapp/stash
|
||||
exec:
|
||||
- plugin_gorpc
|
||||
interface: rpc
|
||||
tasks:
|
||||
- name: Add hawwwwt tag to random scene
|
||||
description: Creates a "Hawwwwt" tag if not present and adds to a random scene.
|
||||
defaultArgs:
|
||||
mode: add
|
||||
- name: Remove hawwwwt tag from system
|
||||
description: Removes the "Hawwwwt" tag from all scenes and deletes the tag.
|
||||
defaultArgs:
|
||||
mode: remove
|
||||
- name: Indefinite task
|
||||
description: Sleeps indefinitely - interruptable
|
||||
defaultArgs:
|
||||
mode: indef
|
||||
- name: Long task
|
||||
description: Sleeps for 100 seconds - interruptable
|
||||
defaultArgs:
|
||||
mode: long
|
||||
|
||||
97
pkg/plugin/examples/gorpc/main.go
Normal file
97
pkg/plugin/examples/gorpc/main.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// +build plugin_example
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
exampleCommon "github.com/stashapp/stash/pkg/plugin/examples/common"
|
||||
|
||||
"github.com/stashapp/stash/pkg/plugin/common"
|
||||
"github.com/stashapp/stash/pkg/plugin/common/log"
|
||||
"github.com/stashapp/stash/pkg/plugin/util"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// serves the plugin, providing an object that satisfies the
|
||||
// common.RPCRunner interface
|
||||
err := common.ServePlugin(&api{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type api struct {
|
||||
stopping bool
|
||||
}
|
||||
|
||||
func (a *api) Stop(input struct{}, output *bool) error {
|
||||
log.Info("Stopping...")
|
||||
a.stopping = true
|
||||
*output = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run is the main work function of the plugin. It interprets the input and
|
||||
// acts accordingly.
|
||||
func (a *api) Run(input common.PluginInput, output *common.PluginOutput) error {
|
||||
modeArg := input.Args.String("mode")
|
||||
|
||||
var err error
|
||||
if modeArg == "" || modeArg == "add" {
|
||||
client := util.NewClient(input.ServerConnection)
|
||||
err = exampleCommon.AddTag(client)
|
||||
} else if modeArg == "remove" {
|
||||
client := util.NewClient(input.ServerConnection)
|
||||
err = exampleCommon.RemoveTag(client)
|
||||
} else if modeArg == "long" {
|
||||
err = a.doLongTask()
|
||||
} else if modeArg == "indef" {
|
||||
err = a.doIndefiniteTask()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
*output = common.PluginOutput{
|
||||
Error: &errStr,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
outputStr := "ok"
|
||||
*output = common.PluginOutput{
|
||||
Output: &outputStr,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *api) doLongTask() error {
|
||||
const total = 100
|
||||
upTo := 0
|
||||
|
||||
log.Info("Doing long task")
|
||||
for upTo < total {
|
||||
time.Sleep(time.Second)
|
||||
if a.stopping {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Progress(float64(upTo) / float64(total))
|
||||
upTo++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *api) doIndefiniteTask() error {
|
||||
log.Warn("Sleeping indefinitely")
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
if a.stopping {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user