Skip to content

Commit d6e21ad

Browse files
committed
merge ids
1 parent b8a4f25 commit d6e21ad

File tree

4 files changed

+123
-7
lines changed

4 files changed

+123
-7
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ sha2 = "0.10"
7373
axum-extra = { version = "0.9.2", features = ["query"] }
7474
http = "1.1.0"
7575
extism = "1.10.0"
76-
rs-plugin-common-interfaces = { version = "0.34.0", features = ["rusqlite",] }
76+
rs-plugin-common-interfaces = { version = "0.34.3", features = ["rusqlite",] }
7777
async-recursion = "1.1.0"
7878
async-compression = { version = "0.4.6", features = ["tokio"] }
7979
youtube_dl = { version = "0.10.0", features = ["tokio", "downloader-rustls-tls"] }

src/model/entity_search.rs

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
use rs_plugin_common_interfaces::lookup::{
2-
RsLookupMetadataResultWrapper, RsLookupMetadataResults, RsLookupQuery,
1+
use std::collections::HashMap;
2+
3+
use rs_plugin_common_interfaces::{
4+
domain::rs_ids::RsIds,
5+
lookup::{RsLookupMetadataResultWrapper, RsLookupMetadataResults, RsLookupQuery},
36
};
47

58
use crate::{domain::library::LibraryRole, error::RsResult};
@@ -9,6 +12,104 @@ use super::{users::ConnectedUser, ModelController};
912
/// Type alias for grouped search results (source_id, source_name, results)
1013
pub type SearchResultGroups = Vec<(String, String, RsLookupMetadataResults)>;
1114

15+
/// Merge RsIds across results that share at least one common ID.
16+
/// Uses union-find for O(n·k) performance where n = total results, k = IDs per result.
17+
pub fn merge_result_ids(groups: &mut SearchResultGroups) {
18+
// 1. Flatten all results into a linear index
19+
let mut entries: Vec<(usize, usize)> = Vec::new();
20+
let mut ids_vec: Vec<RsIds> = Vec::new();
21+
for (gi, (_, _, data)) in groups.iter().enumerate() {
22+
for (ri, wrapper) in data.results.iter().enumerate() {
23+
let extracted = wrapper.metadata.extract_ids().unwrap_or_default();
24+
entries.push((gi, ri));
25+
ids_vec.push(extracted);
26+
}
27+
}
28+
29+
let n = entries.len();
30+
if n <= 1 {
31+
return;
32+
}
33+
34+
// 2. Build index: "key:value" → set of flat indices
35+
let mut id_to_indices: HashMap<String, Vec<usize>> = HashMap::new();
36+
for (idx, ids) in ids_vec.iter().enumerate() {
37+
for id_str in ids.as_all_ids() {
38+
id_to_indices.entry(id_str).or_default().push(idx);
39+
}
40+
}
41+
42+
// 3. Union-Find with path halving
43+
let mut parent: Vec<usize> = (0..n).collect();
44+
fn find(parent: &mut [usize], mut x: usize) -> usize {
45+
while parent[x] != x {
46+
parent[x] = parent[parent[x]];
47+
x = parent[x];
48+
}
49+
x
50+
}
51+
for indices in id_to_indices.values() {
52+
if indices.len() > 1 {
53+
let root = indices[0];
54+
for &idx in &indices[1..] {
55+
let ra = find(&mut parent, root);
56+
let rb = find(&mut parent, idx);
57+
if ra != rb {
58+
parent[ra] = rb;
59+
}
60+
}
61+
}
62+
}
63+
64+
// 4. Group by root and merge
65+
let mut components: HashMap<usize, Vec<usize>> = HashMap::new();
66+
for i in 0..n {
67+
components
68+
.entry(find(&mut parent, i))
69+
.or_default()
70+
.push(i);
71+
}
72+
73+
for members in components.values() {
74+
if members.len() <= 1 {
75+
continue;
76+
}
77+
let mut merged = RsIds::default();
78+
for &idx in members {
79+
merged.merge(&ids_vec[idx]);
80+
}
81+
for &idx in members {
82+
let (gi, ri) = entries[idx];
83+
groups[gi].2.results[ri].metadata.apply_ids(&merged);
84+
}
85+
}
86+
}
87+
88+
/// Enrich results with IDs from previously seen results (for streaming).
89+
/// Returns extracted IDs from the new results to be added to the seen set.
90+
fn enrich_from_seen(
91+
results: &mut [RsLookupMetadataResultWrapper],
92+
seen_ids: &[RsIds],
93+
) -> Vec<RsIds> {
94+
let mut new_ids = Vec::new();
95+
for result in results.iter_mut() {
96+
if let Some(mut result_ids) = result.metadata.extract_ids() {
97+
let mut enriched = false;
98+
for seen in seen_ids {
99+
if result_ids.has_common_id(seen) {
100+
result_ids.merge(seen);
101+
enriched = true;
102+
}
103+
}
104+
if enriched {
105+
result.metadata.apply_ids(&result_ids);
106+
}
107+
new_ids.push(result_ids);
108+
}
109+
}
110+
new_ids
111+
}
112+
12113
impl ModelController {
13114
/// Generic search that queries Trakt (pre-fetched) + plugins, filters results by type.
14115
///
@@ -63,6 +164,7 @@ impl ModelController {
63164
}
64165
}
65166

167+
merge_result_ids(&mut groups);
66168
Ok(groups)
67169
}
68170

@@ -79,8 +181,16 @@ impl ModelController {
79181
requesting_user.check_library_role(library_id, LibraryRole::Read)?;
80182
let (tx, rx) = tokio::sync::mpsc::channel(16);
81183

184+
// Collect Trakt IDs for streaming enrichment
185+
let mut seen_ids: Vec<RsIds> = Vec::new();
186+
82187
if let Some(entries) = trakt_entries {
83188
if !entries.is_empty() {
189+
for e in &entries {
190+
if let Some(ids) = e.metadata.extract_ids() {
191+
seen_ids.push(ids);
192+
}
193+
}
84194
let _ = tx
85195
.send((
86196
"trakt".to_string(),
@@ -110,8 +220,11 @@ impl ModelController {
110220
results,
111221
next_page_key,
112222
} = entries;
113-
let filtered: Vec<_> = results.into_iter().filter(|r| result_filter(r)).collect();
223+
let mut filtered: Vec<_> =
224+
results.into_iter().filter(|r| result_filter(r)).collect();
114225
if !filtered.is_empty() {
226+
let new_ids = enrich_from_seen(&mut filtered, &seen_ids);
227+
seen_ids.extend(new_ids);
115228
if tx
116229
.send((
117230
id,

src/model/plugins/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,9 @@ impl ModelController {
284284
.filter(|p| sources.map_or(true, |s| s.iter().any(|id| id == &p.plugin.path)))
285285
.collect();
286286

287-
self.plugin_manager.lookup_metadata_grouped(query, plugins, target).await
287+
let mut results = self.plugin_manager.lookup_metadata_grouped(query, plugins, target).await?;
288+
crate::model::entity_search::merge_result_ids(&mut results);
289+
Ok(results)
288290
}
289291

290292
pub async fn exec_lookup_images_grouped(&self, query: RsLookupQuery, library_id: Option<String>, requesting_user: &ConnectedUser, target: Option<PluginTarget>) -> RsResult<HashMap<String, Vec<ExternalImage>>> {

0 commit comments

Comments
 (0)