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
58use 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)
1013pub 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+
12113impl 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,
0 commit comments