Fix: Make torrent fetching instant and non-blocking with ultra-fast batched Stash GraphQL subqueries

This commit is contained in:
david
2026-08-17 09:51:15 -04:00
parent 73a8ebe163
commit 8fa9572dea
2 changed files with 70 additions and 44 deletions
+16 -14
View File
@@ -2627,19 +2627,20 @@
qbTabBadge.textContent = qbState.torrents.length;
renderTable();
updateStats();
showToast(`Loaded ${qbState.torrents.length} torrents (${cachedLoadedCount} restored from SQLite cache)`, 'success');
// Auto Stash lookup
// Auto Stash lookup in background (non-blocking)
if (stashAutoLookupSelect && stashAutoLookupSelect.value === 'true') {
const unanalyzed = qbState.torrents.filter(t => !t.analysis && t.moveStatus !== 'moved');
if (unanalyzed.length > 0) {
const stashMatches = await matchItemsWithStash(unanalyzed, 'qb');
if (stashMatches > 0) {
showToast(`⚡ Stash matched ${stashMatches} / ${unanalyzed.length} torrents!`, 'success');
}
setTimeout(async () => {
const stashMatches = await matchItemsWithStash(unanalyzed, 'qb');
if (stashMatches > 0) {
showToast(`⚡ Stash matched ${stashMatches} / ${unanalyzed.length} torrents!`, 'success');
}
}, 50);
}
}
showToast(`Loaded ${qbState.torrents.length} torrents (${cachedLoadedCount} restored from SQLite cache)`, 'success');
} catch (err) {
showToast(err.message, 'error');
} finally {
@@ -3321,19 +3322,20 @@ Return ONLY valid JSON matching:
smbTabBadge.textContent = smbState.files.length;
renderSmbTable();
updateSmbStats();
showToast(`Scanned ${smbState.files.length} SMB video files (${cachedLoadedCount} restored from SQLite cache)`, 'success');
// Auto Stash lookup
// Auto Stash lookup in background (non-blocking)
if (stashAutoLookupSelect && stashAutoLookupSelect.value === 'true') {
const unanalyzed = smbState.files.filter(f => !f.analysis && f.moveStatus !== 'moved');
if (unanalyzed.length > 0) {
const stashMatches = await matchItemsWithStash(unanalyzed, 'smb');
if (stashMatches > 0) {
showToast(`⚡ Stash matched ${stashMatches} / ${unanalyzed.length} video files!`, 'success');
}
setTimeout(async () => {
const stashMatches = await matchItemsWithStash(unanalyzed, 'smb');
if (stashMatches > 0) {
showToast(`⚡ Stash matched ${stashMatches} / ${unanalyzed.length} video files!`, 'success');
}
}, 50);
}
}
showToast(`Scanned ${smbState.files.length} SMB video files (${cachedLoadedCount} restored from SQLite cache)`, 'success');
} catch (err) {
showToast(`SMB scan failed: ${err.message}`, 'error');
} finally {
+54 -30
View File
@@ -322,40 +322,64 @@ def query_stash_batch(items, stash_url=None):
if not items:
return []
queries = []
for idx, item in enumerate(items):
file_path = item.get('path') or item.get('name')
basename = os.path.basename(file_path)
p = f'/data/videodownloader/{basename}'
queries.append(f'item_{idx}: findScenes(scene_filter: {{path: {{value: {json.dumps(p)}, modifier: EQUALS}}}}) {{ scenes {{ id title performers {{ name gender }} studio {{ name }} }} }}')
batch_q = '{\n' + '\n'.join(queries) + '\n}'
results = [None] * len(items)
unmatched_indices = []
try:
res = execute_stash_graphql(batch_q, stash_url, timeout=6)
data = res.get('data', {})
for idx in range(len(items)):
scenes = data.get(f'item_{idx}', {}).get('scenes', [])
if scenes and scenes[0].get('performers'):
parsed = parse_stash_scene_data(scenes[0], 'exact_path')
if parsed:
results[idx] = parsed
if not results[idx]:
unmatched_indices.append(idx)
except Exception as e:
print(f"Batch exact Stash query failed: {e}")
unmatched_indices = list(range(len(items)))
# 1. First pass: Batch exact path queries in chunks of 30
chunk_size = 30
for c_start in range(0, len(items), chunk_size):
c_items = items[c_start:c_start + chunk_size]
queries = []
for i_idx, item in enumerate(c_items):
file_path = item.get('path') or item.get('name') or ''
basename = os.path.basename(file_path)
p = f'/data/videodownloader/{basename}'
queries.append(f'item_{i_idx}: findScenes(scene_filter: {{path: {{value: {json.dumps(p)}, modifier: EQUALS}}}}) {{ scenes {{ id title performers {{ id name gender image_path }} studio {{ name }} }} }}')
# Second pass for unmatched items
for idx in unmatched_indices:
item = items[idx]
file_name = item.get('name', '')
file_path = item.get('path', '')
item_type = item.get('item_type', 'smb')
match = query_stash_single(file_name, file_path, item_type, stash_url)
results[idx] = match
batch_q = '{\n' + '\n'.join(queries) + '\n}'
try:
res = execute_stash_graphql(batch_q, stash_url, timeout=4)
data = res.get('data', {})
for i_idx in range(len(c_items)):
global_idx = c_start + i_idx
scenes = data.get(f'item_{i_idx}', {}).get('scenes', [])
if scenes and scenes[0].get('performers'):
parsed = parse_stash_scene_data(scenes[0], 'exact_path')
if parsed:
results[global_idx] = parsed
if not results[global_idx]:
unmatched_indices.append(global_idx)
except Exception as e:
print(f"Batch exact Stash query failed: {e}")
for i_idx in range(len(c_items)):
unmatched_indices.append(c_start + i_idx)
# 2. Second pass: Batch cleaned title queries for all unmatched items in chunks of 30
if unmatched_indices:
for c_start in range(0, len(unmatched_indices), chunk_size):
c_indices = unmatched_indices[c_start:c_start + chunk_size]
queries = []
for sub_idx, g_idx in enumerate(c_indices):
item = items[g_idx]
raw_name = item.get('name') or os.path.basename(item.get('path') or '')
cleaned_title = clean_torrent_title_for_stash(raw_name)
queries.append(f'title_{sub_idx}: findScenes(filter: {{q: {json.dumps(cleaned_title[:35])}, per_page: 2}}) {{ scenes {{ id title performers {{ id name gender image_path }} studio {{ name }} }} }}')
batch_q = '{\n' + '\n'.join(queries) + '\n}'
try:
res = execute_stash_graphql(batch_q, stash_url, timeout=4)
data = res.get('data', {})
for sub_idx, g_idx in enumerate(c_indices):
scenes = data.get(f'title_{sub_idx}', {}).get('scenes', [])
if scenes:
for s in scenes:
if s.get('performers'):
parsed = parse_stash_scene_data(s, 'title_match')
if parsed:
results[g_idx] = parsed
break
except Exception as e:
print(f"Batch title Stash query failed: {e}")
return results