feat: Add Stash Scene Duplicate Comparator & Plugin Automation Hub to Tab 5
This commit is contained in:
@@ -198,11 +198,14 @@ def format_performer_slug(raw_name):
|
||||
parts = [p for p in re.split(r'[\s_\-\.]+', clean) if p]
|
||||
return '.'.join(parts)
|
||||
|
||||
def execute_stash_graphql(query_str, stash_url=None, timeout=4):
|
||||
def execute_stash_graphql(query_str, stash_url=None, variables=None, timeout=4):
|
||||
endpoint = normalize_stash_url(stash_url)
|
||||
payload = {'query': query_str}
|
||||
if variables:
|
||||
payload['variables'] = variables
|
||||
req = urllib.request.Request(
|
||||
endpoint,
|
||||
data=json.dumps({'query': query_str}).encode('utf-8'),
|
||||
data=json.dumps(payload).encode('utf-8'),
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as res:
|
||||
@@ -789,11 +792,15 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
|
||||
self.handle_performer_image()
|
||||
elif self.path.startswith('/api/image/proxy'):
|
||||
self.handle_image_proxy()
|
||||
# Stash Status, Tasks & Performer Review APIs
|
||||
# Stash Status, Tasks, Duplicates & Performer Review APIs
|
||||
elif self.path.startswith('/api/stash/status'):
|
||||
self.handle_stash_status()
|
||||
elif self.path.startswith('/api/stash/tasks/jobs'):
|
||||
self.handle_stash_tasks_jobs()
|
||||
elif self.path.startswith('/api/stash/duplicates'):
|
||||
self.handle_stash_duplicates()
|
||||
elif self.path.startswith('/api/stash/plugins'):
|
||||
self.handle_stash_plugins_list()
|
||||
elif self.path.startswith('/api/stash/performers/missing-images'):
|
||||
self.handle_stash_missing_images()
|
||||
# SMB API
|
||||
@@ -855,6 +862,10 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
|
||||
self.handle_stash_task_generate()
|
||||
elif self.path == '/api/stash/tasks/stop':
|
||||
self.handle_stash_task_stop()
|
||||
elif self.path == '/api/stash/plugins/run-task':
|
||||
self.handle_stash_run_plugin_task()
|
||||
elif self.path == '/api/stash/scenes/destroy':
|
||||
self.handle_stash_scene_destroy()
|
||||
elif self.path == '/api/stash/performers/find-images':
|
||||
self.handle_stash_find_images()
|
||||
elif self.path == '/api/stash/performers/commit-image':
|
||||
@@ -2148,6 +2159,161 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
|
||||
except Exception as e:
|
||||
self.send_json_response(500, {"error": str(e)})
|
||||
|
||||
# ==================== Stash Duplicate Checker & Plugin Integration Handlers ====================
|
||||
def handle_stash_duplicates(self):
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
params = urllib.parse.parse_qs(parsed.query)
|
||||
stash_url = params.get('stash_url', [DEFAULT_STASH_URL])[0]
|
||||
distance = int(params.get('distance', [0])[0])
|
||||
duration_diff = float(params.get('duration_diff', [5.0])[0])
|
||||
|
||||
query = '''
|
||||
query FindDuplicateScenes($distance: Int, $duration_diff: Float) {
|
||||
findDuplicateScenes(distance: $distance, duration_diff: $duration_diff) {
|
||||
id
|
||||
title
|
||||
date
|
||||
details
|
||||
rating100
|
||||
organized
|
||||
paths {
|
||||
screenshot
|
||||
preview
|
||||
stream
|
||||
}
|
||||
files {
|
||||
id
|
||||
path
|
||||
size
|
||||
duration
|
||||
video_codec
|
||||
width
|
||||
height
|
||||
bit_rate
|
||||
frame_rate
|
||||
}
|
||||
performers {
|
||||
id
|
||||
name
|
||||
image_path
|
||||
}
|
||||
studio {
|
||||
id
|
||||
name
|
||||
image_path
|
||||
}
|
||||
tags {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
try:
|
||||
res = execute_stash_graphql(query, stash_url, variables={"distance": distance, "duration_diff": duration_diff}, timeout=30)
|
||||
raw_groups = res.get('data', {}).get('findDuplicateScenes') or []
|
||||
|
||||
duplicate_groups = []
|
||||
for group in raw_groups:
|
||||
if isinstance(group, list) and len(group) > 1:
|
||||
sorted_group = sorted(group, key=lambda s: (
|
||||
(s.get('files', [{}])[0].get('height') or 0),
|
||||
(s.get('files', [{}])[0].get('bit_rate') or 0),
|
||||
(s.get('files', [{}])[0].get('size') or 0)
|
||||
), reverse=True)
|
||||
duplicate_groups.append(sorted_group)
|
||||
|
||||
self.send_json_response(200, {
|
||||
"groups": duplicate_groups,
|
||||
"group_count": len(duplicate_groups),
|
||||
"total_duplicate_scenes": sum(len(g) for g in duplicate_groups),
|
||||
"distance": distance,
|
||||
"duration_diff": duration_diff
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json_response(500, {"error": str(e), "groups": []})
|
||||
|
||||
def handle_stash_plugins_list(self):
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
params = urllib.parse.parse_qs(parsed.query)
|
||||
stash_url = params.get('stash_url', [DEFAULT_STASH_URL])[0]
|
||||
|
||||
query = '''
|
||||
{
|
||||
plugins {
|
||||
id
|
||||
name
|
||||
description
|
||||
version
|
||||
enabled
|
||||
tasks {
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
'''
|
||||
try:
|
||||
res = execute_stash_graphql(query, stash_url, timeout=10)
|
||||
plugins = res.get('data', {}).get('plugins') or []
|
||||
self.send_json_response(200, {"plugins": plugins, "count": len(plugins)})
|
||||
except Exception as e:
|
||||
self.send_json_response(500, {"error": str(e), "plugins": []})
|
||||
|
||||
def handle_stash_run_plugin_task(self):
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
post_data = self.rfile.read(content_length)
|
||||
try:
|
||||
payload = json.loads(post_data.decode('utf-8'))
|
||||
stash_url = payload.get('stash_url') or DEFAULT_STASH_URL
|
||||
plugin_id = payload.get('plugin_id')
|
||||
task_name = payload.get('task_name')
|
||||
|
||||
if not plugin_id or not task_name:
|
||||
self.send_json_response(400, {"error": "Missing plugin_id or task_name"})
|
||||
return
|
||||
|
||||
mutation = '''
|
||||
mutation RunPluginTask($plugin_id: ID!, $task_name: String!) {
|
||||
runPluginTask(plugin_id: $plugin_id, task_name: $task_name)
|
||||
}
|
||||
'''
|
||||
res = execute_stash_graphql(mutation, stash_url, variables={"plugin_id": plugin_id, "task_name": task_name}, timeout=15)
|
||||
self.send_json_response(200, {"success": True, "job_id": res.get('data', {}).get('runPluginTask')})
|
||||
except Exception as e:
|
||||
self.send_json_response(500, {"error": str(e)})
|
||||
|
||||
def handle_stash_scene_destroy(self):
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
post_data = self.rfile.read(content_length)
|
||||
try:
|
||||
payload = json.loads(post_data.decode('utf-8'))
|
||||
stash_url = payload.get('stash_url') or DEFAULT_STASH_URL
|
||||
scene_id = payload.get('scene_id')
|
||||
delete_file = bool(payload.get('delete_file', True))
|
||||
delete_generated = bool(payload.get('delete_generated', True))
|
||||
|
||||
if not scene_id:
|
||||
self.send_json_response(400, {"error": "Missing scene_id"})
|
||||
return
|
||||
|
||||
mutation = '''
|
||||
mutation SceneDestroy($input: SceneDestroyInput!) {
|
||||
sceneDestroy(input: $input)
|
||||
}
|
||||
'''
|
||||
variables = {
|
||||
"input": {
|
||||
"id": str(scene_id),
|
||||
"delete_file": delete_file,
|
||||
"delete_generated": delete_generated
|
||||
}
|
||||
}
|
||||
res = execute_stash_graphql(mutation, stash_url, variables=variables, timeout=15)
|
||||
self.send_json_response(200, {"success": True, "result": res.get('data', {}).get('sceneDestroy')})
|
||||
except Exception as e:
|
||||
self.send_json_response(500, {"error": str(e)})
|
||||
|
||||
# ==================== SMB Maintenance & Cleanup Handlers ====================
|
||||
def handle_smb_scan_cleanup(self):
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
|
||||
Reference in New Issue
Block a user