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:
@@ -1119,6 +1119,11 @@
|
||||
<span>Relocation History & Analytics</span>
|
||||
<span class="tab-badge" id="history-tab-badge" style="background: rgba(168, 85, 247, 0.2); color: #d8b4fe; border-color: rgba(168, 85, 247, 0.4);">0</span>
|
||||
</button>
|
||||
<button class="tab-btn" id="tab-btn-stash-tasks">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||
<span>Stash Tasks & Tool Control</span>
|
||||
<span class="tab-badge" id="stash-tasks-tab-badge" style="background: rgba(16, 185, 129, 0.2); color: #6ee7b7; border-color: rgba(16, 185, 129, 0.4);">Ready</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Unified Configuration Card (Collapsible - Collapsed by default) -->
|
||||
@@ -1748,6 +1753,227 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== TAB 5: Stash Tasks & Tool Control ==================== -->
|
||||
<div class="tab-panel" id="tab-panel-stash-tasks">
|
||||
<!-- Live Task Queue & Job Monitor Card -->
|
||||
<div class="table-container" style="padding: 1.25rem; margin-bottom: 1.5rem; border-color: rgba(16, 185, 129, 0.4); background: rgba(15, 23, 42, 0.75);">
|
||||
<div class="table-header-bar" style="margin-bottom: 0.75rem;">
|
||||
<div class="table-title" style="display: flex; align-items: center; gap: 0.5rem; color: var(--accent-green);">
|
||||
<span class="status-indicator online" id="stash-task-pulse-dot" style="background: var(--accent-green);"></span>
|
||||
<span>⚡ Stash Live Job Queue & Task Monitor</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<button class="btn btn-secondary" id="btn-refresh-stash-jobs" style="padding: 0.25rem 0.6rem; font-size: 0.75rem;">
|
||||
🔄 Refresh Jobs
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="btn-stop-all-stash-jobs" style="padding: 0.25rem 0.6rem; font-size: 0.75rem; color: var(--accent-rose); border-color: rgba(244, 63, 94, 0.4); background: rgba(244, 63, 94, 0.08);" title="Cancel and abort all active/queued Stash background tasks">
|
||||
⛔ Stop All Jobs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="stash-active-jobs-container">
|
||||
<div style="color: var(--text-muted); font-size: 0.8rem; padding: 0.5rem 0;">
|
||||
No background tasks currently running in Stash. Ready to start tasks below.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4 Core Tool Modules Grid -->
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 1.25rem; margin-bottom: 1.5rem;">
|
||||
|
||||
<!-- CARD 1: Start Scan -->
|
||||
<div class="table-container" style="padding: 1.25rem; display: flex; flex-direction: column; gap: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.6rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.75rem;">
|
||||
<span style="font-size: 1.3rem;">🚀</span>
|
||||
<div>
|
||||
<h3 style="font-size: 0.95rem; font-weight: 700; color: #fff;">1. Start Library Scan</h3>
|
||||
<p style="font-size: 0.72rem; color: var(--text-muted);">Detect new scenes, files, and update database</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="stash-scan-path">Target Scan Path(s)</label>
|
||||
<input type="text" class="form-control" id="stash-scan-path" value="/isolation/videos" placeholder="Leave empty for all library paths or e.g. /isolation/videos" />
|
||||
<span style="font-size: 0.68rem; color: var(--text-muted);">Path relative to Stash server</span>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; background: rgba(0,0,0,0.2); padding: 0.75rem; border-radius: var(--radius-sm); border: 1px solid var(--border-color);">
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-scan-opt-rescan" />
|
||||
<span>Force Rescan All</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-scan-opt-covers" checked />
|
||||
<span>Generate Covers</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-scan-opt-previews" />
|
||||
<span>Generate Previews</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-scan-opt-sprites" />
|
||||
<span>Generate Sprites</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-scan-opt-phashes" checked />
|
||||
<span>Generate pHashes</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: auto; padding-top: 0.5rem;">
|
||||
<button class="btn btn-primary" id="btn-exec-stash-scan" style="width: 100%; justify-content: center; font-weight: 600;">
|
||||
🚀 Start Library Scan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARD 2: Identify Scenes -->
|
||||
<div class="table-container" style="padding: 1.25rem; display: flex; flex-direction: column; gap: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.6rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.75rem;">
|
||||
<span style="font-size: 1.3rem;">🔍</span>
|
||||
<div>
|
||||
<h3 style="font-size: 0.95rem; font-weight: 700; color: #fff;">2. Identify Scenes</h3>
|
||||
<p style="font-size: 0.72rem; color: var(--text-muted);">Scrape StashDB, match titles, tag performers & studios</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="stash-identify-mode">Identification Engine</label>
|
||||
<select class="form-control" id="stash-identify-mode">
|
||||
<option value="autotag">⚡ Auto-Tag (Filename & Metadata Tagger)</option>
|
||||
<option value="identify">🌐 StashDB Scraper Identify (Fingerprint / pHash)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="stash-identify-path">Target Path(s)</label>
|
||||
<input type="text" class="form-control" id="stash-identify-path" value="/isolation/videos" placeholder="Leave empty for all paths or e.g. /isolation/videos" />
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; background: rgba(0,0,0,0.2); padding: 0.75rem; border-radius: var(--radius-sm); border: 1px solid var(--border-color);">
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-identify-opt-performers" checked />
|
||||
<span>Tag Performers</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-identify-opt-studios" checked />
|
||||
<span>Tag Studios</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-identify-opt-tags" checked />
|
||||
<span>Tag Categories</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-identify-opt-covers" checked />
|
||||
<span>Fetch Covers</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: auto; padding-top: 0.5rem;">
|
||||
<button class="btn btn-primary" id="btn-exec-stash-identify" style="width: 100%; justify-content: center; font-weight: 600; background: linear-gradient(135deg, #0284c7, #2563eb);">
|
||||
🔍 Start Scene Identification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARD 3: Clean -->
|
||||
<div class="table-container" style="padding: 1.25rem; display: flex; flex-direction: column; gap: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.6rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.75rem;">
|
||||
<span style="font-size: 1.3rem;">🧹</span>
|
||||
<div>
|
||||
<h3 style="font-size: 0.95rem; font-weight: 700; color: #fff;">3. Clean Metadata & Content</h3>
|
||||
<p style="font-size: 0.72rem; color: var(--text-muted);">Prune missing database files & orphaned generated caches</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="stash-clean-type">Clean Operation Scope</label>
|
||||
<select class="form-control" id="stash-clean-type">
|
||||
<option value="metadata">📁 Clean Missing Files (Remove DB entries with missing disk files)</option>
|
||||
<option value="generated">🎞️ Clean Orphaned Generated Content (Thumbnails/Sprites/Transcodes)</option>
|
||||
<option value="both">⚡ Clean Both (Missing Files + Orphaned Generated Content)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="stash-clean-path">Path Filter</label>
|
||||
<input type="text" class="form-control" id="stash-clean-path" placeholder="Optional: Path to clean or leave empty for all" />
|
||||
</div>
|
||||
|
||||
<div style="background: rgba(0,0,0,0.2); padding: 0.75rem; border-radius: var(--radius-sm); border: 1px solid var(--border-color);">
|
||||
<label style="font-size: 0.75rem; color: var(--accent-amber); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer; font-weight: 600;">
|
||||
<input type="checkbox" id="stash-clean-opt-dryrun" />
|
||||
<span>🧪 Dry Run Mode (Simulate without deleting records)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: auto; padding-top: 0.5rem;">
|
||||
<button class="btn btn-secondary" id="btn-exec-stash-clean" style="width: 100%; justify-content: center; font-weight: 600; color: var(--accent-amber); border-color: rgba(245, 158, 11, 0.4);">
|
||||
🧹 Execute Clean Task
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARD 4: Generate -->
|
||||
<div class="table-container" style="padding: 1.25rem; display: flex; flex-direction: column; gap: 1rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.6rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.75rem;">
|
||||
<span style="font-size: 1.3rem;">⚡</span>
|
||||
<div>
|
||||
<h3 style="font-size: 0.95rem; font-weight: 700; color: #fff;">4. Generate Media & Previews</h3>
|
||||
<p style="font-size: 0.72rem; color: var(--text-muted);">Batch create scrubber previews, sprites, markers & transcodes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="stash-generate-path">Target Path(s)</label>
|
||||
<input type="text" class="form-control" id="stash-generate-path" value="/isolation/videos" placeholder="Leave empty for all paths or e.g. /isolation/videos" />
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; background: rgba(0,0,0,0.2); padding: 0.75rem; border-radius: var(--radius-sm); border: 1px solid var(--border-color);">
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-gen-opt-covers" checked />
|
||||
<span>🖼️ Covers</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-gen-opt-previews" checked />
|
||||
<span>🎞️ Video Previews</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-gen-opt-sprites" checked />
|
||||
<span>📸 Sprites</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-gen-opt-markers" checked />
|
||||
<span>🏷️ Markers & Snaps</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-gen-opt-phashes" checked />
|
||||
<span>🔢 pHashes</span>
|
||||
</label>
|
||||
<label style="font-size: 0.75rem; color: var(--text-main); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-gen-opt-transcodes" />
|
||||
<span>🎬 Transcodes</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="font-size: 0.75rem; color: var(--text-muted); display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;">
|
||||
<input type="checkbox" id="stash-gen-opt-overwrite" />
|
||||
<span>Overwrite existing generated assets</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: auto; padding-top: 0.5rem;">
|
||||
<button class="btn btn-success" id="btn-exec-stash-generate" style="width: 100%; justify-content: center; font-weight: 600;">
|
||||
⚡ Start Generation Task
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Command Palette Modal (Ctrl + K) -->
|
||||
@@ -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 = `
|
||||
<div style="color: var(--text-muted); font-size: 0.8rem; padding: 0.5rem 0;">
|
||||
No background tasks currently active in Stash. Select any tool below to launch.
|
||||
</div>
|
||||
`;
|
||||
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 `
|
||||
<div style="background: rgba(30, 41, 59, 0.7); border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 0.85rem; margin-bottom: 0.6rem; display: flex; flex-direction: column; gap: 0.45rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span style="font-size: 1.1rem;">${isRunning ? '⚙️' : isFinished ? '✅' : isFailed ? '❌' : '⏳'}</span>
|
||||
<span style="font-weight: 600; color: #fff; font-size: 0.85rem;">${escapeHtml(job.description || 'Stash Task')}</span>
|
||||
<span class="badge badge-${badgeClass}" style="font-size: 0.68rem; text-transform: uppercase;">${job.status}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 0.6rem;">
|
||||
<span style="font-family: var(--font-mono); font-size: 0.75rem; color: var(--accent-cyan);">${pct}%</span>
|
||||
${(isRunning || isQueued) ? `
|
||||
<button class="btn btn-secondary btn-stop-single-job" data-job-id="${job.id}" style="padding: 0.2rem 0.5rem; font-size: 0.7rem; color: var(--accent-rose); border-color: rgba(244,63,94,0.4);" title="Cancel this job">
|
||||
⛔ Stop
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div style="background: rgba(0,0,0,0.35); border-radius: 999px; height: 6px; overflow: hidden; width: 100%;">
|
||||
<div style="background: ${isFailed ? 'var(--accent-rose)' : isFinished ? 'var(--accent-green)' : 'linear-gradient(90deg, var(--accent-cyan), var(--accent-purple))'}; width: ${pct}%; height: 100%; transition: width 0.3s ease;"></div>
|
||||
</div>
|
||||
|
||||
${subTasksText ? `<div style="font-size: 0.7rem; color: var(--text-muted); font-family: var(--font-mono);">${escapeHtml(subTasksText)}</div>` : ''}
|
||||
${job.error ? `<div style="font-size: 0.72rem; color: var(--accent-rose);">${escapeHtml(job.error)}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user