From 8fa9572dea815dbf93dced4c82c7a29bdd7ff33a Mon Sep 17 00:00:00 2001 From: david Date: Mon, 17 Aug 2026 09:51:15 -0400 Subject: [PATCH] Fix: Make torrent fetching instant and non-blocking with ultra-fast batched Stash GraphQL subqueries --- qbittorrent_performer_sorter.html | 30 +++++------ server.py | 84 ++++++++++++++++++++----------- 2 files changed, 70 insertions(+), 44 deletions(-) diff --git a/qbittorrent_performer_sorter.html b/qbittorrent_performer_sorter.html index 1649584..a15bb4c 100644 --- a/qbittorrent_performer_sorter.html +++ b/qbittorrent_performer_sorter.html @@ -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 { diff --git a/server.py b/server.py index cc21fdc..5b14274 100644 --- a/server.py +++ b/server.py @@ -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 }} }} }}') + + 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) - # 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 + # 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