feat: Add Tab 5 for Stash Task & Tool Control (Start Scan, Identify Scenes, Clean, Generate) with live Job Queue Monitor

This commit is contained in:
david
2026-08-18 08:45:52 -04:00
parent 1a5cbc3e47
commit e354b52652
2 changed files with 788 additions and 5 deletions
+230 -3
View File
@@ -789,9 +789,11 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
self.handle_performer_image()
elif self.path.startswith('/api/image/proxy'):
self.handle_image_proxy()
# Stash Status & Performer Review APIs
# Stash Status, Tasks & 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/performers/missing-images'):
self.handle_stash_missing_images()
# SMB API
@@ -836,13 +838,23 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
self.handle_history_undo()
elif self.path == '/api/history/clear':
self.handle_history_clear()
# Stash Lookup, Scan & Image Review APIs
# Stash Lookup, Tasks, Scan & Image Review APIs
elif self.path == '/api/stash/lookup':
self.handle_stash_lookup()
elif self.path == '/api/stash/batch-lookup':
self.handle_stash_batch_lookup()
elif self.path == '/api/stash/scan':
self.handle_stash_metadata_scan()
elif self.path == '/api/stash/tasks/scan':
self.handle_stash_task_scan()
elif self.path == '/api/stash/tasks/identify':
self.handle_stash_task_identify()
elif self.path == '/api/stash/tasks/clean':
self.handle_stash_task_clean()
elif self.path == '/api/stash/tasks/generate':
self.handle_stash_task_generate()
elif self.path == '/api/stash/tasks/stop':
self.handle_stash_task_stop()
elif self.path == '/api/stash/performers/find-images':
self.handle_stash_find_images()
elif self.path == '/api/stash/performers/commit-image':
@@ -1895,7 +1907,222 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
except Exception as e:
self.send_json_response(500, {"error": str(e)})
# ==================== Stash Library Metadata Scan Trigger ====================
# ==================== Stash Task & Tool Control Handlers ====================
def handle_stash_tasks_jobs(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 = '''
query JobQueue {
jobQueue {
id
status
subTasks
description
progress
startTime
endTime
addTime
error
}
}
'''
try:
res = execute_stash_graphql(query, stash_url, timeout=10)
jobs = res.get('data', {}).get('jobQueue') or []
self.send_json_response(200, {"jobs": jobs, "count": len(jobs)})
except Exception as e:
self.send_json_response(500, {"error": str(e), "jobs": []})
def handle_stash_task_scan(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
paths = payload.get('paths', [])
rescan = payload.get('rescan', False)
scanGenerateCovers = payload.get('scanGenerateCovers', True)
scanGeneratePreviews = payload.get('scanGeneratePreviews', False)
scanGenerateSprites = payload.get('scanGenerateSprites', False)
scanGeneratePhashes = payload.get('scanGeneratePhashes', False)
mutation = '''
mutation MetadataScan($input: ScanMetadataInput!) {
metadataScan(input: $input)
}
'''
variables = {
"input": {
"paths": paths if paths else None,
"rescan": bool(rescan),
"scanGenerateCovers": bool(scanGenerateCovers),
"scanGeneratePreviews": bool(scanGeneratePreviews),
"scanGenerateSprites": bool(scanGenerateSprites),
"scanGeneratePhashes": bool(scanGeneratePhashes)
}
}
res = execute_stash_graphql(mutation, stash_url, variables=variables, timeout=15)
self.send_json_response(200, {"success": True, "result": res.get('data', {}).get('metadataScan')})
except Exception as e:
self.send_json_response(500, {"error": str(e)})
def handle_stash_task_identify(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
paths = payload.get('paths', [])
mode = payload.get('mode', 'autotag')
if mode == 'autotag':
mutation = '''
mutation MetadataAutoTag($input: AutoTagMetadataInput!) {
metadataAutoTag(input: $input)
}
'''
variables = {
"input": {
"paths": paths if paths else None,
"performers": bool(payload.get('performers', True)),
"studios": bool(payload.get('studios', True)),
"tags": bool(payload.get('tags', True))
}
}
res = execute_stash_graphql(mutation, stash_url, variables=variables, timeout=15)
self.send_json_response(200, {"success": True, "job_id": res.get('data', {}).get('metadataAutoTag')})
else:
mutation = '''
mutation MetadataIdentify($input: IdentifyMetadataInput!) {
metadataIdentify(input: $input)
}
'''
variables = {
"input": {
"paths": paths if paths else None,
"options": {
"setCoverImage": True,
"setOrganized": True
}
}
}
res = execute_stash_graphql(mutation, stash_url, variables=variables, timeout=15)
self.send_json_response(200, {"success": True, "job_id": res.get('data', {}).get('metadataIdentify')})
except Exception as e:
self.send_json_response(500, {"error": str(e)})
def handle_stash_task_clean(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
clean_type = payload.get('clean_type', 'metadata')
dry_run = bool(payload.get('dry_run', False))
paths = payload.get('paths', [])
results = {}
if clean_type in ['metadata', 'both']:
mutation = '''
mutation MetadataClean($input: CleanMetadataInput!) {
metadataClean(input: $input)
}
'''
variables = {
"input": {
"dryRun": dry_run,
"paths": paths if paths else None
}
}
res = execute_stash_graphql(mutation, stash_url, variables=variables, timeout=15)
results['metadataClean'] = res.get('data', {}).get('metadataClean')
if clean_type in ['generated', 'both']:
mutation_gen = '''
mutation MetadataCleanGenerated($input: CleanGeneratedInput!) {
metadataCleanGenerated(input: $input)
}
'''
variables_gen = {
"input": {
"dryRun": dry_run,
"blobFiles": True,
"sprites": True,
"screenshots": True,
"transcodes": True,
"markers": True,
"imageThumbnails": True
}
}
res_gen = execute_stash_graphql(mutation_gen, stash_url, variables=variables_gen, timeout=15)
results['metadataCleanGenerated'] = res_gen.get('data', {}).get('metadataCleanGenerated')
self.send_json_response(200, {"success": True, "results": results})
except Exception as e:
self.send_json_response(500, {"error": str(e)})
def handle_stash_task_generate(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
mutation = '''
mutation MetadataGenerate($input: GenerateMetadataInput!) {
metadataGenerate(input: $input)
}
'''
variables = {
"input": {
"covers": bool(payload.get('covers', True)),
"sprites": bool(payload.get('sprites', True)),
"previews": bool(payload.get('previews', True)),
"imagePreviews": bool(payload.get('imagePreviews', True)),
"markers": bool(payload.get('markers', True)),
"transcodes": bool(payload.get('transcodes', False)),
"phashes": bool(payload.get('phashes', True)),
"overwrite": bool(payload.get('overwrite', False)),
"paths": payload.get('paths') if payload.get('paths') else None
}
}
res = execute_stash_graphql(mutation, stash_url, variables=variables, timeout=15)
self.send_json_response(200, {"success": True, "job_id": res.get('data', {}).get('metadataGenerate')})
except Exception as e:
self.send_json_response(500, {"error": str(e)})
def handle_stash_task_stop(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
job_id = payload.get('job_id')
stop_all = payload.get('stop_all', False)
if stop_all or not job_id:
mutation = '''
mutation StopAllJobs {
stopAllJobs
}
'''
res = execute_stash_graphql(mutation, stash_url, timeout=10)
self.send_json_response(200, {"success": True, "result": res.get('data', {}).get('stopAllJobs')})
else:
mutation = '''
mutation StopJob($job_id: ID!) {
stopJob(job_id: $job_id)
}
'''
res = execute_stash_graphql(mutation, stash_url, variables={"job_id": str(job_id)}, timeout=10)
self.send_json_response(200, {"success": True, "result": res.get('data', {}).get('stopJob')})
except Exception as e:
self.send_json_response(500, {"error": str(e)})
def handle_stash_metadata_scan(self):
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length)