mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Example python plugin (#825)
* Add example python plugin * Fix log incorrectly detecting as progress level
This commit is contained in:
@@ -52,6 +52,7 @@ var (
|
|||||||
}
|
}
|
||||||
ProgressLevel = Level{
|
ProgressLevel = Level{
|
||||||
char: 'p',
|
char: 'p',
|
||||||
|
Name: "progress",
|
||||||
}
|
}
|
||||||
NoneLevel = Level{
|
NoneLevel = Level{
|
||||||
Name: "none",
|
Name: "none",
|
||||||
|
|||||||
44
pkg/plugin/examples/python/log.py
Normal file
44
pkg/plugin/examples/python/log.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
# Log messages sent from a plugin instance are transmitted via stderr and are
|
||||||
|
# encoded with a prefix consisting of special character SOH, then the log
|
||||||
|
# level (one of t, d, i, w, e, or p - corresponding to trace, debug, info,
|
||||||
|
# warning, error and progress levels respectively), then special character
|
||||||
|
# STX.
|
||||||
|
#
|
||||||
|
# The LogTrace, LogDebug, LogInfo, LogWarning, and LogError methods, and their equivalent
|
||||||
|
# formatted methods are intended for use by plugin instances to transmit log
|
||||||
|
# messages. The LogProgress method is also intended for sending progress data.
|
||||||
|
#
|
||||||
|
|
||||||
|
def __prefix(levelChar):
|
||||||
|
startLevelChar = b'\x01'
|
||||||
|
endLevelChar = b'\x02'
|
||||||
|
|
||||||
|
ret = startLevelChar + levelChar + endLevelChar
|
||||||
|
return ret.decode()
|
||||||
|
|
||||||
|
def __log(levelChar, s):
|
||||||
|
if levelChar == "":
|
||||||
|
return
|
||||||
|
|
||||||
|
print(__prefix(levelChar) + s + "\n", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
def LogTrace(s):
|
||||||
|
__log(b't', s)
|
||||||
|
|
||||||
|
def LogDebug(s):
|
||||||
|
__log(b'd', s)
|
||||||
|
|
||||||
|
def LogInfo(s):
|
||||||
|
__log(b'i', s)
|
||||||
|
|
||||||
|
def LogWarning(s):
|
||||||
|
__log(b'w', s)
|
||||||
|
|
||||||
|
def LogError(s):
|
||||||
|
__log(b'e', s)
|
||||||
|
|
||||||
|
def LogProgress(p):
|
||||||
|
progress = min(max(0, p), 1)
|
||||||
|
__log(b'p', str(progress))
|
||||||
123
pkg/plugin/examples/python/pyplugin.py
Normal file
123
pkg/plugin/examples/python/pyplugin.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import log
|
||||||
|
from stash_interface import StashInterface
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
def main():
|
||||||
|
input = None
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
input = readJSONInput()
|
||||||
|
log.LogDebug("Raw input: %s" % json.dumps(input))
|
||||||
|
else:
|
||||||
|
log.LogDebug("Using command line inputs")
|
||||||
|
mode = sys.argv[1]
|
||||||
|
log.LogDebug("Command line inputs: {}".format(sys.argv[1:]))
|
||||||
|
|
||||||
|
input = {}
|
||||||
|
input['args'] = {
|
||||||
|
"mode": mode
|
||||||
|
}
|
||||||
|
|
||||||
|
# just some hard-coded values
|
||||||
|
input['server_connection'] = {
|
||||||
|
"Scheme": "http",
|
||||||
|
"Port": 9999,
|
||||||
|
}
|
||||||
|
|
||||||
|
output = {}
|
||||||
|
run(input, output)
|
||||||
|
|
||||||
|
out = json.dumps(output)
|
||||||
|
print(out + "\n")
|
||||||
|
|
||||||
|
def readJSONInput():
|
||||||
|
input = sys.stdin.read()
|
||||||
|
return json.loads(input)
|
||||||
|
|
||||||
|
def run(input, output):
|
||||||
|
modeArg = input['args']["mode"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if modeArg == "" or modeArg == "add":
|
||||||
|
client = StashInterface(input["server_connection"])
|
||||||
|
addTag(client)
|
||||||
|
elif modeArg == "remove":
|
||||||
|
client = StashInterface(input["server_connection"])
|
||||||
|
removeTag(client)
|
||||||
|
elif modeArg == "long":
|
||||||
|
doLongTask()
|
||||||
|
elif modeArg == "indef":
|
||||||
|
doIndefiniteTask()
|
||||||
|
except Exception as e:
|
||||||
|
raise
|
||||||
|
#output["error"] = str(e)
|
||||||
|
#return
|
||||||
|
|
||||||
|
output["output"] = "ok"
|
||||||
|
|
||||||
|
def doLongTask():
|
||||||
|
total = 100
|
||||||
|
upTo = 0
|
||||||
|
|
||||||
|
log.LogInfo("Doing long task")
|
||||||
|
while upTo < total:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
log.LogProgress(float(upTo) / float(total))
|
||||||
|
upTo = upTo + 1
|
||||||
|
|
||||||
|
def doIndefiniteTask():
|
||||||
|
log.LogWarning("Sleeping indefinitely")
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def addTag(client):
|
||||||
|
tagName = "Hawwwwt"
|
||||||
|
tagID = client.findTagIdWithName(tagName)
|
||||||
|
|
||||||
|
if tagID == None:
|
||||||
|
tagID = client.createTagWithName(tagName)
|
||||||
|
|
||||||
|
scene = client.findRandomSceneId()
|
||||||
|
|
||||||
|
if scene == None:
|
||||||
|
raise Exception("no scenes to add tag to")
|
||||||
|
|
||||||
|
tagIds = []
|
||||||
|
for t in scene["tags"]:
|
||||||
|
tagIds.append(t["id"])
|
||||||
|
|
||||||
|
# remove first to ensure we don't re-add the same id
|
||||||
|
try:
|
||||||
|
tagIds.remove(tagID)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
tagIds.append(tagID)
|
||||||
|
|
||||||
|
input = {
|
||||||
|
"id": scene["id"],
|
||||||
|
"tag_ids": tagIds
|
||||||
|
}
|
||||||
|
|
||||||
|
log.LogInfo("Adding tag to scene {}".format(scene["id"]))
|
||||||
|
client.updateScene(input)
|
||||||
|
|
||||||
|
def removeTag(client):
|
||||||
|
tagName = "Hawwwwt"
|
||||||
|
tagID = client.findTagIdWithName(tagName)
|
||||||
|
|
||||||
|
if tagID == None:
|
||||||
|
log.LogInfo("Tag does not exist. Nothing to remove")
|
||||||
|
return
|
||||||
|
|
||||||
|
log.LogInfo("Destroying tag")
|
||||||
|
client.destroyTag(tagID)
|
||||||
|
|
||||||
|
main()
|
||||||
29
pkg/plugin/examples/python/pyraw.yml
Normal file
29
pkg/plugin/examples/python/pyraw.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# example plugin config
|
||||||
|
name: Hawwwwt Tagger (Raw Python edition)
|
||||||
|
description: Python Hawwwwt tagging utility (using raw interface).
|
||||||
|
version: 1.0
|
||||||
|
url: http://www.github.com/stashapp/stash
|
||||||
|
exec:
|
||||||
|
- python
|
||||||
|
- "{pluginDir}/pyplugin.py"
|
||||||
|
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
|
||||||
|
|
||||||
122
pkg/plugin/examples/python/stash_interface.py
Normal file
122
pkg/plugin/examples/python/stash_interface.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
class StashInterface:
|
||||||
|
port = ""
|
||||||
|
url = ""
|
||||||
|
headers = {
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"DNT": "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, conn):
|
||||||
|
self.port = conn['Port']
|
||||||
|
scheme = conn['Scheme']
|
||||||
|
|
||||||
|
self.url = scheme + "://localhost:" + str(self.port) + "/graphql"
|
||||||
|
|
||||||
|
# TODO - cookies
|
||||||
|
|
||||||
|
def __callGraphQL(self, query, variables = None):
|
||||||
|
json = {}
|
||||||
|
json['query'] = query
|
||||||
|
if variables != None:
|
||||||
|
json['variables'] = variables
|
||||||
|
|
||||||
|
# handle cookies
|
||||||
|
response = requests.post(self.url, json=json, headers=self.headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
if result.get("error", None):
|
||||||
|
for error in result["error"]["errors"]:
|
||||||
|
raise Exception("GraphQL error: {}".format(error))
|
||||||
|
if result.get("data", None):
|
||||||
|
return result.get("data")
|
||||||
|
else:
|
||||||
|
raise Exception("GraphQL query failed:{} - {}. Query: {}. Variables: {}".format(response.status_code, response.content, query, variables))
|
||||||
|
|
||||||
|
def findTagIdWithName(self, name):
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
allTags {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = self.__callGraphQL(query)
|
||||||
|
|
||||||
|
for tag in result["allTags"]:
|
||||||
|
if tag["name"] == name:
|
||||||
|
return tag["id"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def createTagWithName(self, name):
|
||||||
|
query = """
|
||||||
|
mutation tagCreate($input:TagCreateInput!) {
|
||||||
|
tagCreate(input: $input){
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
variables = {'input': {
|
||||||
|
'name': name
|
||||||
|
}}
|
||||||
|
|
||||||
|
result = self.__callGraphQL(query, variables)
|
||||||
|
return result["tagCreate"]["id"]
|
||||||
|
|
||||||
|
def destroyTag(self, id):
|
||||||
|
query = """
|
||||||
|
mutation tagDestroy($input: TagDestroyInput!) {
|
||||||
|
tagDestroy(input: $input)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
variables = {'input': {
|
||||||
|
'id': id
|
||||||
|
}}
|
||||||
|
|
||||||
|
self.__callGraphQL(query, variables)
|
||||||
|
|
||||||
|
def findRandomSceneId(self):
|
||||||
|
query = """
|
||||||
|
query findScenes($filter: FindFilterType!) {
|
||||||
|
findScenes(filter: $filter) {
|
||||||
|
count
|
||||||
|
scenes {
|
||||||
|
id
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
variables = {'filter': {
|
||||||
|
'per_page': 1,
|
||||||
|
'sort': 'random'
|
||||||
|
}}
|
||||||
|
|
||||||
|
result = self.__callGraphQL(query, variables)
|
||||||
|
|
||||||
|
if result["findScenes"]["count"] == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result["findScenes"]["scenes"][0]
|
||||||
|
|
||||||
|
def updateScene(self, sceneData):
|
||||||
|
query = """
|
||||||
|
mutation sceneUpdate($input:SceneUpdateInput!) {
|
||||||
|
sceneUpdate(input: $input) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
variables = {'input': sceneData}
|
||||||
|
|
||||||
|
self.__callGraphQL(query, variables)
|
||||||
Reference in New Issue
Block a user