Fix: Make torrent fetching instant and non-blocking with ultra-fast batched Stash GraphQL subqueries
This commit is contained in:
@@ -2627,19 +2627,20 @@
|
|||||||
qbTabBadge.textContent = qbState.torrents.length;
|
qbTabBadge.textContent = qbState.torrents.length;
|
||||||
renderTable();
|
renderTable();
|
||||||
updateStats();
|
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') {
|
if (stashAutoLookupSelect && stashAutoLookupSelect.value === 'true') {
|
||||||
const unanalyzed = qbState.torrents.filter(t => !t.analysis && t.moveStatus !== 'moved');
|
const unanalyzed = qbState.torrents.filter(t => !t.analysis && t.moveStatus !== 'moved');
|
||||||
if (unanalyzed.length > 0) {
|
if (unanalyzed.length > 0) {
|
||||||
|
setTimeout(async () => {
|
||||||
const stashMatches = await matchItemsWithStash(unanalyzed, 'qb');
|
const stashMatches = await matchItemsWithStash(unanalyzed, 'qb');
|
||||||
if (stashMatches > 0) {
|
if (stashMatches > 0) {
|
||||||
showToast(`⚡ Stash matched ${stashMatches} / ${unanalyzed.length} torrents!`, 'success');
|
showToast(`⚡ Stash matched ${stashMatches} / ${unanalyzed.length} torrents!`, 'success');
|
||||||
}
|
}
|
||||||
|
}, 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast(`Loaded ${qbState.torrents.length} torrents (${cachedLoadedCount} restored from SQLite cache)`, 'success');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -3321,19 +3322,20 @@ Return ONLY valid JSON matching:
|
|||||||
smbTabBadge.textContent = smbState.files.length;
|
smbTabBadge.textContent = smbState.files.length;
|
||||||
renderSmbTable();
|
renderSmbTable();
|
||||||
updateSmbStats();
|
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') {
|
if (stashAutoLookupSelect && stashAutoLookupSelect.value === 'true') {
|
||||||
const unanalyzed = smbState.files.filter(f => !f.analysis && f.moveStatus !== 'moved');
|
const unanalyzed = smbState.files.filter(f => !f.analysis && f.moveStatus !== 'moved');
|
||||||
if (unanalyzed.length > 0) {
|
if (unanalyzed.length > 0) {
|
||||||
|
setTimeout(async () => {
|
||||||
const stashMatches = await matchItemsWithStash(unanalyzed, 'smb');
|
const stashMatches = await matchItemsWithStash(unanalyzed, 'smb');
|
||||||
if (stashMatches > 0) {
|
if (stashMatches > 0) {
|
||||||
showToast(`⚡ Stash matched ${stashMatches} / ${unanalyzed.length} video files!`, 'success');
|
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) {
|
} catch (err) {
|
||||||
showToast(`SMB scan failed: ${err.message}`, 'error');
|
showToast(`SMB scan failed: ${err.message}`, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -322,40 +322,64 @@ def query_stash_batch(items, stash_url=None):
|
|||||||
if not items:
|
if not items:
|
||||||
return []
|
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)
|
results = [None] * len(items)
|
||||||
unmatched_indices = []
|
unmatched_indices = []
|
||||||
|
|
||||||
|
# 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 }} }} }}')
|
||||||
|
|
||||||
|
batch_q = '{\n' + '\n'.join(queries) + '\n}'
|
||||||
try:
|
try:
|
||||||
res = execute_stash_graphql(batch_q, stash_url, timeout=6)
|
res = execute_stash_graphql(batch_q, stash_url, timeout=4)
|
||||||
data = res.get('data', {})
|
data = res.get('data', {})
|
||||||
for idx in range(len(items)):
|
for i_idx in range(len(c_items)):
|
||||||
scenes = data.get(f'item_{idx}', {}).get('scenes', [])
|
global_idx = c_start + i_idx
|
||||||
|
scenes = data.get(f'item_{i_idx}', {}).get('scenes', [])
|
||||||
if scenes and scenes[0].get('performers'):
|
if scenes and scenes[0].get('performers'):
|
||||||
parsed = parse_stash_scene_data(scenes[0], 'exact_path')
|
parsed = parse_stash_scene_data(scenes[0], 'exact_path')
|
||||||
if parsed:
|
if parsed:
|
||||||
results[idx] = parsed
|
results[global_idx] = parsed
|
||||||
if not results[idx]:
|
if not results[global_idx]:
|
||||||
unmatched_indices.append(idx)
|
unmatched_indices.append(global_idx)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Batch exact Stash query failed: {e}")
|
print(f"Batch exact Stash query failed: {e}")
|
||||||
unmatched_indices = list(range(len(items)))
|
for i_idx in range(len(c_items)):
|
||||||
|
unmatched_indices.append(c_start + i_idx)
|
||||||
|
|
||||||
# Second pass for unmatched items
|
# 2. Second pass: Batch cleaned title queries for all unmatched items in chunks of 30
|
||||||
for idx in unmatched_indices:
|
if unmatched_indices:
|
||||||
item = items[idx]
|
for c_start in range(0, len(unmatched_indices), chunk_size):
|
||||||
file_name = item.get('name', '')
|
c_indices = unmatched_indices[c_start:c_start + chunk_size]
|
||||||
file_path = item.get('path', '')
|
queries = []
|
||||||
item_type = item.get('item_type', 'smb')
|
for sub_idx, g_idx in enumerate(c_indices):
|
||||||
match = query_stash_single(file_name, file_path, item_type, stash_url)
|
item = items[g_idx]
|
||||||
results[idx] = match
|
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
|
return results
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user