feat: Add 1-Click Batch Merge All for Performer and Tag Duplicates
This commit is contained in:
@@ -2064,10 +2064,13 @@
|
|||||||
Detect similarly-named performers (e.g. "cory.chase" vs "Cory Chase") and merge profiles while combining all scenes, tags, and images into the primary performer.
|
Detect similarly-named performers (e.g. "cory.chase" vs "Cory Chase") and merge profiles while combining all scenes, tags, and images into the primary performer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
|
||||||
<button class="btn btn-secondary" id="btn-scan-performer-dupes" style="font-size: 0.8rem;">
|
<button class="btn btn-secondary" id="btn-scan-performer-dupes" style="font-size: 0.8rem;">
|
||||||
🔍 Find Performer Duplicates
|
🔍 Find Performer Duplicates
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-primary" id="btn-batch-merge-performers" style="font-size: 0.8rem; display: none; background: linear-gradient(135deg, #a855f7, #7e22ce);">
|
||||||
|
⚡ Batch Merge All (<span id="performer-batch-count">0</span>)
|
||||||
|
</button>
|
||||||
<span class="badge" id="performer-dupe-count-badge" style="background: rgba(168, 85, 247, 0.15); color: var(--accent-purple); border-color: rgba(168, 85, 247, 0.3); font-size: 0.75rem;">
|
<span class="badge" id="performer-dupe-count-badge" style="background: rgba(168, 85, 247, 0.15); color: var(--accent-purple); border-color: rgba(168, 85, 247, 0.3); font-size: 0.75rem;">
|
||||||
0 duplicate clusters
|
0 duplicate clusters
|
||||||
</span>
|
</span>
|
||||||
@@ -2136,10 +2139,13 @@
|
|||||||
Find duplicate tags with casing/dash/dot variations and merge them into canonical tags.
|
Find duplicate tags with casing/dash/dot variations and merge them into canonical tags.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
|
||||||
<button class="btn btn-secondary" id="btn-scan-tag-dupes" style="font-size: 0.8rem;">
|
<button class="btn btn-secondary" id="btn-scan-tag-dupes" style="font-size: 0.8rem;">
|
||||||
🔍 Find Tag Duplicates
|
🔍 Find Tag Duplicates
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-primary" id="btn-batch-merge-tags" style="font-size: 0.8rem; display: none; background: linear-gradient(135deg, #06b6d4, #0891b2);">
|
||||||
|
⚡ Batch Merge All (<span id="tag-batch-count">0</span>)
|
||||||
|
</button>
|
||||||
<span class="badge" id="tag-dupe-count-badge" style="background: rgba(6, 182, 212, 0.15); color: var(--accent-cyan); border-color: rgba(6, 182, 212, 0.3); font-size: 0.75rem;">
|
<span class="badge" id="tag-dupe-count-badge" style="background: rgba(6, 182, 212, 0.15); color: var(--accent-cyan); border-color: rgba(6, 182, 212, 0.3); font-size: 0.75rem;">
|
||||||
0 tag clusters
|
0 tag clusters
|
||||||
</span>
|
</span>
|
||||||
@@ -7239,6 +7245,8 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
|
|
||||||
async function scanPerformerDuplicates() {
|
async function scanPerformerDuplicates() {
|
||||||
const btn = document.getElementById('btn-scan-performer-dupes');
|
const btn = document.getElementById('btn-scan-performer-dupes');
|
||||||
|
const batchBtn = document.getElementById('btn-batch-merge-performers');
|
||||||
|
const batchCount = document.getElementById('performer-batch-count');
|
||||||
const container = document.getElementById('performer-dupes-container');
|
const container = document.getElementById('performer-dupes-container');
|
||||||
const badge = document.getElementById('performer-dupe-count-badge');
|
const badge = document.getElementById('performer-dupe-count-badge');
|
||||||
|
|
||||||
@@ -7248,7 +7256,7 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div style="padding: 2rem; text-align: center; color: var(--text-muted);">
|
<div style="padding: 2rem; text-align: center; color: var(--text-muted);">
|
||||||
<div class="empty-state-icon">⏳</div>
|
<div class="empty-state-icon">⏳</div>
|
||||||
<p>Scanning Stash performer library for name variations and duplicates...</p>
|
<p>Scanning Stash performer library for name variations...</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -7260,6 +7268,15 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
|
|
||||||
if (badge) badge.textContent = `${performerDuplicateClusters.length} duplicate clusters (${data.total_performers || 0} total performers)`;
|
if (badge) badge.textContent = `${performerDuplicateClusters.length} duplicate clusters (${data.total_performers || 0} total performers)`;
|
||||||
|
|
||||||
|
if (batchBtn && batchCount) {
|
||||||
|
if (performerDuplicateClusters.length > 0) {
|
||||||
|
batchCount.textContent = performerDuplicateClusters.length;
|
||||||
|
batchBtn.style.display = 'inline-flex';
|
||||||
|
} else {
|
||||||
|
batchBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderPerformerDuplicates(performerDuplicateClusters);
|
renderPerformerDuplicates(performerDuplicateClusters);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(`Performer scan error: ${err.message}`, 'error');
|
showToast(`Performer scan error: ${err.message}`, 'error');
|
||||||
@@ -7392,9 +7409,51 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeBatchPerformerMerge() {
|
||||||
|
if (!performerDuplicateClusters || performerDuplicateClusters.length === 0) {
|
||||||
|
showToast('No performer duplicate clusters to merge', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalClusters = performerDuplicateClusters.length;
|
||||||
|
if (!confirm(`Batch merge ALL ${totalClusters} duplicate performer clusters into their primary profiles?\n\nThis will transfer all linked scenes, tags, and gallery images to the primary performer profile for each cluster.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('btn-batch-merge-performers');
|
||||||
|
try {
|
||||||
|
if (btn) { btn.disabled = true; btn.textContent = `Merging ${totalClusters} clusters...`; }
|
||||||
|
|
||||||
|
const payloadClusters = performerDuplicateClusters.map(c => ({
|
||||||
|
destination_id: c.primary.id,
|
||||||
|
source_ids: c.candidates.map(cand => cand.id)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const res = await fetch('/api/stash/performers/batch-merge', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ clusters: payloadClusters })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
showToast(`✓ Batch merged ${data.merged_count} performer clusters!`, 'success');
|
||||||
|
await scanPerformerDuplicates();
|
||||||
|
} else {
|
||||||
|
showToast(`Batch merge error: ${data.error}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`Batch merge failed: ${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
if (btn) { btn.disabled = false; btn.textContent = `⚡ Batch Merge All (${performerDuplicateClusters.length})`; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const btnScanPerformerDupes = document.getElementById('btn-scan-performer-dupes');
|
const btnScanPerformerDupes = document.getElementById('btn-scan-performer-dupes');
|
||||||
if (btnScanPerformerDupes) btnScanPerformerDupes.addEventListener('click', scanPerformerDuplicates);
|
if (btnScanPerformerDupes) btnScanPerformerDupes.addEventListener('click', scanPerformerDuplicates);
|
||||||
|
|
||||||
|
const btnBatchMergePerformers = document.getElementById('btn-batch-merge-performers');
|
||||||
|
if (btnBatchMergePerformers) btnBatchMergePerformers.addEventListener('click', executeBatchPerformerMerge);
|
||||||
|
|
||||||
// ==================== SECTION B: MISSING METADATA RADAR ====================
|
// ==================== SECTION B: MISSING METADATA RADAR ====================
|
||||||
let currentRadarType = 'studio';
|
let currentRadarType = 'studio';
|
||||||
|
|
||||||
@@ -7505,6 +7564,8 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
|
|
||||||
async function scanTagDuplicates() {
|
async function scanTagDuplicates() {
|
||||||
const btn = document.getElementById('btn-scan-tag-dupes');
|
const btn = document.getElementById('btn-scan-tag-dupes');
|
||||||
|
const batchBtn = document.getElementById('btn-batch-merge-tags');
|
||||||
|
const batchCount = document.getElementById('tag-batch-count');
|
||||||
const container = document.getElementById('tag-dupes-container');
|
const container = document.getElementById('tag-dupes-container');
|
||||||
const badge = document.getElementById('tag-dupe-count-badge');
|
const badge = document.getElementById('tag-dupe-count-badge');
|
||||||
|
|
||||||
@@ -7526,6 +7587,15 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
|
|
||||||
if (badge) badge.textContent = `${tagDuplicateClusters.length} tag clusters (${data.total_tags || 0} total tags)`;
|
if (badge) badge.textContent = `${tagDuplicateClusters.length} tag clusters (${data.total_tags || 0} total tags)`;
|
||||||
|
|
||||||
|
if (batchBtn && batchCount) {
|
||||||
|
if (tagDuplicateClusters.length > 0) {
|
||||||
|
batchCount.textContent = tagDuplicateClusters.length;
|
||||||
|
batchBtn.style.display = 'inline-flex';
|
||||||
|
} else {
|
||||||
|
batchBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderTagDuplicates(tagDuplicateClusters);
|
renderTagDuplicates(tagDuplicateClusters);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(`Tag scan error: ${err.message}`, 'error');
|
showToast(`Tag scan error: ${err.message}`, 'error');
|
||||||
@@ -7631,9 +7701,51 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeBatchTagMerge() {
|
||||||
|
if (!tagDuplicateClusters || tagDuplicateClusters.length === 0) {
|
||||||
|
showToast('No tag duplicate clusters to merge', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalClusters = tagDuplicateClusters.length;
|
||||||
|
if (!confirm(`Batch merge ALL ${totalClusters} duplicate tag clusters into their primary tags?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('btn-batch-merge-tags');
|
||||||
|
try {
|
||||||
|
if (btn) { btn.disabled = true; btn.textContent = `Merging ${totalClusters} tags...`; }
|
||||||
|
|
||||||
|
const payloadClusters = tagDuplicateClusters.map(c => ({
|
||||||
|
destination_id: c.primary.id,
|
||||||
|
source_ids: c.candidates.map(cand => cand.id)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const res = await fetch('/api/stash/tags/batch-merge', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ clusters: payloadClusters })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
showToast(`✓ Batch merged ${data.merged_count} tag clusters!`, 'success');
|
||||||
|
await scanTagDuplicates();
|
||||||
|
} else {
|
||||||
|
showToast(`Batch merge error: ${data.error}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`Batch merge failed: ${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
if (btn) { btn.disabled = false; btn.textContent = `⚡ Batch Merge All (${tagDuplicateClusters.length})`; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const btnScanTagDupes = document.getElementById('btn-scan-tag-dupes');
|
const btnScanTagDupes = document.getElementById('btn-scan-tag-dupes');
|
||||||
if (btnScanTagDupes) btnScanTagDupes.addEventListener('click', scanTagDuplicates);
|
if (btnScanTagDupes) btnScanTagDupes.addEventListener('click', scanTagDuplicates);
|
||||||
|
|
||||||
|
const btnBatchMergeTags = document.getElementById('btn-batch-merge-tags');
|
||||||
|
if (btnBatchMergeTags) btnBatchMergeTags.addEventListener('click', executeBatchTagMerge);
|
||||||
|
|
||||||
// ==================== SECTION D: TRANSCODE CORRELATOR & STORAGE SAVER ====================
|
// ==================== SECTION D: TRANSCODE CORRELATOR & STORAGE SAVER ====================
|
||||||
let transcodeItems = [];
|
let transcodeItems = [];
|
||||||
let selectedTranscodeHashes = new Set();
|
let selectedTranscodeHashes = new Set();
|
||||||
@@ -8007,8 +8119,10 @@ Return ONLY a valid JSON object with an array named "results":
|
|||||||
{ title: 'Stash: Start Library Scan', icon: '🚀', action: () => { switchTab('stash-tasks'); executeStashScanTask(); } },
|
{ 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: Identify & Auto-Tag Scenes', icon: '🔍', action: () => { switchTab('stash-tasks'); executeStashIdentifyTask(); } },
|
||||||
{ title: 'Stash: Find Performer Duplicates & Merge', icon: '👥', action: () => { switchTab('stash-tasks'); scanPerformerDuplicates(); } },
|
{ title: 'Stash: Find Performer Duplicates & Merge', icon: '👥', action: () => { switchTab('stash-tasks'); scanPerformerDuplicates(); } },
|
||||||
|
{ title: 'Stash: Batch Merge All Performer Duplicates', icon: '⚡', action: () => { switchTab('stash-tasks'); executeBatchPerformerMerge(); } },
|
||||||
{ title: 'Stash: Missing Metadata Radar', icon: '📡', action: () => { switchTab('stash-tasks'); loadMissingMetadataRadar(); } },
|
{ title: 'Stash: Missing Metadata Radar', icon: '📡', action: () => { switchTab('stash-tasks'); loadMissingMetadataRadar(); } },
|
||||||
{ title: 'Stash: Tag Normalizer & Deduplication', icon: '🏷️', action: () => { switchTab('stash-tasks'); scanTagDuplicates(); } },
|
{ title: 'Stash: Tag Normalizer & Deduplication', icon: '🏷️', action: () => { switchTab('stash-tasks'); scanTagDuplicates(); } },
|
||||||
|
{ title: 'Stash: Batch Merge All Tag Duplicates', icon: '⚡', action: () => { switchTab('stash-tasks'); executeBatchTagMerge(); } },
|
||||||
{ title: 'Stash: Correlate Transcodes & Replace Originals', icon: '🔀', action: () => { switchTab('stash-tasks'); scanStashTranscodes(); } },
|
{ title: 'Stash: Correlate Transcodes & Replace Originals', icon: '🔀', action: () => { switchTab('stash-tasks'); scanStashTranscodes(); } },
|
||||||
{ title: 'Stash: Clean Database & Generated Files', icon: '🧹', action: () => { switchTab('stash-tasks'); executeStashCleanTask(); } },
|
{ title: 'Stash: Clean Database & Generated Files', icon: '🧹', action: () => { switchTab('stash-tasks'); executeStashCleanTask(); } },
|
||||||
{ title: 'Stash: Generate Previews & Sprites', icon: '⚡', action: () => { switchTab('stash-tasks'); executeStashGenerateTask(); } },
|
{ title: 'Stash: Generate Previews & Sprites', icon: '⚡', action: () => { switchTab('stash-tasks'); executeStashGenerateTask(); } },
|
||||||
|
|||||||
@@ -884,8 +884,12 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
|
|||||||
self.handle_stash_batch_commit_images()
|
self.handle_stash_batch_commit_images()
|
||||||
elif self.path == '/api/stash/performers/merge':
|
elif self.path == '/api/stash/performers/merge':
|
||||||
self.handle_stash_performer_merge()
|
self.handle_stash_performer_merge()
|
||||||
|
elif self.path == '/api/stash/performers/batch-merge':
|
||||||
|
self.handle_stash_performer_batch_merge()
|
||||||
elif self.path == '/api/stash/tags/merge':
|
elif self.path == '/api/stash/tags/merge':
|
||||||
self.handle_stash_tag_merge()
|
self.handle_stash_tag_merge()
|
||||||
|
elif self.path == '/api/stash/tags/batch-merge':
|
||||||
|
self.handle_stash_tag_batch_merge()
|
||||||
elif self.path == '/api/stash/transcodes/replace':
|
elif self.path == '/api/stash/transcodes/replace':
|
||||||
self.handle_stash_transcode_replace()
|
self.handle_stash_transcode_replace()
|
||||||
elif self.path == '/api/stash/transcodes/batch-replace':
|
elif self.path == '/api/stash/transcodes/batch-replace':
|
||||||
@@ -2435,6 +2439,51 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.send_json_response(500, {"error": str(e)})
|
self.send_json_response(500, {"error": str(e)})
|
||||||
|
|
||||||
|
def handle_stash_performer_batch_merge(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
|
||||||
|
clusters = payload.get('clusters', [])
|
||||||
|
|
||||||
|
if not clusters:
|
||||||
|
self.send_json_response(400, {"error": "No clusters provided for batch merge"})
|
||||||
|
return
|
||||||
|
|
||||||
|
merged_count = 0
|
||||||
|
errors = []
|
||||||
|
mutation = '''
|
||||||
|
mutation PerformerMerge($input: PerformerMergeInput!) {
|
||||||
|
performerMerge(input: $input) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
for c in clusters:
|
||||||
|
source_ids = [str(sid) for sid in c.get('source_ids', [])]
|
||||||
|
dest_id = str(c.get('destination_id'))
|
||||||
|
if not source_ids or not dest_id:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
res = execute_stash_graphql(mutation, stash_url, variables={"input": {"source": source_ids, "destination": dest_id}}, timeout=20)
|
||||||
|
if res.get('data', {}).get('performerMerge'):
|
||||||
|
merged_count += 1
|
||||||
|
elif res.get('errors'):
|
||||||
|
errors.append({"destination_id": dest_id, "error": str(res.get('errors'))})
|
||||||
|
except Exception as ex:
|
||||||
|
errors.append({"destination_id": dest_id, "error": str(ex)})
|
||||||
|
|
||||||
|
self.send_json_response(200, {
|
||||||
|
"success": True,
|
||||||
|
"merged_count": merged_count,
|
||||||
|
"total_requested": len(clusters),
|
||||||
|
"errors": errors
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
self.send_json_response(500, {"error": str(e)})
|
||||||
|
|
||||||
# ==================== Missing Metadata Radar Handlers ====================
|
# ==================== Missing Metadata Radar Handlers ====================
|
||||||
def handle_stash_missing_metadata(self):
|
def handle_stash_missing_metadata(self):
|
||||||
parsed = urllib.parse.urlparse(self.path)
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
@@ -2618,6 +2667,51 @@ class RobustProxyHandler(http.server.SimpleHTTPRequestHandler):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.send_json_response(500, {"error": str(e)})
|
self.send_json_response(500, {"error": str(e)})
|
||||||
|
|
||||||
|
def handle_stash_tag_batch_merge(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
|
||||||
|
clusters = payload.get('clusters', [])
|
||||||
|
|
||||||
|
if not clusters:
|
||||||
|
self.send_json_response(400, {"error": "No clusters provided for batch merge"})
|
||||||
|
return
|
||||||
|
|
||||||
|
merged_count = 0
|
||||||
|
errors = []
|
||||||
|
mutation = '''
|
||||||
|
mutation TagsMerge($input: TagsMergeInput!) {
|
||||||
|
tagsMerge(input: $input) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
for c in clusters:
|
||||||
|
source_ids = [str(sid) for sid in c.get('source_ids', [])]
|
||||||
|
dest_id = str(c.get('destination_id'))
|
||||||
|
if not source_ids or not dest_id:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
res = execute_stash_graphql(mutation, stash_url, variables={"input": {"source": source_ids, "destination": dest_id}}, timeout=20)
|
||||||
|
if res.get('data', {}).get('tagsMerge'):
|
||||||
|
merged_count += 1
|
||||||
|
elif res.get('errors'):
|
||||||
|
errors.append({"destination_id": dest_id, "error": str(res.get('errors'))})
|
||||||
|
except Exception as ex:
|
||||||
|
errors.append({"destination_id": dest_id, "error": str(ex)})
|
||||||
|
|
||||||
|
self.send_json_response(200, {
|
||||||
|
"success": True,
|
||||||
|
"merged_count": merged_count,
|
||||||
|
"total_requested": len(clusters),
|
||||||
|
"errors": errors
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
self.send_json_response(500, {"error": str(e)})
|
||||||
|
|
||||||
# ==================== Stash Transcode Correlator & Storage Saver Handlers ====================
|
# ==================== Stash Transcode Correlator & Storage Saver Handlers ====================
|
||||||
def handle_stash_transcode_scan(self):
|
def handle_stash_transcode_scan(self):
|
||||||
parsed = urllib.parse.urlparse(self.path)
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
|
|||||||
Reference in New Issue
Block a user