From e354b5265243828db5ce0341a2afacab4f14a3c6 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 18 Aug 2026 08:45:52 -0400 Subject: [PATCH] feat: Add Tab 5 for Stash Task & Tool Control (Start Scan, Identify Scenes, Clean, Generate) with live Job Queue Monitor --- qbittorrent_performer_sorter.html | 560 +++++++++++++++++++++++++++++- server.py | 233 ++++++++++++- 2 files changed, 788 insertions(+), 5 deletions(-) diff --git a/qbittorrent_performer_sorter.html b/qbittorrent_performer_sorter.html index 370708a..7f99bfa 100644 --- a/qbittorrent_performer_sorter.html +++ b/qbittorrent_performer_sorter.html @@ -1119,6 +1119,11 @@ Relocation History & Analytics 0 + @@ -1748,6 +1753,227 @@ + + +
+ +
+
+
+ + โšก Stash Live Job Queue & Task Monitor +
+
+ + +
+
+ +
+
+ No background tasks currently running in Stash. Ready to start tasks below. +
+
+
+ + +
+ + +
+
+ ๐Ÿš€ +
+

1. Start Library Scan

+

Detect new scenes, files, and update database

+
+
+ +
+ + + Path relative to Stash server +
+ +
+ + + + + +
+ +
+ +
+
+ + +
+
+ ๐Ÿ” +
+

2. Identify Scenes

+

Scrape StashDB, match titles, tag performers & studios

+
+
+ +
+ + +
+ +
+ + +
+ +
+ + + + +
+ +
+ +
+
+ + +
+
+ ๐Ÿงน +
+

3. Clean Metadata & Content

+

Prune missing database files & orphaned generated caches

+
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+ + +
+
+ โšก +
+

4. Generate Media & Previews

+

Batch create scrubber previews, sprites, markers & transcodes

+
+
+ +
+ + +
+ +
+ + + + + + +
+ +
+ +
+ +
+ +
+
+ +
+
@@ -2230,7 +2456,7 @@ ] }; - // DOM Elements - Tab 3 Stash Images & Tab 4 History + // DOM Elements - Tab 3 Stash Images, Tab 4 History & Tab 5 Stash Tasks const tabBtnStashImages = document.getElementById('tab-btn-stash-images'); const tabPanelStashImages = document.getElementById('tab-panel-stash-images'); const stashImagesTabBadge = document.getElementById('stash-images-tab-badge'); @@ -2239,23 +2465,43 @@ const tabPanelHistory = document.getElementById('tab-panel-history'); const historyTabBadge = document.getElementById('history-tab-badge'); + const tabBtnStashTasks = document.getElementById('tab-btn-stash-tasks'); + const tabPanelStashTasks = document.getElementById('tab-panel-stash-tasks'); + const stashTasksTabBadge = document.getElementById('stash-tasks-tab-badge'); + + let stashJobsPollingInterval = null; + // Tab Switching Logic function switchTab(tabName) { tabBtnQb.classList.toggle('active', tabName === 'qb'); tabBtnSmb.classList.toggle('active', tabName === 'smb'); if (tabBtnStashImages) tabBtnStashImages.classList.toggle('active', tabName === 'stash-images'); if (tabBtnHistory) tabBtnHistory.classList.toggle('active', tabName === 'history'); + if (tabBtnStashTasks) tabBtnStashTasks.classList.toggle('active', tabName === 'stash-tasks'); tabPanelQb.classList.toggle('active', tabName === 'qb'); tabPanelSmb.classList.toggle('active', tabName === 'smb'); if (tabPanelStashImages) tabPanelStashImages.classList.toggle('active', tabName === 'stash-images'); if (tabPanelHistory) tabPanelHistory.classList.toggle('active', tabName === 'history'); + if (tabPanelStashTasks) tabPanelStashTasks.classList.toggle('active', tabName === 'stash-tasks'); if (tabName === 'history') { loadRelocationHistory(); loadStorageAnalytics(); } + if (tabName === 'stash-tasks') { + pollStashJobs(); + if (!stashJobsPollingInterval) { + stashJobsPollingInterval = setInterval(pollStashJobs, 3000); + } + } else { + if (stashJobsPollingInterval) { + clearInterval(stashJobsPollingInterval); + stashJobsPollingInterval = null; + } + } + localStorage.setItem('qb_active_tab', tabName); } @@ -2263,6 +2509,7 @@ tabBtnSmb.addEventListener('click', () => switchTab('smb')); if (tabBtnStashImages) tabBtnStashImages.addEventListener('click', () => switchTab('stash-images')); if (tabBtnHistory) tabBtnHistory.addEventListener('click', () => switchTab('history')); + if (tabBtnStashTasks) tabBtnStashTasks.addEventListener('click', () => switchTab('stash-tasks')); function getProviderDisplayName() { const provider = aiProviderSelect ? aiProviderSelect.value : 'local'; @@ -6209,6 +6456,310 @@ Return ONLY a valid JSON object with an array named "results": }); } + // ==================== TAB 5: STASH TASKS & TOOL CONTROL ==================== + let activeStashJobs = []; + + async function pollStashJobs() { + try { + const res = await fetch('/api/stash/tasks/jobs'); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + activeStashJobs = data.jobs || []; + renderStashJobs(activeStashJobs); + } catch (err) { + console.warn('Failed polling Stash jobs:', err); + } + } + + function renderStashJobs(jobs) { + const container = document.getElementById('stash-active-jobs-container'); + const badge = document.getElementById('stash-tasks-tab-badge'); + const pulseDot = document.getElementById('stash-task-pulse-dot'); + + const runningJobs = jobs.filter(j => j.status === 'RUNNING' || j.status === 'QUEUED' || j.status === 'STOPPING'); + + if (badge) { + if (runningJobs.length > 0) { + badge.textContent = `${runningJobs.length} Running`; + badge.style.background = 'rgba(6, 182, 212, 0.2)'; + badge.style.color = '#67e8f9'; + badge.style.borderColor = 'rgba(6, 182, 212, 0.4)'; + if (pulseDot) pulseDot.style.background = 'var(--accent-cyan)'; + } else { + badge.textContent = 'Ready'; + badge.style.background = 'rgba(16, 185, 129, 0.2)'; + badge.style.color = '#6ee7b7'; + badge.style.borderColor = 'rgba(16, 185, 129, 0.4)'; + if (pulseDot) pulseDot.style.background = 'var(--accent-green)'; + } + } + + if (!container) return; + + if (!jobs || jobs.length === 0) { + container.innerHTML = ` +
+ No background tasks currently active in Stash. Select any tool below to launch. +
+ `; + return; + } + + container.innerHTML = jobs.map(job => { + const isRunning = job.status === 'RUNNING'; + const isQueued = job.status === 'QUEUED'; + const isStopping = job.status === 'STOPPING'; + const isFinished = job.status === 'FINISHED'; + const isFailed = job.status === 'FAILED'; + const isCancelled = job.status === 'CANCELLED'; + + const pct = Math.round((job.progress || 0) * 100); + const subTasksText = (job.subTasks && job.subTasks.length) ? job.subTasks.join(' โ€ข ') : ''; + + let badgeClass = 'primary'; + if (isFinished) badgeClass = 'success'; + if (isFailed) badgeClass = 'danger'; + if (isStopping || isCancelled) badgeClass = 'amber'; + + return ` +
+
+
+ ${isRunning ? 'โš™๏ธ' : isFinished ? 'โœ…' : isFailed ? 'โŒ' : 'โณ'} + ${escapeHtml(job.description || 'Stash Task')} + ${job.status} +
+
+ ${pct}% + ${(isRunning || isQueued) ? ` + + ` : ''} +
+
+ + +
+
+
+ + ${subTasksText ? `
${escapeHtml(subTasksText)}
` : ''} + ${job.error ? `
${escapeHtml(job.error)}
` : ''} +
+ `; + }).join(''); + + container.querySelectorAll('.btn-stop-single-job').forEach(btn => { + btn.addEventListener('click', async () => { + const jid = btn.getAttribute('data-job-id'); + await executeStashStopJob(jid); + }); + }); + } + + async function executeStashScanTask() { + const btn = document.getElementById('btn-exec-stash-scan'); + const pathVal = document.getElementById('stash-scan-path')?.value.trim(); + const rescan = document.getElementById('stash-scan-opt-rescan')?.checked; + const covers = document.getElementById('stash-scan-opt-covers')?.checked; + const previews = document.getElementById('stash-scan-opt-previews')?.checked; + const sprites = document.getElementById('stash-scan-opt-sprites')?.checked; + const phashes = document.getElementById('stash-scan-opt-phashes')?.checked; + + const paths = pathVal ? [pathVal] : []; + + try { + btn.disabled = true; + btn.textContent = 'Launching Scan...'; + const res = await fetch('/api/stash/tasks/scan', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + paths, + rescan, + scanGenerateCovers: covers, + scanGeneratePreviews: previews, + scanGenerateSprites: sprites, + scanGeneratePhashes: phashes + }) + }); + const data = await res.json(); + if (data.success) { + showToast('๐Ÿš€ Stash library scan launched successfully!', 'success'); + await pollStashJobs(); + } else { + showToast(`Scan launch failed: ${data.error}`, 'error'); + } + } catch (err) { + showToast(`Scan error: ${err.message}`, 'error'); + } finally { + btn.disabled = false; + btn.textContent = '๐Ÿš€ Start Library Scan'; + } + } + + async function executeStashIdentifyTask() { + const btn = document.getElementById('btn-exec-stash-identify'); + const mode = document.getElementById('stash-identify-mode')?.value || 'autotag'; + const pathVal = document.getElementById('stash-identify-path')?.value.trim(); + const performers = document.getElementById('stash-identify-opt-performers')?.checked; + const studios = document.getElementById('stash-identify-opt-studios')?.checked; + const tags = document.getElementById('stash-identify-opt-tags')?.checked; + const paths = pathVal ? [pathVal] : []; + + try { + btn.disabled = true; + btn.textContent = 'Launching Identify...'; + const res = await fetch('/api/stash/tasks/identify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + mode, + paths, + performers, + studios, + tags + }) + }); + const data = await res.json(); + if (data.success) { + showToast('๐Ÿ” Stash scene identify/auto-tag task launched!', 'success'); + await pollStashJobs(); + } else { + showToast(`Identify launch failed: ${data.error}`, 'error'); + } + } catch (err) { + showToast(`Identify error: ${err.message}`, 'error'); + } finally { + btn.disabled = false; + btn.textContent = '๐Ÿ” Start Scene Identification'; + } + } + + async function executeStashCleanTask() { + const btn = document.getElementById('btn-exec-stash-clean'); + const cleanType = document.getElementById('stash-clean-type')?.value || 'metadata'; + const dryRun = document.getElementById('stash-clean-opt-dryrun')?.checked; + const pathVal = document.getElementById('stash-clean-path')?.value.trim(); + const paths = pathVal ? [pathVal] : []; + + if (!dryRun && !confirm('Are you sure you want to run Clean? Deleted database records or generated caches cannot be undone.')) return; + + try { + btn.disabled = true; + btn.textContent = 'Running Clean...'; + const res = await fetch('/api/stash/tasks/clean', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + clean_type: cleanType, + dry_run: dryRun, + paths + }) + }); + const data = await res.json(); + if (data.success) { + showToast(`๐Ÿงน Stash Clean task launched ${dryRun ? '(DRY RUN mode)' : ''}!`, 'success'); + await pollStashJobs(); + } else { + showToast(`Clean launch failed: ${data.error}`, 'error'); + } + } catch (err) { + showToast(`Clean error: ${err.message}`, 'error'); + } finally { + btn.disabled = false; + btn.textContent = '๐Ÿงน Execute Clean Task'; + } + } + + async function executeStashGenerateTask() { + const btn = document.getElementById('btn-exec-stash-generate'); + const pathVal = document.getElementById('stash-generate-path')?.value.trim(); + const covers = document.getElementById('stash-gen-opt-covers')?.checked; + const previews = document.getElementById('stash-gen-opt-previews')?.checked; + const sprites = document.getElementById('stash-gen-opt-sprites')?.checked; + const markers = document.getElementById('stash-gen-opt-markers')?.checked; + const phashes = document.getElementById('stash-gen-opt-phashes')?.checked; + const transcodes = document.getElementById('stash-gen-opt-transcodes')?.checked; + const overwrite = document.getElementById('stash-gen-opt-overwrite')?.checked; + const paths = pathVal ? [pathVal] : []; + + try { + btn.disabled = true; + btn.textContent = 'Launching Generation...'; + const res = await fetch('/api/stash/tasks/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + covers, + previews, + sprites, + imagePreviews: true, + markers, + phashes, + transcodes, + overwrite, + paths + }) + }); + const data = await res.json(); + if (data.success) { + showToast('โšก Stash media generation task started!', 'success'); + await pollStashJobs(); + } else { + showToast(`Generate launch failed: ${data.error}`, 'error'); + } + } catch (err) { + showToast(`Generate error: ${err.message}`, 'error'); + } finally { + btn.disabled = false; + btn.textContent = 'โšก Start Generation Task'; + } + } + + async function executeStashStopJob(jobId, stopAll = false) { + try { + const res = await fetch('/api/stash/tasks/stop', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ job_id: jobId, stop_all: stopAll }) + }); + const data = await res.json(); + if (data.success) { + showToast(stopAll ? 'โ›” All Stash jobs stopped.' : 'โ›” Job cancellation requested.', 'info'); + await pollStashJobs(); + } + } catch (err) { + showToast(`Stop job error: ${err.message}`, 'error'); + } + } + + // Attach Tab 5 Listeners + const btnRefreshStashJobs = document.getElementById('btn-refresh-stash-jobs'); + if (btnRefreshStashJobs) btnRefreshStashJobs.addEventListener('click', pollStashJobs); + + const btnStopAllStashJobs = document.getElementById('btn-stop-all-stash-jobs'); + if (btnStopAllStashJobs) { + btnStopAllStashJobs.addEventListener('click', () => { + if (confirm('Cancel and abort all active/queued Stash tasks?')) { + executeStashStopJob(null, true); + } + }); + } + + const btnExecStashScan = document.getElementById('btn-exec-stash-scan'); + if (btnExecStashScan) btnExecStashScan.addEventListener('click', executeStashScanTask); + + const btnExecStashIdentify = document.getElementById('btn-exec-stash-identify'); + if (btnExecStashIdentify) btnExecStashIdentify.addEventListener('click', executeStashIdentifyTask); + + const btnExecStashClean = document.getElementById('btn-exec-stash-clean'); + if (btnExecStashClean) btnExecStashClean.addEventListener('click', executeStashCleanTask); + + const btnExecStashGenerate = document.getElementById('btn-exec-stash-generate'); + if (btnExecStashGenerate) btnExecStashGenerate.addEventListener('click', executeStashGenerateTask); + // ==================== COMMAND PALETTE (CTRL + K) ==================== const cmdPaletteModal = document.getElementById('cmd-palette-modal'); const cmdPaletteInput = document.getElementById('cmd-palette-input'); @@ -6219,10 +6770,14 @@ Return ONLY a valid JSON object with an array named "results": { title: 'Switch to SMB Video Sorter', icon: '๐Ÿ“‚', action: () => switchTab('smb') }, { title: 'Switch to Stash Image Review', icon: '๐Ÿ–ผ๏ธ', action: () => switchTab('stash-images') }, { title: 'Switch to Relocation History & Analytics', icon: '๐Ÿ“œ', action: () => switchTab('history') }, + { title: 'Switch to Stash Tasks & Tool Control', icon: 'โšก', action: () => switchTab('stash-tasks') }, + { title: 'Stash: Start Library Scan', icon: '๐Ÿš€', action: () => { switchTab('stash-tasks'); executeStashScanTask(); } }, + { title: 'Stash: Identify & Auto-Tag Scenes', icon: '๐Ÿ”', action: () => { switchTab('stash-tasks'); executeStashIdentifyTask(); } }, + { title: 'Stash: Clean Database & Generated Files', icon: '๐Ÿงน', action: () => { switchTab('stash-tasks'); executeStashCleanTask(); } }, + { title: 'Stash: Generate Previews & Sprites', icon: 'โšก', action: () => { switchTab('stash-tasks'); executeStashGenerateTask(); } }, { title: 'Scan SMB Files (/videodownloader)', icon: '๐Ÿ”', action: () => { switchTab('smb'); document.getElementById('btn-fetch-smb')?.click(); } }, { title: 'Scan Stash Missing Performer Images', icon: 'โšก', action: () => { switchTab('stash-images'); document.getElementById('btn-scan-stash-images')?.click(); } }, { title: 'Scan Zero-Byte & Empty Folders', icon: '๐Ÿงน', action: () => scanSmbCleanup() }, - { title: 'Trigger Stash Library Metadata Scan', icon: 'โšก', action: () => triggerStashLibraryScan() }, { title: 'Export qBittorrent Plan to CSV', icon: '๐Ÿ“ฅ', action: () => document.getElementById('btn-export-qb-plan')?.click() }, { title: 'Export SMB Plan to CSV', icon: '๐Ÿ“ฅ', action: () => document.getElementById('btn-export-smb-plan')?.click() } ]; @@ -6309,6 +6864,7 @@ Return ONLY a valid JSON object with an array named "results": await checkStashStatus(); await loadExistingPerformers(); loadRelocationHistory(); + pollStashJobs(); if (window.location.protocol.startsWith('http')) { try { await qbLogin(); diff --git a/server.py b/server.py index ee25f1c..301035f 100644 --- a/server.py +++ b/server.py @@ -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)