mirror of
https://github.com/stashapp/CommunityScripts.git
synced 2026-06-11 09:09:31 -05:00
merge
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
name: VideoScrollWheel
|
||||
# requires: CommunityScriptsUILibrary
|
||||
description: Adds functionality to change volume/time in scene video player by hovering over left/right side of player and scrolling with mouse scrollwheel. Scroll while hovering on left side to adjust volume, scroll on right side to skip forward/back.
|
||||
#requires: CommunityScriptsUILibrary
|
||||
version: 0.2
|
||||
settings:
|
||||
allowVolumeChange:
|
||||
|
||||
@@ -19,7 +19,7 @@ function ok() {
|
||||
function main() {
|
||||
var hookContext = input.Args.hookContext;
|
||||
var type = hookContext.type;
|
||||
var ID = hookContext.ID;
|
||||
var ID = hookContext.id;
|
||||
|
||||
if (!type || !ID) {
|
||||
// just return
|
||||
@@ -183,7 +183,11 @@ function getImagePath(ID) {
|
||||
"\
|
||||
query findImage($id: ID) {\
|
||||
findImage(id: $id) {\
|
||||
path\
|
||||
visual_files {\
|
||||
... on ImageFile {\
|
||||
path\
|
||||
}\
|
||||
}\
|
||||
}\
|
||||
}";
|
||||
|
||||
@@ -197,7 +201,7 @@ query findImage($id: ID) {\
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = findImage.path;
|
||||
var path = findImage["visual_files"][0].path;
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -241,7 +245,7 @@ function getTagId(tagName) {
|
||||
},
|
||||
};
|
||||
|
||||
result = gql.Do(query, variables);
|
||||
var result = gql.Do(query, variables);
|
||||
if (result.findTags.tags[0]) {
|
||||
return result.findTags.tags[0].id;
|
||||
} else {
|
||||
@@ -267,7 +271,7 @@ function getPerformerId(performerName) {
|
||||
},
|
||||
};
|
||||
|
||||
result = gql.Do(query, variables);
|
||||
var result = gql.Do(query, variables);
|
||||
if (result.findPerformers.performers[0]) {
|
||||
return result.findPerformers.performers[0].id;
|
||||
} else {
|
||||
@@ -293,7 +297,7 @@ function getStudioId(studioName) {
|
||||
},
|
||||
};
|
||||
|
||||
result = gql.Do(query, variables);
|
||||
var result = gql.Do(query, variables);
|
||||
if (result.findStudios.studios[0]) {
|
||||
return result.findStudios.studios[0].id;
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Discord Presence
|
||||
description: Sets currently playing scene data as your Discord status. See README for prerequisites and config options (blue hyperlink next to enable/disable button)
|
||||
url: https://github.com/stashapp/CommunityScripts/tree/main/plugins/discordPresence
|
||||
#requires: CommunityScriptsUILibrary
|
||||
# requires: CommunityScriptsUILibrary
|
||||
version: 1.0
|
||||
settings:
|
||||
discordClientId:
|
||||
|
||||
@@ -4,28 +4,139 @@ function ok() {
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
var hookContext = input.Args.hookContext;
|
||||
var opInput = hookContext.Input;
|
||||
var primaryTagID = opInput.PrimaryTagID;
|
||||
var sceneID = opInput.SceneID;
|
||||
function includes(haystack, needle) {
|
||||
for (var i = 0; i < haystack.length; ++i) {
|
||||
if (haystack[i] == needle) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function mapSceneTagsToIds(sceneTags) {
|
||||
var ret = [];
|
||||
for (var i = 0; i < sceneTags.length; ++i) {
|
||||
ret.push(sceneTags[i].id);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function shouldHandleAllTags() {
|
||||
var query =
|
||||
"query Query {\
|
||||
configuration {\
|
||||
plugins\
|
||||
}\
|
||||
}";
|
||||
|
||||
var result = gql.Do(query);
|
||||
//log.Info("Config is " + JSON.stringify(result.configuration));
|
||||
if (!result.configuration) {
|
||||
throw "Unable to get library paths";
|
||||
}
|
||||
if (result.configuration.plugins.hasOwnProperty("markerTagToScene")) {
|
||||
//log.Info("allTags is " + result.configuration.plugins.markerTagToScene.allTags);
|
||||
return !!result.configuration.plugins.markerTagToScene.allTags;
|
||||
} else {
|
||||
//log.Info("all tags wasn't found. defaulting to false");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function processMarker(marker, shouldHandleAllTags) {
|
||||
log.Debug("processMarker (allTags = " + shouldHandleAllTags + ") " + marker);
|
||||
var primaryTagID = marker.primary_tag_id;
|
||||
var sceneID = marker.scene_id;
|
||||
|
||||
var tagIDsToCheck = [];
|
||||
if (primaryTagID != null) tagIDsToCheck.push(primaryTagID);
|
||||
|
||||
if (shouldHandleAllTags && marker.tag_ids != null)
|
||||
tagIDsToCheck = tagIDsToCheck.concat(marker.tag_ids);
|
||||
|
||||
// we can't currently find scene markers. If it's not in the input
|
||||
// then just return
|
||||
if (!primaryTagID || !sceneID) {
|
||||
if (tagIDsToCheck.length == 0) {
|
||||
// just return
|
||||
return ok();
|
||||
}
|
||||
|
||||
// get the existing scene tags
|
||||
var sceneTags = getSceneTags(sceneID);
|
||||
var sceneTags = mapSceneTagsToIds(getSceneTags(sceneID));
|
||||
var newTags = [];
|
||||
for (var i = 0; i < tagIDsToCheck.length; ++i) {
|
||||
var tag = tagIDsToCheck[i];
|
||||
if (!includes(sceneTags, tag)) newTags.push(tag);
|
||||
}
|
||||
|
||||
// Combine all tags from scene and the new marker
|
||||
var allTags = [...new Set([...sceneTags, primaryTagID, ...opInput.TagIds])];
|
||||
var newTags = allTags.filter((t) => sceneTags.includes(t));
|
||||
if (newTags.length == 0) {
|
||||
// All tags were present; don't do anything
|
||||
log.Debug("All tags were already present on scene " + sceneID);
|
||||
return ok();
|
||||
}
|
||||
var tagIDs = sceneTags.concat(newTags);
|
||||
setSceneTags(sceneID, tagIDs);
|
||||
log.Info("adding tags " + newTags + " to scene " + sceneID);
|
||||
}
|
||||
|
||||
setSceneTags(sceneID, allTags);
|
||||
log.Debug("added new tags " + newTags.join(", ") + " to scene " + sceneID);
|
||||
function main() {
|
||||
log.Info(JSON.stringify(input));
|
||||
if (input.Args.mode == "processMarkers") {
|
||||
var allTags = shouldHandleAllTags();
|
||||
log.Trace("Mode is processMarkers, allTags is " + allTags);
|
||||
var allMarkers = getAllMarkers();
|
||||
//The markers come back as {primary_tag: { id: 600 } }
|
||||
//but processMarker (because of the hook) expects 'primary_tag_id', so transform it here
|
||||
log.Info(
|
||||
"markerTagToScene has " + allMarkers.length + " markers to process"
|
||||
);
|
||||
for (var i = 0; i < allMarkers.length; ++i) {
|
||||
var marker = allMarkers[i];
|
||||
var sceneMarker = {};
|
||||
sceneMarker.id = marker.id;
|
||||
sceneMarker.scene_id = marker.scene.id;
|
||||
sceneMarker.primary_tag_id = marker.primary_tag.id;
|
||||
var tag_ids = [];
|
||||
for (var j = 0; j < marker.tags.length; ++j) {
|
||||
tag_ids.push(marker.tags[j].id);
|
||||
}
|
||||
sceneMarker.tag_ids = tag_ids;
|
||||
//log.Info(sceneMarker);
|
||||
processMarker(sceneMarker, allTags);
|
||||
log.Progress(i / allMarkers.length);
|
||||
}
|
||||
log.Progress("Finished processing markers");
|
||||
} else if (input.Args.mode == "hook") {
|
||||
log.Info("Mode is hook");
|
||||
processMarker(input.Args.hookContext.input, shouldHandleAllTags());
|
||||
} else {
|
||||
log.Error("Unknown mode");
|
||||
}
|
||||
}
|
||||
|
||||
function getAllMarkers() {
|
||||
var query =
|
||||
"\
|
||||
query Query($filter: FindFilterType) {\
|
||||
findSceneMarkers (filter: $filter) {\
|
||||
scene_markers {\
|
||||
id,\
|
||||
primary_tag {\
|
||||
id\
|
||||
}\
|
||||
tags {\
|
||||
id\
|
||||
}\
|
||||
scene {\
|
||||
id\
|
||||
}\
|
||||
}\
|
||||
}\
|
||||
}";
|
||||
var variables = { filter: { per_page: -1 } };
|
||||
var result = gql.Do(query, variables);
|
||||
var findSceneMarkers = result.findSceneMarkers;
|
||||
if (findSceneMarkers) {
|
||||
return findSceneMarkers.scene_markers;
|
||||
}
|
||||
}
|
||||
|
||||
function getSceneTags(sceneID) {
|
||||
@@ -46,7 +157,7 @@ query findScene($id: ID) {\
|
||||
var result = gql.Do(query, variables);
|
||||
var findScene = result.findScene;
|
||||
if (findScene) {
|
||||
return findScene.tags.map((t) => t.id);
|
||||
return findScene.tags;
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
@@ -6,9 +6,21 @@ version: 1.0
|
||||
exec:
|
||||
- markerTagToScene.js
|
||||
interface: js
|
||||
settings:
|
||||
allTags:
|
||||
displayName: All Tags
|
||||
description: Add all scene tags instead of just the primary scene tag.
|
||||
type: BOOLEAN
|
||||
hooks:
|
||||
- name: Update scene with scene marker tag
|
||||
description: Adds primary tag of Scene Marker to the Scene on marker create/update.
|
||||
triggeredBy:
|
||||
- SceneMarker.Create.Post
|
||||
- SceneMarker.Update.Post
|
||||
defaultArgs:
|
||||
mode: hook
|
||||
tasks:
|
||||
- name: Process all markers
|
||||
description: Add tags from all markers to scenes
|
||||
defaultArgs:
|
||||
mode: processMarkers
|
||||
|
||||
@@ -9,7 +9,16 @@ per_page = 100
|
||||
skip_tag = "[MiscTags: Skip]"
|
||||
|
||||
# Defaults if nothing has changed in the stash ui
|
||||
settings = {"addStashVrCompanionTags": False, "addVrTags": False,"addVSoloTags": True,"addVThreesomeTags":True, "flatStudio": ""}
|
||||
settings = {"addStashVrCompanionTags": False,
|
||||
"addVRTags": False,
|
||||
"addSoloTags": True,
|
||||
"addThreesomeTags": True,
|
||||
"addFoursomeTags": True,
|
||||
"addFivesomeTags": True,
|
||||
"addSixsomeTags": True,
|
||||
"addSevensomeTags": True,
|
||||
"assumeMissingMale": True,
|
||||
"flatStudio": ""}
|
||||
|
||||
VRCTags = {
|
||||
"flat": {"VRCTags": ["FLAT"], "projTags": []},
|
||||
@@ -20,7 +29,7 @@ VRCTags = {
|
||||
"180_lr": {"VRCTags": ["DOME", "SBS"], "projTags": ["180°"]},
|
||||
"180_3dh_lr": {"VRCTags": ["DOME", "SBS"], "projTags": ["180°"]},
|
||||
"360_tb": {"VRCTags": ["SPHERE", "TB"], "projTags": ["360°"]},
|
||||
"mkx200": {"VRCTags": ["MKX200", "FISHEYE", "SBS"], "projTags": ["220°"]},
|
||||
"mkx200": {"VRCTags": ["MKX200", "FISHEYE", "SBS"], "projTags": ["200°"]},
|
||||
"mkx220": {"VRCTags": ["MKX220", "FISHEYE", "SBS"], "projTags": ["220°"]},
|
||||
"vrca220": {"VRCTags": ["VRCA220", "FISHEYE", "SBS"], "projTags": ["220°"]},
|
||||
"rf52": {"VRCTags": ["RF52", "FISHEYE", "SBS"], "projTags": ["190°"]},
|
||||
@@ -32,6 +41,7 @@ VRCTags = {
|
||||
"5k": {"VRCTags": [], "projTags": ["5K"]},
|
||||
}
|
||||
tags_cache = {}
|
||||
performer_cache = {}
|
||||
|
||||
|
||||
def processScene(scene):
|
||||
@@ -45,12 +55,21 @@ def processScene(scene):
|
||||
if settings["addStashVrCompanionTags"]:
|
||||
processStashVRCompanionTags(scene, tags)
|
||||
log.debug(tags)
|
||||
if settings["addVrTags"]:
|
||||
if settings["addVRTags"]:
|
||||
processVRTags(scene, tags)
|
||||
if settings["addVSoloTags"]:
|
||||
soloTag(scene,tags)
|
||||
if settings["addVThreesomeTags"]:
|
||||
threesomeTag(scene, tags)
|
||||
if settings["addSoloTags"]:
|
||||
soloTag(scene, tags)
|
||||
if settings["addThreesomeTags"]:
|
||||
processGroupMakeup(['threesome'], 'Threesome', 3, scene, tags)
|
||||
if settings["addFoursomeTags"]:
|
||||
processGroupMakeup(['foursome', '4some'], 'Foursome', 4, scene, tags)
|
||||
if settings["addFivesomeTags"]:
|
||||
processGroupMakeup(['fivesome', 'fiveway'], 'Fivesome', 5, scene, tags)
|
||||
if settings["addSixsomeTags"]:
|
||||
processGroupMakeup(['sixsome'], 'Sixsome', 6, scene, tags)
|
||||
if settings["addSevensomeTags"]:
|
||||
processGroupMakeup(['sevensome'], 'Sevensome', 7, scene, tags)
|
||||
|
||||
if len(settings["flatStudio"]) > 0:
|
||||
processFlatStudio(scene, tags)
|
||||
if len(tags) > 0:
|
||||
@@ -135,58 +154,69 @@ def processFlatStudio(scene, tags):
|
||||
|
||||
def soloTag(scene,tags):
|
||||
"""Add Solo Female, Solo male, Solo Trans where there is a single performer and the solo tag"""
|
||||
for name in ['solo', 'solo model','solo models']:
|
||||
for name in ['solo', 'solo model', 'solo models']:
|
||||
if name in [x["name"].lower() for x in scene['tags']]:
|
||||
if len(scene['performers']) ==1:
|
||||
p=stash.find_performer(scene['performers'][0])
|
||||
if p['gender']=='FEMALE':
|
||||
if len(scene['performers']) == 1:
|
||||
p=getPerformer(scene['performers'][0])
|
||||
if p['gender'] == 'FEMALE':
|
||||
tags.append('Solo Female')
|
||||
elif p['gender'] == 'MALE':
|
||||
tags.append('Solo Male')
|
||||
elif p['gender'] =='TRANSGENDER_MALE':
|
||||
elif p['gender'] == 'TRANSGENDER_MALE':
|
||||
tags.append('Solo Trans')
|
||||
elif p['gender'] =='TRANSGENDER_FEMALE':
|
||||
elif p['gender'] == 'TRANSGENDER_FEMALE':
|
||||
tags.append('Solo Trans')
|
||||
|
||||
def threesomeTag(scene, tags):
|
||||
"""Add Threesome (XXX) tags based on group makeup"""
|
||||
for name in ['threesome']:
|
||||
|
||||
def getPerformer(p):
|
||||
if p['id'] not in performer_cache:
|
||||
p2 = stash.find_performer(p)
|
||||
performer_cache[p['id']] = p2
|
||||
log.debug(performer_cache)
|
||||
return p2
|
||||
return performer_cache[p]
|
||||
|
||||
|
||||
def processGroupMakeup(tag_strings, makeup_label, count, scene, tags):
|
||||
for name in tag_strings:
|
||||
if name in [x["name"].lower() for x in scene['tags']]:
|
||||
if len(scene['performers']) == 3:
|
||||
makeup=[]
|
||||
for p in scene['performers']:
|
||||
p2=stash.find_performer(p)
|
||||
makeup.append(p2['gender'])
|
||||
makeup = []
|
||||
for p in scene['performers']:
|
||||
p2 = getPerformer(p)
|
||||
if p2['gender'] == 'FEMALE':
|
||||
makeup.append('G')
|
||||
elif p2['gender'] == 'MALE':
|
||||
makeup.append('B')
|
||||
elif p2['gender'] == 'TRANSGENDER_FEMALE':
|
||||
makeup.append('T')
|
||||
elif p2['gender'] == 'TRANSGENDER_MALE':
|
||||
makeup.append('T')
|
||||
elif p2['gender'] == 'INTERSEX':
|
||||
makeup.append('I')
|
||||
elif p2['gender'] == 'NON_BINARY':
|
||||
makeup.append('I')
|
||||
else:
|
||||
makeup.append('U')
|
||||
# unknown or not set yet
|
||||
#
|
||||
if settings["assumeMissingMale"]:
|
||||
# If there is a mising performer
|
||||
for x in range(len(makeup), count):
|
||||
makeup.append('B')
|
||||
|
||||
if len(makeup) == count:
|
||||
makeup.sort()
|
||||
log.debug(makeup)
|
||||
if makeup[0]=='FEMALE' and makeup[1]=='FEMALE' and makeup[2]=='FEMALE':
|
||||
tags.append('Threesome (Lesbian)')
|
||||
elif makeup[0]=='FEMALE' and makeup[1]=='FEMALE' and makeup[2]=='MALE':
|
||||
tags.append('Threesome (BGG)')
|
||||
elif makeup[0]=='FEMALE' and makeup[1]=='MALE' and makeup[2]=='MALE':
|
||||
tags.append('Threesome (BBG)')
|
||||
elif makeup[0]=='FEMALE' and makeup[1]=='FEMALE' and ( makeup[2]=='TRANSGENDER_FEMALE' or makeup[2]=='TRANSGENDER_MALE'):
|
||||
tags.append('Threesome (GGT)')
|
||||
elif makeup[0]=='FEMALE' and ( makeup[1]=='TRANSGENDER_FEMALE' or makeup[1]=='TRANSGENDER_MALE') and ( makeup[2]=='TRANSGENDER_FEMALE' or makeup[2]=='TRANSGENDER_MALE'):
|
||||
tags.append('Threesome (GTT)')
|
||||
elif makeup[0]=='FEMALE' and makeup[1]=='MALE' and ( makeup[2]=='TRANSGENDER_FEMALE' or makeup[2]=='TRANSGENDER_MALE'):
|
||||
tags.append('Threesome (BGT)')
|
||||
elif makeup[0]=='MALE' and makeup[1]=='MALE' and makeup[2]=='MALE':
|
||||
tags.append('Threesome (Gay)')
|
||||
elif makeup[0]=='MALE' and makeup[1]=='MALE' and ( makeup[2]=='TRANSGENDER_FEMALE' or makeup[2]=='TRANSGENDER_MALE'):
|
||||
tags.append('Threesome (BBT)')
|
||||
elif makeup[0]=='MALE' and ( makeup[1]=='TRANSGENDER_FEMALE' or makeup[1]=='TRANSGENDER_MALE') and ( makeup[2]=='TRANSGENDER_FEMALE' or makeup[2]=='TRANSGENDER_MALE'):
|
||||
tags.append('Threesome (BTT)')
|
||||
elif ( makeup[0]=='TRANSGENDER_FEMALE' or makeup[0]=='TRANSGENDER_MALE') and ( makeup[1]=='TRANSGENDER_FEMALE' or makeup[1]=='TRANSGENDER_MALE') and ( makeup[2]=='TRANSGENDER_FEMALE' or makeup[2]=='TRANSGENDER_MALE'):
|
||||
tags.append('Threesome (Trans)')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
makeup_str = ''.join(makeup)
|
||||
if makeup_str == ('G' * count):
|
||||
tags.append('%s (Lesbian)' % (makeup_label, ))
|
||||
elif makeup_str == ('B' * count):
|
||||
tags.append('%s (Gay)' % (makeup_label, ))
|
||||
elif makeup_str == ('G' * count):
|
||||
tags.append('%s (Lesbian)' % (makeup_label, ))
|
||||
else:
|
||||
tags.append('%s (%s)' % (makeup_label, makeup_str, ) )
|
||||
else:
|
||||
log.debug('missing performers for group makeup %s, have %s performers instead' % (makeup_label, len(makeup), ))
|
||||
|
||||
|
||||
def processScenes():
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Misc Tags
|
||||
description: Add extra tags for VR and other ues
|
||||
version: 0.3
|
||||
version: 0.4
|
||||
url: https://github.com/stashapp/CommunityScripts/
|
||||
exec:
|
||||
- python
|
||||
@@ -15,14 +15,34 @@ settings:
|
||||
displayName: Add VR related tags
|
||||
description: Add projection based tags 180°, 200°, VR
|
||||
type: BOOLEAN
|
||||
addVSoloTags:
|
||||
addSoloTags:
|
||||
displayName: Add solo tags
|
||||
description: if there is the "solo" tag and 1 performer add the "Solo Female" tag
|
||||
type: BOOLEAN
|
||||
addVThreesomeTags:
|
||||
displayName: Add threesome tags
|
||||
addThreesomeTags:
|
||||
displayName: Add threesome (xxx)
|
||||
description: if there is the "threesome" tag with 3 tagged performers add a group makeup tag eg Threesome (BGG)
|
||||
type: BOOLEAN
|
||||
addFoursomeTags:
|
||||
displayName: Add Foursome (XXXX)
|
||||
description: if there is the "foursome" tag with 4 tagged performers add a group makeup tag eg Threesome (BGG)
|
||||
type: BOOLEAN
|
||||
addFivesomeTags:
|
||||
displayName: Add Fivesome (xxxxx) tags
|
||||
description: if there is the "fivesome" tag with 5 tagged performers add a group makeup tag eg Threesome (BGG)
|
||||
type: BOOLEAN
|
||||
addSixsomeTags:
|
||||
displayName: Add Sixsome (XXXXXX)
|
||||
description: if there is the "sixsome" tag with 6 tagged performers add a group makeup tag eg Threesome (BGG)
|
||||
type: BOOLEAN
|
||||
addSevensomeTags:
|
||||
displayName: Add Sevensome (XXXXXXX)
|
||||
description: if there is the "sevensome" tag with 7 tagged performers add a group makeup tag eg Threesome (BGG)
|
||||
type: BOOLEAN
|
||||
assumeMissingMale:
|
||||
displayName: Assume missing male performer
|
||||
description: For the group makeup tag assume the missing performer is male if there are unknown performers
|
||||
type: BOOLEAN
|
||||
|
||||
flatStudio:
|
||||
displayName: 2d Studio for stash-vr-companion
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Extended Stats
|
||||
#requires: CommunityScriptsUILibrary
|
||||
# requires: CommunityScriptsUILibrary
|
||||
description: Adds new stats to the stats page
|
||||
version: 1.1
|
||||
ui:
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
|
||||
# Tag Graph Generator
|
||||
|
||||
## Requirements
|
||||
* python >= 3.7.X
|
||||
* `pip install -r requirements.txt`
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Running as a plugin
|
||||
move the `tagGraph` directory into Stash's plugins directory, reload plugins and you can run the **Generate Graph** task
|
||||
|
||||
### Running as a script
|
||||
> **⚠️ Note:** use this if you are connecting to a remote instance of stash
|
||||
|
||||
ensure `STASH_SETTINGS` is configured properly, you will likely need to change it
|
||||
|
||||
run `python .\tag_graph.py -script`
|
||||
|
||||
### View graph
|
||||
a `tag_graph.html` file will be generated inside the tagGraph directory, open it with a browser to view/interact with the graph
|
||||
|
||||
---
|
||||
|
||||
## Customizing the graph
|
||||
set `SHOW_OPTIONS` to `True` and you will get an interface to play around with that will affect what the graph looks like.
|
||||
|
||||
for more info see [pyvis docs](https://pyvis.readthedocs.io/en/latest/tutorial.html#using-the-configuration-ui-to-dynamically-tweak-network-settings)
|
||||
@@ -1,7 +0,0 @@
|
||||
STASH_SETTINGS = {
|
||||
"Scheme": "http",
|
||||
"Domain": "localhost",
|
||||
"Port": "9999",
|
||||
"ApiKey": "YOUR_API_KEY_HERE",
|
||||
}
|
||||
SHOW_OPTIONS = False
|
||||
@@ -1,2 +0,0 @@
|
||||
pyvis==0.1.9
|
||||
requests==2.25.1
|
||||
@@ -1,272 +0,0 @@
|
||||
import os, re, sys, copy, json, requests
|
||||
|
||||
# local dependencies
|
||||
import config
|
||||
|
||||
# external dependencies
|
||||
from pyvis.network import Network
|
||||
|
||||
|
||||
class StashLogger:
|
||||
# Log messages sent from a script scraper 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 or e - corresponding to trace, debug, info,
|
||||
# warning and error levels respectively), then special character
|
||||
# STX.
|
||||
#
|
||||
# The log.trace, log.debug, log.info, log.warning, and log.error methods, and their equivalent
|
||||
# formatted methods are intended for use by script scraper instances to transmit log
|
||||
# messages.
|
||||
#
|
||||
def __log(self, level_char: bytes, s):
|
||||
if level_char:
|
||||
lvl_char = "\x01{}\x02".format(level_char.decode())
|
||||
s = re.sub(r"data:image.+?;base64(.+?')", "[...]", str(s))
|
||||
for x in s.split("\n"):
|
||||
print(lvl_char, x, file=sys.stderr, flush=True)
|
||||
|
||||
def trace(self, s):
|
||||
self.__log(b"t", s)
|
||||
|
||||
def debug(self, s):
|
||||
self.__log(b"d", s)
|
||||
|
||||
def info(self, s):
|
||||
self.__log(b"i", s)
|
||||
|
||||
def warning(self, s):
|
||||
self.__log(b"w", s)
|
||||
|
||||
def error(self, s):
|
||||
self.__log(b"e", s)
|
||||
|
||||
def progress(self, p):
|
||||
progress = min(max(0, p), 1)
|
||||
self.__log(b"p", str(progress))
|
||||
|
||||
|
||||
class StashInterface:
|
||||
port = ""
|
||||
url = ""
|
||||
headers = {
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Connection": "keep-alive",
|
||||
"DNT": "1",
|
||||
}
|
||||
cookies = {}
|
||||
|
||||
def __init__(self, conn, fragments={}):
|
||||
global log
|
||||
|
||||
if conn.get("Logger"):
|
||||
log = conn.get("Logger")
|
||||
else:
|
||||
raise Exception("No logger passed to StashInterface")
|
||||
|
||||
self.port = conn["Port"] if conn.get("Port") else "9999"
|
||||
scheme = conn["Scheme"] if conn.get("Scheme") else "http"
|
||||
|
||||
api_key = conn.get("ApiKey")
|
||||
if api_key:
|
||||
self.headers["ApiKey"] = api_key
|
||||
|
||||
# Session cookie for authentication
|
||||
self.cookies = {}
|
||||
if conn.get("SessionCookie"):
|
||||
self.cookies.update({"session": conn["SessionCookie"]["Value"]})
|
||||
|
||||
domain = conn["Domain"] if conn.get("Domain") else "localhost"
|
||||
|
||||
# Stash GraphQL endpoint
|
||||
self.url = f"{scheme}://{domain}:{self.port}/graphql"
|
||||
|
||||
try:
|
||||
self.get_stash_config()
|
||||
except Exception:
|
||||
log.error(f"Could not connect to Stash at {self.url}")
|
||||
sys.exit()
|
||||
|
||||
log.info(f"Using Stash's GraphQl endpoint at {self.url}")
|
||||
|
||||
self.fragments = fragments
|
||||
|
||||
def __resolveFragments(self, query):
|
||||
|
||||
fragmentReferences = list(set(re.findall(r"(?<=\.\.\.)\w+", query)))
|
||||
fragments = []
|
||||
for ref in fragmentReferences:
|
||||
fragments.append(
|
||||
{
|
||||
"fragment": ref,
|
||||
"defined": bool(re.search("fragment {}".format(ref), query)),
|
||||
}
|
||||
)
|
||||
|
||||
if all([f["defined"] for f in fragments]):
|
||||
return query
|
||||
else:
|
||||
for fragment in [f["fragment"] for f in fragments if not f["defined"]]:
|
||||
if fragment not in self.fragments:
|
||||
raise Exception(f'GraphQL error: fragment "{fragment}" not defined')
|
||||
query += self.fragments[fragment]
|
||||
return self.__resolveFragments(query)
|
||||
|
||||
def __callGraphQL(self, query, variables=None):
|
||||
|
||||
query = self.__resolveFragments(query)
|
||||
|
||||
json_request = {"query": query}
|
||||
if variables is not None:
|
||||
json_request["variables"] = variables
|
||||
|
||||
response = requests.post(
|
||||
self.url, json=json_request, headers=self.headers, cookies=self.cookies
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
|
||||
if result.get("errors"):
|
||||
for error in result["errors"]:
|
||||
log.error(f"GraphQL error: {error}")
|
||||
if result.get("error"):
|
||||
for error in result["error"]["errors"]:
|
||||
log.error(f"GraphQL error: {error}")
|
||||
if result.get("data"):
|
||||
return result["data"]
|
||||
elif response.status_code == 401:
|
||||
sys.exit(
|
||||
"HTTP Error 401, Unauthorized. Cookie authentication most likely failed"
|
||||
)
|
||||
else:
|
||||
raise ConnectionError(
|
||||
"GraphQL query failed:{} - {}. Query: {}. Variables: {}".format(
|
||||
response.status_code, response.content, query, variables
|
||||
)
|
||||
)
|
||||
|
||||
def __match_alias_item(self, search, items):
|
||||
item_matches = {}
|
||||
for item in items:
|
||||
if re.match(rf"{search}$", item.name, re.IGNORECASE):
|
||||
log.debug(
|
||||
f'matched "{search}" to "{item.name}" ({item.id}) using primary name'
|
||||
)
|
||||
item_matches[item.id] = item
|
||||
if not item.aliases:
|
||||
continue
|
||||
for alias in item.aliases:
|
||||
if re.match(rf"{search}$", alias.strip(), re.IGNORECASE):
|
||||
log.debug(
|
||||
f'matched "{search}" to "{alias}" ({item.id}) using alias'
|
||||
)
|
||||
item_matches[item.id] = item
|
||||
return list(item_matches.values())
|
||||
|
||||
def get_stash_config(self):
|
||||
query = """
|
||||
query Configuration {
|
||||
configuration { general { stashes{ path } } }
|
||||
}
|
||||
"""
|
||||
result = self.__callGraphQL(query)
|
||||
return result["configuration"]
|
||||
|
||||
def get_tags_with_relations(self):
|
||||
query = """
|
||||
query FindTags($filter: FindFilterType, $tag_filter: TagFilterType) {
|
||||
findTags(filter: $filter, tag_filter: $tag_filter) {
|
||||
count
|
||||
tags {
|
||||
id
|
||||
name
|
||||
parents { id }
|
||||
children { id }
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
variables = {
|
||||
"tag_filter": {
|
||||
"child_count": {"modifier": "GREATER_THAN", "value": 0},
|
||||
"OR": {"parent_count": {"modifier": "GREATER_THAN", "value": 0}},
|
||||
},
|
||||
"filter": {"q": "", "per_page": -1},
|
||||
}
|
||||
result = self.__callGraphQL(query, variables)
|
||||
return result["findTags"]["tags"]
|
||||
|
||||
|
||||
def script_init():
|
||||
import logging as log
|
||||
|
||||
log.basicConfig(level=log.INFO, format="%(levelname)s: %(message)s")
|
||||
stash_connection = config.STASH_SETTINGS
|
||||
stash_connection["Logger"] = log
|
||||
generate_graph(stash_connection)
|
||||
|
||||
|
||||
def plugin_init():
|
||||
log = StashLogger()
|
||||
stash_connection = json.loads(sys.stdin.read())["server_connection"]
|
||||
stash_connection["Logger"] = log
|
||||
generate_graph(stash_connection)
|
||||
print(json.dumps({"output": "ok"}))
|
||||
|
||||
|
||||
def generate_graph(stash_connection):
|
||||
log = stash_connection["Logger"]
|
||||
|
||||
stash = StashInterface(stash_connection)
|
||||
log.info("getting tags from stash...")
|
||||
tags = stash.get_tags_with_relations()
|
||||
|
||||
log.info("generating graph...")
|
||||
if config.SHOW_OPTIONS:
|
||||
G = Network(
|
||||
directed=True,
|
||||
height="100%",
|
||||
width="66%",
|
||||
bgcolor="#202b33",
|
||||
font_color="white",
|
||||
)
|
||||
G.show_buttons()
|
||||
else:
|
||||
G = Network(
|
||||
directed=True,
|
||||
height="100%",
|
||||
width="100%",
|
||||
bgcolor="#202b33",
|
||||
font_color="white",
|
||||
)
|
||||
|
||||
node_theme = {
|
||||
"border": "#adb5bd",
|
||||
"background": "#394b59",
|
||||
"highlight": {"border": "#137cbd", "background": "#FFFFFF"},
|
||||
}
|
||||
edge_theme = {"color": "#FFFFFF", "highlight": "#137cbd"}
|
||||
|
||||
# create all nodes
|
||||
for tag in tags:
|
||||
G.add_node(tag["id"], label=tag["name"], color=node_theme)
|
||||
# create all edges
|
||||
for tag in tags:
|
||||
for child in tag["children"]:
|
||||
G.add_edge(tag["id"], child["id"], color=edge_theme)
|
||||
|
||||
current_abs_path = os.path.dirname(os.path.abspath(__file__))
|
||||
save_path = os.path.join(current_abs_path, "tag_graph.html")
|
||||
|
||||
G.save_graph(save_path)
|
||||
log.info(f'saved graph to "{save_path}"')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
script_init()
|
||||
else:
|
||||
plugin_init()
|
||||
@@ -1,12 +0,0 @@
|
||||
name: Tag Graph
|
||||
description: Creates a visual of the Tag relations
|
||||
version: 0.2
|
||||
exec:
|
||||
- python
|
||||
- "{pluginDir}/tag_graph.py"
|
||||
interface: raw
|
||||
tasks:
|
||||
- name: Generate Graph
|
||||
description: generates graph from current tag data
|
||||
defaultArgs:
|
||||
mode: generate
|
||||
Reference in New Issue
Block a user