feat: Add torrent and SMB file deletion with confirmation and batch delete
This commit is contained in:
@@ -1171,13 +1171,14 @@
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary" id="btn-stop-analysis" style="display: none; background: #7f1d1d; border-color: #991b1b; color: #fff;">
|
||||
⛔ Stop Analysis
|
||||
</button>
|
||||
|
||||
<button class="btn btn-success" id="btn-batch-relocate" disabled>
|
||||
⛔ Stop Analy <button class="btn btn-success" id="btn-batch-relocate" disabled>
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
|
||||
Approve & Move Selected Torrents
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary" id="btn-batch-delete-torrents" disabled style="color: var(--accent-rose); border-color: rgba(244, 63, 94, 0.4); background: rgba(244, 63, 94, 0.08);" title="Delete selected torrents and delete their downloaded files from disk">
|
||||
🗑️ Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="search-filter">
|
||||
@@ -1251,7 +1252,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMB Actions Control Bar -->
|
||||
<!-- Actions Control Bar -->
|
||||
<div class="controls-bar">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" id="btn-fetch-smb">
|
||||
@@ -1280,6 +1281,11 @@
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg>
|
||||
Approve & Move Selected Files
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary" id="btn-smb-batch-delete" disabled style="color: var(--accent-rose); border-color: rgba(244, 63, 94, 0.4); background: rgba(244, 63, 94, 0.08);" title="Permanently delete selected files from SMB share">
|
||||
🗑️ Delete Selected
|
||||
</button>
|
||||
</div> </button>
|
||||
</div>
|
||||
|
||||
<div class="search-filter">
|
||||
@@ -1593,6 +1599,7 @@
|
||||
const btnAnalyzeAll = document.getElementById('btn-ai-analyze-all');
|
||||
const btnStopAnalysis = document.getElementById('btn-stop-analysis');
|
||||
const btnBatchRelocate = document.getElementById('btn-batch-relocate');
|
||||
const btnBatchDeleteTorrents = document.getElementById('btn-batch-delete-torrents');
|
||||
const progressBarContainer = document.getElementById('global-progress-container');
|
||||
const progressBarFill = document.getElementById('global-progress-fill');
|
||||
|
||||
@@ -1610,6 +1617,7 @@
|
||||
const btnSmbAnalyzeAll = document.getElementById('btn-smb-analyze-all');
|
||||
const btnSmbStopAnalysis = document.getElementById('btn-smb-stop-analysis');
|
||||
const btnSmbBatchRelocate = document.getElementById('btn-smb-batch-relocate');
|
||||
const btnSmbBatchDelete = document.getElementById('btn-smb-batch-delete');
|
||||
const smbProgressBarContainer = document.getElementById('smb-progress-container');
|
||||
const smbProgressBarFill = document.getElementById('smb-progress-fill');
|
||||
|
||||
@@ -1845,6 +1853,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteItemFromDb(itemKey) {
|
||||
delete dbCache.all[itemKey];
|
||||
dbStatusText.textContent = `SQLite: ${Object.keys(dbCache.all).length} Cached`;
|
||||
try {
|
||||
const isServingFromProxy = window.location.protocol.startsWith('http') && window.location.port === '8000';
|
||||
const endpoint = isServingFromProxy ? '/api/db/delete' : 'http://localhost:8000/api/db/delete';
|
||||
await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ item_key: itemKey })
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('Failed deleting item from SQLite DB:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load saved settings
|
||||
function loadConfig() {
|
||||
const raw = localStorage.getItem(STORAGE_KEY) || localStorage.getItem('qb_performer_sorter_cfg_v3');
|
||||
@@ -3006,6 +3030,87 @@ Return ONLY valid JSON matching:
|
||||
updateButtonStates();
|
||||
});
|
||||
|
||||
async function deleteTorrent(torrent, confirmPrompt = true) {
|
||||
if (confirmPrompt) {
|
||||
const ok = confirm(`⚠️ PERMANENT DELETE:\n\nAre you sure you want to permanently delete "${torrent.name}" AND ALL ITS DOWNLOADED FILES from disk?`);
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
torrent.moveStatus = 'moving';
|
||||
renderRow(torrent.hash);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('hashes', torrent.hash);
|
||||
params.append('deleteFiles', 'true');
|
||||
|
||||
const res = await qbRequest('/api/v2/torrents/delete', { method: 'POST', body: params });
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
torrent.moveStatus = 'error';
|
||||
torrent.errorMessage = 'Failed delete: ' + text;
|
||||
renderRow(torrent.hash);
|
||||
throw new Error('Failed to delete torrent: ' + text);
|
||||
}
|
||||
|
||||
await deleteItemFromDb(torrent.hash);
|
||||
qbState.torrents = qbState.torrents.filter(t => t.hash !== torrent.hash);
|
||||
qbState.selectedHashes.delete(torrent.hash);
|
||||
qbTabBadge.textContent = qbState.torrents.length;
|
||||
renderTable();
|
||||
updateStats();
|
||||
showToast(`Deleted ${torrent.name} and downloaded files from disk`, 'success');
|
||||
}
|
||||
|
||||
if (btnBatchDeleteTorrents) {
|
||||
btnBatchDeleteTorrents.addEventListener('click', async () => {
|
||||
const selected = qbState.torrents.filter(t => qbState.selectedHashes.has(t.hash));
|
||||
if (selected.length === 0) {
|
||||
showToast('Please select at least one torrent checkbox to delete', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = confirm(`⚠️ PERMANENT BATCH DELETE:\n\nAre you sure you want to permanently delete ${selected.length} selected torrent(s) AND ALL THEIR DOWNLOADED FILES from disk?\n\nThis cannot be undone.`);
|
||||
if (!ok) return;
|
||||
|
||||
btnBatchDeleteTorrents.disabled = true;
|
||||
btnBatchDeleteTorrents.textContent = 'Deleting...';
|
||||
progressBarContainer.style.display = 'block';
|
||||
progressBarFill.style.width = '0%';
|
||||
|
||||
try {
|
||||
const hashes = selected.map(t => t.hash).join('|');
|
||||
const params = new URLSearchParams();
|
||||
params.append('hashes', hashes);
|
||||
params.append('deleteFiles', 'true');
|
||||
|
||||
const res = await qbRequest('/api/v2/torrents/delete', { method: 'POST', body: params });
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error('Failed deleting torrents from qBittorrent: ' + text);
|
||||
}
|
||||
|
||||
for (const t of selected) {
|
||||
await deleteItemFromDb(t.hash);
|
||||
}
|
||||
|
||||
const deletedHashSet = new Set(selected.map(t => t.hash));
|
||||
qbState.torrents = qbState.torrents.filter(t => !deletedHashSet.has(t.hash));
|
||||
qbState.selectedHashes.clear();
|
||||
qbTabBadge.textContent = qbState.torrents.length;
|
||||
renderTable();
|
||||
updateStats();
|
||||
showToast(`Permanently deleted ${selected.length} torrent(s) and files from disk`, 'success');
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btnBatchDeleteTorrents.disabled = false;
|
||||
btnBatchDeleteTorrents.textContent = '🗑️ Delete Selected';
|
||||
setTimeout(() => { progressBarContainer.style.display = 'none'; }, 1000);
|
||||
updateButtonStates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
statTotal.textContent = qbState.torrents.length;
|
||||
statAnalyzed.textContent = qbState.torrents.filter(t => t.analysis).length;
|
||||
@@ -3024,6 +3129,7 @@ Return ONLY valid JSON matching:
|
||||
btnAnalyzeSelected.disabled = !hasSelection || qbState.isAnalyzing;
|
||||
btnAnalyzeAll.disabled = !hasTorrents || qbState.isAnalyzing;
|
||||
btnBatchRelocate.disabled = !hasReadySelected || qbState.isMoving;
|
||||
if (btnBatchDeleteTorrents) btnBatchDeleteTorrents.disabled = !hasSelection || qbState.isMoving;
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
@@ -3150,6 +3256,7 @@ Return ONLY valid JSON matching:
|
||||
${t.analysis && t.moveStatus !== 'moved' ? `
|
||||
<button class="btn btn-secondary btn-sm btn-single-analyze" data-hash="${t.hash}" title="Re-run AI analysis">Re-run</button>
|
||||
` : ''}
|
||||
<button type="button" class="btn btn-secondary btn-sm btn-single-delete-torrent" data-hash="${t.hash}" title="Permanently delete torrent and downloaded files from disk" style="color: var(--accent-rose); border-color: rgba(244, 63, 94, 0.3); padding: 0.25rem 0.5rem;">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align: center; white-space: nowrap; min-width: 105px;">
|
||||
@@ -3258,6 +3365,14 @@ Return ONLY valid JSON matching:
|
||||
});
|
||||
});
|
||||
|
||||
root.querySelectorAll('.btn-single-delete-torrent').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const hash = btn.getAttribute('data-hash');
|
||||
const t = qbState.torrents.find(item => item.hash === hash);
|
||||
if (t) await deleteTorrent(t, true);
|
||||
});
|
||||
});
|
||||
|
||||
root.querySelectorAll('.view-files-link').forEach(link => {
|
||||
link.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -3754,6 +3869,95 @@ Return ONLY a valid JSON object with an array named "results":
|
||||
updateSmbButtonStates();
|
||||
});
|
||||
|
||||
async function deleteSmbFile(fileItem, confirmPrompt = true) {
|
||||
if (confirmPrompt) {
|
||||
const ok = confirm(`⚠️ PERMANENT DELETE:\n\nAre you sure you want to permanently delete "${fileItem.name}" from the SMB share?`);
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
fileItem.moveStatus = 'moving';
|
||||
renderSmbRow(fileItem.path);
|
||||
|
||||
const isServingFromProxy = window.location.protocol.startsWith('http') && window.location.port === '8000';
|
||||
const endpoint = isServingFromProxy ? '/api/smb/delete' : 'http://localhost:8000/api/smb/delete';
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: fileItem.path })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errData = await res.json();
|
||||
fileItem.moveStatus = 'error';
|
||||
fileItem.errorMessage = 'Failed delete: ' + (errData.error || `HTTP ${res.status}`);
|
||||
renderSmbRow(fileItem.path);
|
||||
throw new Error(errData.error || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
await deleteItemFromDb(fileItem.path);
|
||||
smbState.files = smbState.files.filter(f => f.path !== fileItem.path);
|
||||
smbState.selectedPaths.delete(fileItem.path);
|
||||
smbTabBadge.textContent = smbState.files.length;
|
||||
renderSmbTable();
|
||||
updateSmbStats();
|
||||
showToast(`Deleted ${fileItem.name} from SMB share`, 'success');
|
||||
}
|
||||
|
||||
if (btnSmbBatchDelete) {
|
||||
btnSmbBatchDelete.addEventListener('click', async () => {
|
||||
const selected = smbState.files.filter(f => smbState.selectedPaths.has(f.path));
|
||||
if (selected.length === 0) {
|
||||
showToast('Please select at least one SMB file to delete', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = confirm(`⚠️ PERMANENT BATCH DELETE:\n\nAre you sure you want to permanently delete ${selected.length} selected video file(s) from the SMB share?\n\nThis cannot be undone.`);
|
||||
if (!ok) return;
|
||||
|
||||
btnSmbBatchDelete.disabled = true;
|
||||
btnSmbBatchDelete.textContent = 'Deleting...';
|
||||
smbProgressBarContainer.style.display = 'block';
|
||||
smbProgressBarFill.style.width = '0%';
|
||||
|
||||
try {
|
||||
const paths = selected.map(f => f.path);
|
||||
const isServingFromProxy = window.location.protocol.startsWith('http') && window.location.port === '8000';
|
||||
const endpoint = isServingFromProxy ? '/api/smb/delete' : 'http://localhost:8000/api/smb/delete';
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paths: paths })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errData = await res.json();
|
||||
throw new Error(errData.error || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
for (const f of selected) {
|
||||
await deleteItemFromDb(f.path);
|
||||
}
|
||||
|
||||
const deletedPathSet = new Set(paths);
|
||||
smbState.files = smbState.files.filter(f => !deletedPathSet.has(f.path));
|
||||
smbState.selectedPaths.clear();
|
||||
smbTabBadge.textContent = smbState.files.length;
|
||||
renderSmbTable();
|
||||
updateSmbStats();
|
||||
showToast(`Permanently deleted ${selected.length} video file(s) from SMB share`, 'success');
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
} finally {
|
||||
btnSmbBatchDelete.disabled = false;
|
||||
btnSmbBatchDelete.textContent = '🗑️ Delete Selected';
|
||||
setTimeout(() => { smbProgressBarContainer.style.display = 'none'; }, 1000);
|
||||
updateSmbButtonStates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateSmbStats() {
|
||||
smbStatTotal.textContent = smbState.files.length;
|
||||
smbStatAnalyzed.textContent = smbState.files.filter(f => f.analysis).length;
|
||||
@@ -3772,6 +3976,7 @@ Return ONLY a valid JSON object with an array named "results":
|
||||
btnSmbAnalyzeSelected.disabled = !hasSelection || smbState.isAnalyzing;
|
||||
btnSmbAnalyzeAll.disabled = !hasFiles || smbState.isAnalyzing;
|
||||
btnSmbBatchRelocate.disabled = !hasReadySelected || smbState.isMoving;
|
||||
if (btnSmbBatchDelete) btnSmbBatchDelete.disabled = !hasSelection || smbState.isMoving;
|
||||
}
|
||||
|
||||
function renderSmbTable() {
|
||||
@@ -3893,6 +4098,7 @@ Return ONLY a valid JSON object with an array named "results":
|
||||
${f.analysis && f.moveStatus !== 'moved' ? `
|
||||
<button class="btn btn-secondary btn-sm btn-smb-single-analyze" data-path="${escapeHtml(f.path)}" title="Re-run AI analysis">Re-run</button>
|
||||
` : ''}
|
||||
<button type="button" class="btn btn-secondary btn-sm btn-smb-single-delete" data-path="${escapeHtml(f.path)}" title="Permanently delete file from SMB share" style="color: var(--accent-rose); border-color: rgba(244, 63, 94, 0.3); padding: 0.25rem 0.5rem;">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -3924,6 +4130,14 @@ Return ONLY a valid JSON object with an array named "results":
|
||||
});
|
||||
});
|
||||
|
||||
root.querySelectorAll('.btn-smb-single-delete').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const path = btn.getAttribute('data-path');
|
||||
const f = smbState.files.find(item => item.path === path);
|
||||
if (f) await deleteSmbFile(f, true);
|
||||
});
|
||||
});
|
||||
|
||||
// Video Preview Button / Title Click
|
||||
root.querySelectorAll('.btn-open-video-preview').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
@@ -4153,6 +4367,9 @@ Return ONLY a valid JSON object with an array named "results":
|
||||
` : `
|
||||
<span class="badge badge-success" style="font-size: 0.85rem; padding: 0.4rem 0.8rem;">✓ Moved Successfully</span>
|
||||
`}
|
||||
<button type="button" class="btn btn-secondary" id="btn-modal-delete-now" style="color: var(--accent-rose); border-color: rgba(244,63,94,0.4); background: rgba(244,63,94,0.08);" title="Permanently delete this video and files from disk">
|
||||
🗑️ Delete Video
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4172,6 +4389,21 @@ Return ONLY a valid JSON object with an array named "results":
|
||||
`;
|
||||
|
||||
// Event listeners inside modal
|
||||
const btnDeleteNow = videoPerformerBox.querySelector('#btn-modal-delete-now');
|
||||
if (btnDeleteNow) {
|
||||
btnDeleteNow.addEventListener('click', async () => {
|
||||
try {
|
||||
if (type === 'smb') {
|
||||
await deleteSmbFile(item, true);
|
||||
} else {
|
||||
await deleteTorrent(item, true);
|
||||
}
|
||||
closeVideoPreview();
|
||||
} catch (e) {
|
||||
showToast(`Delete failed: ${e.message}`, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
videoPerformerBox.querySelectorAll('.modal-performer-pill').forEach(pill => {
|
||||
pill.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -583,22 +583,25 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
|
||||
else:
|
||||
super().do_GET()
|
||||
|
||||
def do_POST(self):
|
||||
# Database Cache Save APIs
|
||||
# Database Cache Save/Delete APIs
|
||||
if self.path == '/api/db/save':
|
||||
self.handle_db_save()
|
||||
elif self.path == '/api/db/batch-save':
|
||||
self.handle_db_batch_save()
|
||||
elif self.path == '/api/db/delete':
|
||||
self.handle_db_delete()
|
||||
# Stash Lookup APIs
|
||||
elif self.path == '/api/stash/lookup':
|
||||
self.handle_stash_lookup()
|
||||
elif self.path == '/api/stash/batch-lookup':
|
||||
self.handle_stash_batch_lookup()
|
||||
# SMB API Moves
|
||||
# SMB API Moves & Deletes
|
||||
elif self.path == '/api/smb/move':
|
||||
self.handle_smb_move()
|
||||
elif self.path == '/api/smb/batch-move':
|
||||
self.handle_smb_batch_move()
|
||||
elif self.path == '/api/smb/delete':
|
||||
self.handle_smb_delete()
|
||||
elif self.path.startswith('/qb-proxy/'):
|
||||
self.proxy_qb_request('POST')
|
||||
elif self.path.startswith('/anthropic-proxy/'):
|
||||
@@ -783,6 +786,43 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": f"Failed batch saving to DB: {str(e)}"}).encode('utf-8'))
|
||||
|
||||
def handle_db_delete(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'))
|
||||
item_key = payload.get('item_key')
|
||||
item_keys = payload.get('item_keys', [])
|
||||
if item_key and item_key not in item_keys:
|
||||
item_keys.append(item_key)
|
||||
|
||||
if not item_keys:
|
||||
self.send_response(400)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_cors_headers()
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": "Missing item_key or item_keys"}).encode('utf-8'))
|
||||
return
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
for key in item_keys:
|
||||
cursor.execute("DELETE FROM performer_analysis_cache WHERE item_key = ? OR name = ?", (key, key))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_cors_headers()
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"success": True, "deleted_keys": item_keys}).encode('utf-8'))
|
||||
except Exception as e:
|
||||
self.send_response(500)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_cors_headers()
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": f"Failed deleting from DB: {str(e)}"}).encode('utf-8'))
|
||||
|
||||
# ==================== Video Streaming Handlers (Range 206) ====================
|
||||
def handle_stream_video(self):
|
||||
head_only = False
|
||||
@@ -1217,6 +1257,65 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": f"Batch move failed: {str(e)}"}).encode('utf-8'))
|
||||
|
||||
def handle_smb_delete(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'))
|
||||
path = payload.get('path')
|
||||
paths = payload.get('paths', [])
|
||||
if path and path not in paths:
|
||||
paths.append(path)
|
||||
|
||||
if not paths:
|
||||
self.send_response(400)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_cors_headers()
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": "Missing path or paths"}).encode('utf-8'))
|
||||
return
|
||||
|
||||
deleted = []
|
||||
errors = []
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
for p in paths:
|
||||
try:
|
||||
resolved = resolve_stream_video_path(p) or p
|
||||
if os.path.exists(resolved):
|
||||
if os.path.isdir(resolved):
|
||||
shutil.rmtree(resolved)
|
||||
else:
|
||||
os.remove(resolved)
|
||||
deleted.append(p)
|
||||
cursor.execute("DELETE FROM performer_analysis_cache WHERE item_key = ? OR name = ?", (p, os.path.basename(p)))
|
||||
else:
|
||||
errors.append(f"File not found: {p}")
|
||||
except Exception as del_err:
|
||||
errors.append(f"Failed deleting {p}: {str(del_err)}")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_cors_headers()
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({
|
||||
"success": len(deleted) > 0,
|
||||
"deleted": deleted,
|
||||
"deleted_count": len(deleted),
|
||||
"errors": errors
|
||||
}).encode('utf-8'))
|
||||
|
||||
except Exception as e:
|
||||
self.send_response(500)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_cors_headers()
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": f"Delete request failed: {str(e)}"}).encode('utf-8'))
|
||||
|
||||
# ==================== External API Proxies ====================
|
||||
def proxy_external_api(self, upstream_base, proxy_prefix, method='POST'):
|
||||
upstream_base = upstream_base.rstrip('/')
|
||||
|
||||
Reference in New Issue
Block a user