1use std::{collections::HashMap, fmt::Display, rc::Rc, hash::Hash};
2
3use fancy_regex::{Captures, Regex};
4use rusqlite::{
5 params,
6 types::{ToSqlOutput, Value, ValueRef},
7 Connection, OptionalExtension, Result, ToSql,
8};
9
10use crate::{debug_println, game::{ext::ExtSearchableType, get_game_add_apps}};
11
12use super::{ext::ExtSearchableRegistered, find_ext_data, get_game_data, get_game_platforms, get_game_tags, Game};
13
14#[derive(Debug, Clone)]
15pub enum SearchParam {
16 Boolean(bool),
17 String(String),
18 StringVec(Vec<String>),
19 Integer64(i64),
20 Float64(f64),
21 Value(serde_json::Value),
22}
23
24#[derive(Debug, Clone)]
25pub struct TagFilterInfo {
26 pub key: String,
27 pub dirty: bool,
28}
29
30impl ToSql for SearchParam {
31 fn to_sql(&self) -> Result<rusqlite::types::ToSqlOutput<'_>> {
32 match self {
33 SearchParam::Boolean(b) => Ok(ToSqlOutput::from(b.clone())),
34 SearchParam::String(s) => Ok(ToSqlOutput::from(s.as_str())),
35 SearchParam::StringVec(m) => {
36 let v: Rc<Vec<Value>> = Rc::new(
37 m.iter()
38 .map(|v| Value::from(v.clone()))
39 .collect::<Vec<Value>>(),
40 );
41 Ok(ToSqlOutput::Array(v))
42 }
43 SearchParam::Integer64(i) => Ok(ToSqlOutput::from(i.clone())),
44 SearchParam::Float64(f) => Ok(ToSqlOutput::from(f.clone())),
45 SearchParam::Value(v) => match v {
46 serde_json::Value::Null => Ok(ToSqlOutput::Borrowed(ValueRef::Null)),
47 serde_json::Value::Number(n) if n.is_i64() => Ok(ToSqlOutput::from(n.as_i64().unwrap())),
48 serde_json::Value::Number(n) if n.is_f64() => Ok(ToSqlOutput::from(n.as_f64().unwrap())),
49 _ => serde_json::to_string(v)
50 .map(ToSqlOutput::from)
51 .map_err(|err| rusqlite::Error::ToSqlConversionFailure(err.into())),
52 },
53 }
54 }
55}
56
57impl Display for SearchParam {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 SearchParam::Boolean(b) => f.write_str(b.to_string().as_str()),
61 SearchParam::String(s) => f.write_str(s),
62 SearchParam::StringVec(m) => f.write_str(format!("{}", m.join("', '")).as_str()),
63 SearchParam::Integer64(i) => f.write_str(i.to_string().as_str()),
64 SearchParam::Float64(nf) => f.write_str(nf.to_string().as_str()),
65 SearchParam::Value(v) => f.write_str(serde_json::to_string(v).unwrap_or_default().as_str()),
66 }
67 }
68}
69
70#[cfg_attr(feature = "napi", napi(object))]
71#[derive(Debug, Clone)]
72pub struct GameSearch {
73 pub filter: GameFilter,
74 pub load_relations: GameSearchRelations,
75 pub custom_id_order: Option<Vec<String>>,
76 pub order: GameSearchOrder,
77 pub ext_order: Option<GameSearchOrderExt>,
78 pub offset: Option<GameSearchOffset>,
79 pub limit: i64,
80 pub slim: bool,
81 pub with_tag_filter: Option<Vec<String>>,
82}
83
84#[cfg_attr(feature = "napi", napi(object))]
85#[derive(Debug, Clone)]
86pub struct GameSearchOffset {
87 pub value: serde_json::Value,
88 pub title: String, pub game_id: String,
90}
91
92#[cfg_attr(feature = "napi", napi(object))]
93#[derive(Debug, Clone)]
94pub struct GameSearchOrder {
95 pub column: GameSearchSortable,
96 pub direction: GameSearchDirection,
97}
98
99#[cfg_attr(feature = "napi", napi(object))]
100#[derive(Debug, Clone)]
101pub struct GameSearchOrderExt {
102 pub ext_id: String,
103 pub key: String,
104 pub default: serde_json::Value,
105}
106
107#[cfg_attr(feature = "napi", napi)]
108#[cfg_attr(not(feature = "napi"), derive(Clone))]
109#[derive(Debug, PartialEq)]
110pub enum GameSearchSortable {
111 TITLE,
112 DEVELOPER,
113 PUBLISHER,
114 SERIES,
115 PLATFORM,
116 DATEADDED,
117 DATEMODIFIED,
118 RELEASEDATE,
119 LASTPLAYED,
120 PLAYTIME,
121 RANDOM,
122 CUSTOM,
123}
124
125#[cfg_attr(feature = "napi", napi)]
126#[cfg_attr(not(feature = "napi"), derive(Clone))]
127#[derive(Debug)]
128pub enum GameSearchDirection {
129 ASC,
130 DESC,
131}
132
133#[cfg_attr(feature = "napi", napi(object))]
134#[derive(Debug, Clone)]
135pub struct GameSearchRelations {
136 pub tags: bool,
137 pub platforms: bool,
138 pub game_data: bool,
139 pub add_apps: bool,
140 pub ext_data: bool,
141}
142
143#[cfg_attr(feature = "napi", napi(object))]
144#[derive(Debug, Clone)]
145pub struct GameFilter {
146 pub subfilters: Vec<GameFilter>,
147 pub whitelist: FieldFilter,
148 pub blacklist: FieldFilter,
149 pub exact_whitelist: FieldFilter,
150 pub exact_blacklist: FieldFilter,
151 pub lower_than: SizeFilter,
152 pub higher_than: SizeFilter,
153 pub equal_to: SizeFilter,
154 pub bool_comp: BoolFilter,
155 pub match_any: bool,
156}
157
158#[cfg_attr(feature = "napi", napi(object))]
159#[derive(Debug, Clone)]
160pub struct FieldFilter {
161 pub id: Option<Vec<String>>,
162 pub generic: Option<Vec<String>>,
163 pub library: Option<Vec<String>>,
164 pub title: Option<Vec<String>>,
165 pub developer: Option<Vec<String>>,
166 pub publisher: Option<Vec<String>>,
167 pub series: Option<Vec<String>>,
168 pub tags: Option<Vec<String>>,
169 pub platforms: Option<Vec<String>>,
170 pub play_mode: Option<Vec<String>>,
171 pub status: Option<Vec<String>>,
172 pub notes: Option<Vec<String>>,
173 pub source: Option<Vec<String>>,
174 pub original_description: Option<Vec<String>>,
175 pub language: Option<Vec<String>>,
176 pub application_path: Option<Vec<String>>,
177 pub launch_command: Option<Vec<String>>,
178 pub ruffle_support: Option<Vec<String>>,
179 pub owner: Option<Vec<String>>,
180 pub ext: Option<HashMap<String, HashMap<String, Vec<String>>>>,
181}
182
183#[cfg_attr(feature = "napi", napi(object))]
184#[derive(Debug, Clone)]
185pub struct BoolFilter {
186 pub installed: Option<bool>,
187 pub ext: Option<HashMap<String, HashMap<String, bool>>>,
188}
189
190#[cfg_attr(feature = "napi", napi(object))]
191#[derive(Debug, Clone)]
192pub struct SizeFilter {
193 pub tags: Option<i64>,
194 pub platforms: Option<i64>,
195 pub date_added: Option<String>,
196 pub date_modified: Option<String>,
197 pub release_date: Option<String>,
198 pub game_data: Option<i64>,
199 pub add_apps: Option<i64>,
200 pub playtime: Option<i64>,
201 pub playcount: Option<i64>,
202 pub last_played: Option<String>,
203 pub ext: Option<HashMap<String, HashMap<String, i64>>>,
204}
205
206#[derive(Debug, Clone)]
207struct ForcedGameFilter {
208 pub whitelist: ForcedFieldFilter,
209 pub blacklist: ForcedFieldFilter,
210 pub exact_whitelist: ForcedFieldFilter,
211 pub exact_blacklist: ForcedFieldFilter,
212 pub lower_than: SizeFilter,
213 pub higher_than: SizeFilter,
214 pub equal_to: SizeFilter,
215 pub bool_comp: BoolFilter,
216}
217
218#[derive(Debug, Clone)]
219struct ForcedFieldFilter {
220 pub id: Vec<String>,
221 pub generic: Vec<String>,
222 pub library: Vec<String>,
223 pub title: Vec<String>,
224 pub developer: Vec<String>,
225 pub publisher: Vec<String>,
226 pub series: Vec<String>,
227 pub tags: Vec<String>,
228 pub platforms: Vec<String>,
229 pub play_mode: Vec<String>,
230 pub status: Vec<String>,
231 pub notes: Vec<String>,
232 pub source: Vec<String>,
233 pub original_description: Vec<String>,
234 pub language: Vec<String>,
235 pub application_path: Vec<String>,
236 pub launch_command: Vec<String>,
237 pub ruffle_support: Vec<String>,
238 pub owner: Vec<String>,
239 pub ext: HashMap<String, HashMap<String, Vec<String>>>,
240}
241
242#[cfg_attr(feature = "napi", napi(object))]
243#[derive(Debug, Clone)]
244pub struct PageTuple {
245 pub id: String,
246 pub order_val: serde_json::Value,
247 pub title: String,
248}
249
250impl Default for GameSearch {
251 fn default() -> Self {
252 GameSearch {
253 filter: GameFilter::default(),
254 load_relations: GameSearchRelations::default(),
255 order: GameSearchOrder {
256 column: GameSearchSortable::TITLE,
257 direction: GameSearchDirection::ASC,
258 },
259 custom_id_order: None,
260 ext_order: None,
261 offset: None,
262 limit: 1000,
263 slim: false,
264 with_tag_filter: None,
265 }
266 }
267}
268
269impl Default for GameFilter {
270 fn default() -> Self {
271 GameFilter {
272 subfilters: vec![],
273 whitelist: FieldFilter::default(),
274 blacklist: FieldFilter::default(),
275 exact_whitelist: FieldFilter::default(),
276 exact_blacklist: FieldFilter::default(),
277 lower_than: SizeFilter::default(),
278 higher_than: SizeFilter::default(),
279 equal_to: SizeFilter::default(),
280 bool_comp: BoolFilter::default(),
281 match_any: false,
282 }
283 }
284}
285
286impl Default for GameSearchRelations {
287 fn default() -> Self {
288 GameSearchRelations {
289 tags: false,
290 platforms: false,
291 game_data: false,
292 add_apps: false,
293 ext_data: true,
294 }
295 }
296}
297
298impl Default for FieldFilter {
299 fn default() -> Self {
300 FieldFilter {
301 id: None,
302 generic: None,
303 library: None,
304 title: None,
305 developer: None,
306 publisher: None,
307 series: None,
308 tags: None,
309 platforms: None,
310 play_mode: None,
311 status: None,
312 notes: None,
313 source: None,
314 original_description: None,
315 language: None,
316 application_path: None,
317 launch_command: None,
318 ruffle_support: None,
319 owner: None,
320 ext: None,
321 }
322 }
323}
324
325impl Default for ForcedGameFilter {
326 fn default() -> Self {
327 ForcedGameFilter {
328 whitelist: ForcedFieldFilter::default(),
329 blacklist: ForcedFieldFilter::default(),
330 exact_whitelist: ForcedFieldFilter::default(),
331 exact_blacklist: ForcedFieldFilter::default(),
332 lower_than: SizeFilter::default(),
333 higher_than: SizeFilter::default(),
334 equal_to: SizeFilter::default(),
335 bool_comp: BoolFilter::default(),
336 }
337 }
338}
339
340impl Default for ForcedFieldFilter {
341 fn default() -> Self {
342 ForcedFieldFilter {
343 id: vec![],
344 generic: vec![],
345 library: vec![],
346 title: vec![],
347 developer: vec![],
348 publisher: vec![],
349 series: vec![],
350 tags: vec![],
351 platforms: vec![],
352 play_mode: vec![],
353 status: vec![],
354 notes: vec![],
355 source: vec![],
356 original_description: vec![],
357 language: vec![],
358 application_path: vec![],
359 launch_command: vec![],
360 ruffle_support: vec![],
361 owner: vec![],
362 ext: HashMap::default(),
363 }
364 }
365}
366
367impl Default for SizeFilter {
368 fn default() -> Self {
369 return SizeFilter {
370 tags: None,
371 platforms: None,
372 date_added: None,
373 date_modified: None,
374 release_date: None,
375 game_data: None,
376 add_apps: None,
377 playtime: None,
378 playcount: None,
379 last_played: None,
380 ext: None,
381 };
382 }
383}
384
385impl Default for BoolFilter {
386 fn default() -> Self {
387 return BoolFilter {
388 installed: None,
389 ext: None,
390 };
391 }
392}
393
394impl From<&ForcedGameFilter> for GameFilter {
395 fn from(value: &ForcedGameFilter) -> Self {
396 let mut search = GameFilter::default();
397
398 if value.whitelist.id.len() > 0 {
401 search.whitelist.id = Some(value.whitelist.id.clone());
402 }
403 if value.whitelist.generic.len() > 0 {
404 search.whitelist.generic = Some(value.whitelist.generic.clone());
405 }
406 if value.whitelist.title.len() > 0 {
407 search.whitelist.title = Some(value.whitelist.title.clone());
408 }
409 if value.whitelist.developer.len() > 0 {
410 search.whitelist.developer = Some(value.whitelist.developer.clone());
411 }
412 if value.whitelist.publisher.len() > 0 {
413 search.whitelist.publisher = Some(value.whitelist.publisher.clone());
414 }
415 if value.whitelist.series.len() > 0 {
416 search.whitelist.series = Some(value.whitelist.series.clone());
417 }
418 if value.whitelist.tags.len() > 0 {
419 search.whitelist.tags = Some(value.whitelist.tags.clone());
420 }
421 if value.whitelist.platforms.len() > 0 {
422 search.whitelist.platforms = Some(value.whitelist.platforms.clone());
423 }
424 if value.whitelist.play_mode.len() > 0 {
425 search.whitelist.play_mode = Some(value.whitelist.play_mode.clone());
426 }
427 if value.whitelist.status.len() > 0 {
428 search.whitelist.status = Some(value.whitelist.status.clone());
429 }
430 if value.whitelist.notes.len() > 0 {
431 search.whitelist.notes = Some(value.whitelist.notes.clone());
432 }
433 if value.whitelist.source.len() > 0 {
434 search.whitelist.source = Some(value.whitelist.source.clone());
435 }
436 if value.whitelist.original_description.len() > 0 {
437 search.whitelist.original_description =
438 Some(value.whitelist.original_description.clone());
439 }
440 if value.whitelist.language.len() > 0 {
441 search.whitelist.language = Some(value.whitelist.language.clone());
442 }
443 if value.whitelist.application_path.len() > 0 {
444 search.whitelist.application_path = Some(value.whitelist.application_path.clone());
445 }
446 if value.whitelist.launch_command.len() > 0 {
447 search.whitelist.launch_command = Some(value.whitelist.launch_command.clone());
448 }
449 if value.whitelist.ruffle_support.len() > 0 {
450 search.whitelist.ruffle_support = Some(value.whitelist.ruffle_support.clone());
451 }
452 if value.whitelist.owner.len() > 0 {
453 search.whitelist.owner = Some(value.whitelist.owner.clone());
454 }
455 if value.whitelist.ext.len() > 0 {
456 search.whitelist.ext = Some(value.whitelist.ext.clone());
457 }
458
459 if value.blacklist.id.len() > 0 {
462 search.blacklist.id = Some(value.blacklist.id.clone());
463 }
464 if value.blacklist.generic.len() > 0 {
465 search.blacklist.generic = Some(value.blacklist.generic.clone());
466 }
467 if value.blacklist.title.len() > 0 {
468 search.blacklist.title = Some(value.blacklist.title.clone());
469 }
470 if value.blacklist.developer.len() > 0 {
471 search.blacklist.developer = Some(value.blacklist.developer.clone());
472 }
473 if value.blacklist.publisher.len() > 0 {
474 search.blacklist.publisher = Some(value.blacklist.publisher.clone());
475 }
476 if value.blacklist.series.len() > 0 {
477 search.blacklist.series = Some(value.blacklist.series.clone());
478 }
479 if value.blacklist.tags.len() > 0 {
480 search.blacklist.tags = Some(value.blacklist.tags.clone());
481 }
482 if value.blacklist.platforms.len() > 0 {
483 search.blacklist.platforms = Some(value.blacklist.platforms.clone());
484 }
485 if value.blacklist.play_mode.len() > 0 {
486 search.blacklist.play_mode = Some(value.blacklist.play_mode.clone());
487 }
488 if value.blacklist.status.len() > 0 {
489 search.blacklist.status = Some(value.blacklist.status.clone());
490 }
491 if value.blacklist.notes.len() > 0 {
492 search.blacklist.notes = Some(value.blacklist.notes.clone());
493 }
494 if value.blacklist.source.len() > 0 {
495 search.blacklist.source = Some(value.blacklist.source.clone());
496 }
497 if value.blacklist.original_description.len() > 0 {
498 search.blacklist.original_description =
499 Some(value.blacklist.original_description.clone());
500 }
501 if value.blacklist.language.len() > 0 {
502 search.blacklist.language = Some(value.blacklist.language.clone());
503 }
504 if value.blacklist.application_path.len() > 0 {
505 search.blacklist.application_path = Some(value.blacklist.application_path.clone());
506 }
507 if value.blacklist.launch_command.len() > 0 {
508 search.blacklist.launch_command = Some(value.blacklist.launch_command.clone());
509 }
510 if value.blacklist.ruffle_support.len() > 0 {
511 search.blacklist.ruffle_support = Some(value.blacklist.ruffle_support.clone());
512 }
513 if value.blacklist.owner.len() > 0 {
514 search.blacklist.owner = Some(value.blacklist.owner.clone());
515 }
516 if value.blacklist.ext.len() > 0 {
517 search.blacklist.ext = Some(value.blacklist.ext.clone());
518 }
519
520 if value.exact_whitelist.id.len() > 0 {
523 search.exact_whitelist.id = Some(value.exact_whitelist.id.clone());
524 }
525 if value.exact_whitelist.generic.len() > 0 {
526 search.exact_whitelist.generic = Some(value.exact_whitelist.generic.clone());
527 }
528 if value.exact_whitelist.title.len() > 0 {
529 search.exact_whitelist.title = Some(value.exact_whitelist.title.clone());
530 }
531 if value.exact_whitelist.developer.len() > 0 {
532 search.exact_whitelist.developer = Some(value.exact_whitelist.developer.clone());
533 }
534 if value.exact_whitelist.publisher.len() > 0 {
535 search.exact_whitelist.publisher = Some(value.exact_whitelist.publisher.clone());
536 }
537 if value.exact_whitelist.series.len() > 0 {
538 search.exact_whitelist.series = Some(value.exact_whitelist.series.clone());
539 }
540 if value.exact_whitelist.tags.len() > 0 {
541 search.exact_whitelist.tags = Some(value.exact_whitelist.tags.clone());
542 }
543 if value.exact_whitelist.platforms.len() > 0 {
544 search.exact_whitelist.platforms = Some(value.exact_whitelist.platforms.clone());
545 }
546 if value.exact_whitelist.play_mode.len() > 0 {
547 search.exact_whitelist.play_mode = Some(value.exact_whitelist.play_mode.clone());
548 }
549 if value.exact_whitelist.status.len() > 0 {
550 search.exact_whitelist.status = Some(value.exact_whitelist.status.clone());
551 }
552 if value.exact_whitelist.notes.len() > 0 {
553 search.exact_whitelist.notes = Some(value.exact_whitelist.notes.clone());
554 }
555 if value.exact_whitelist.source.len() > 0 {
556 search.exact_whitelist.source = Some(value.exact_whitelist.source.clone());
557 }
558 if value.exact_whitelist.original_description.len() > 0 {
559 search.exact_whitelist.original_description =
560 Some(value.exact_whitelist.original_description.clone());
561 }
562 if value.exact_whitelist.language.len() > 0 {
563 search.exact_whitelist.language = Some(value.exact_whitelist.language.clone());
564 }
565 if value.exact_whitelist.application_path.len() > 0 {
566 search.exact_whitelist.application_path =
567 Some(value.exact_whitelist.application_path.clone());
568 }
569 if value.exact_whitelist.launch_command.len() > 0 {
570 search.exact_whitelist.launch_command =
571 Some(value.exact_whitelist.launch_command.clone());
572 }
573 if value.exact_whitelist.ruffle_support.len() > 0 {
574 search.exact_whitelist.ruffle_support =
575 Some(value.exact_whitelist.ruffle_support.clone());
576 }
577 if value.exact_whitelist.owner.len() > 0 {
578 search.exact_whitelist.owner = Some(value.exact_whitelist.owner.clone());
579 }
580 if value.exact_whitelist.ext.len() > 0 {
581 search.exact_whitelist.ext = Some(value.exact_whitelist.ext.clone());
582 }
583
584 if value.exact_blacklist.id.len() > 0 {
587 search.exact_blacklist.id = Some(value.exact_blacklist.id.clone());
588 }
589 if value.exact_blacklist.generic.len() > 0 {
590 search.exact_blacklist.generic = Some(value.exact_blacklist.generic.clone());
591 }
592 if value.exact_blacklist.title.len() > 0 {
593 search.exact_blacklist.title = Some(value.exact_blacklist.title.clone());
594 }
595 if value.exact_blacklist.developer.len() > 0 {
596 search.exact_blacklist.developer = Some(value.exact_blacklist.developer.clone());
597 }
598 if value.exact_blacklist.publisher.len() > 0 {
599 search.exact_blacklist.publisher = Some(value.exact_blacklist.publisher.clone());
600 }
601 if value.exact_blacklist.series.len() > 0 {
602 search.exact_blacklist.series = Some(value.exact_blacklist.series.clone());
603 }
604 if value.exact_blacklist.tags.len() > 0 {
605 search.exact_blacklist.tags = Some(value.exact_blacklist.tags.clone());
606 }
607 if value.exact_blacklist.platforms.len() > 0 {
608 search.exact_blacklist.platforms = Some(value.exact_blacklist.platforms.clone());
609 }
610 if value.exact_blacklist.play_mode.len() > 0 {
611 search.exact_blacklist.play_mode = Some(value.exact_blacklist.play_mode.clone());
612 }
613 if value.exact_blacklist.status.len() > 0 {
614 search.exact_blacklist.status = Some(value.exact_blacklist.status.clone());
615 }
616 if value.exact_blacklist.notes.len() > 0 {
617 search.exact_blacklist.notes = Some(value.exact_blacklist.notes.clone());
618 }
619 if value.exact_blacklist.source.len() > 0 {
620 search.exact_blacklist.source = Some(value.exact_blacklist.source.clone());
621 }
622 if value.exact_blacklist.original_description.len() > 0 {
623 search.exact_blacklist.original_description =
624 Some(value.exact_blacklist.original_description.clone());
625 }
626 if value.exact_blacklist.language.len() > 0 {
627 search.exact_blacklist.language = Some(value.exact_blacklist.language.clone());
628 }
629 if value.exact_blacklist.application_path.len() > 0 {
630 search.exact_blacklist.application_path =
631 Some(value.exact_blacklist.application_path.clone());
632 }
633 if value.exact_blacklist.launch_command.len() > 0 {
634 search.exact_blacklist.launch_command =
635 Some(value.exact_blacklist.launch_command.clone());
636 }
637 if value.exact_blacklist.ruffle_support.len() > 0 {
638 search.exact_blacklist.ruffle_support =
639 Some(value.exact_blacklist.ruffle_support.clone());
640 }
641 if value.exact_blacklist.owner.len() > 0 {
642 search.exact_blacklist.owner = Some(value.exact_blacklist.owner.clone());
643 }
644 if value.exact_blacklist.ext.len() > 0 {
645 search.exact_blacklist.ext = Some(value.exact_blacklist.ext.clone());
646 }
647
648 search.higher_than = value.higher_than.clone();
649 search.lower_than = value.lower_than.clone();
650 search.equal_to = value.equal_to.clone();
651 search.bool_comp = value.bool_comp.clone();
652
653 search
654 }
655}
656
657pub trait InsertOrGet<K: Eq + Hash, V: Default> {
658 fn insert_or_get(&mut self, item: K) -> &mut V;
659}
660
661impl<K: Eq + Hash, V: Default> InsertOrGet<K, V> for HashMap<K, V> {
662 fn insert_or_get(&mut self, item: K) -> &mut V {
663 return match self.entry(item) {
664 std::collections::hash_map::Entry::Occupied(o) => o.into_mut(),
665 std::collections::hash_map::Entry::Vacant(v) => v.insert(V::default()),
666 };
667 }
668}
669
670macro_rules! whitelist_clause {
671 ($func:ident, $field_name:expr, $filter:expr) => {
672 $func($field_name, $filter, false, false)
673 };
674}
675
676macro_rules! blacklist_clause {
677 ($func:ident, $field_name:expr, $filter:expr) => {
678 $func($field_name, $filter, false, true)
679 };
680}
681
682macro_rules! exact_whitelist_clause {
683 ($func:ident, $field_name:expr, $filter:expr) => {
684 $func($field_name, $filter, true, false)
685 };
686}
687
688macro_rules! exact_blacklist_clause {
689 ($func:ident, $field_name:expr, $filter:expr) => {
690 $func($field_name, $filter, true, true)
691 };
692}
693
694const COUNT_QUERY: &str = "SELECT COUNT(*) FROM game";
695
696const RESULTS_QUERY: &str =
697 "SELECT game.id, title, alternateTitles, series, developer, publisher, platformsStr, \
698platformName, dateAdded, dateModified, broken, extreme, playMode, status, notes, \
699tagsStr, source, applicationPath, launchCommand, releaseDate, version, \
700originalDescription, language, activeDataId, activeDataOnDisk, lastPlayed, playtime, \
701activeGameConfigId, activeGameConfigOwner, archiveState, library, playCounter, logoPath, screenshotPath, ruffleSupport, \
702owner FROM game";
703
704const SLIM_RESULTS_QUERY: &str =
705 "SELECT game.id, title, series, developer, publisher, platformsStr,
706platformName, tagsStr, library, logoPath, screenshotPath, owner
707FROM game";
708
709const TAG_FILTER_INDEX_QUERY: &str = "INSERT INTO tag_filter_index (id) SELECT game.id FROM game";
710
711pub fn search_index(
712 conn: &Connection,
713 search: &mut GameSearch,
714 limit: Option<i64>,
715) -> Result<Vec<PageTuple>> {
716 rusqlite::vtab::array::load_module(conn)?;
718
719 if let Some(tags) = &search.with_tag_filter {
721 if tags.len() > 0 {
722 let mut filtered_search = GameSearch::default();
723 filtered_search.limit = 999999999;
724 filtered_search.filter.exact_blacklist.tags = Some(tags.to_vec());
725 filtered_search.filter.match_any = true;
726 new_tag_filter_index(conn, &mut filtered_search)?;
727 }
728 }
729
730 if search.order.column == GameSearchSortable::CUSTOM {
731 if let Some(custom_id_order) = &search.custom_id_order {
732 if custom_id_order.len() > 0 {
733 new_custom_id_order(conn, custom_id_order.clone())?;
734 }
735 }
736 }
737
738 let order_column = match search.order.column {
739 GameSearchSortable::TITLE => "game.title",
740 GameSearchSortable::DEVELOPER => "game.developer",
741 GameSearchSortable::PUBLISHER => "game.publisher",
742 GameSearchSortable::SERIES => "game.series",
743 GameSearchSortable::PLATFORM => "game.platformName",
744 GameSearchSortable::DATEADDED => "game.dateAdded",
745 GameSearchSortable::DATEMODIFIED => "game.dateModified",
746 GameSearchSortable::RELEASEDATE => "game.releaseDate",
747 GameSearchSortable::LASTPLAYED => "game.lastPlayed",
748 GameSearchSortable::PLAYTIME => "game.playtime",
749 GameSearchSortable::CUSTOM => "RowNum",
750 _ => "unknown",
751 };
752 let order_direction = match search.order.direction {
753 GameSearchDirection::ASC => "ASC",
754 GameSearchDirection::DESC => "DESC",
755 };
756 let page_size = search.limit;
757 search.limit = limit.or_else(|| Some(999999999)).unwrap();
758 let selection = match &search.ext_order {
759 Some(ext_order) => format!("
760 WITH OrderedExt AS (
761 SELECT
762 gameId AS id,
763 COALESCE(JSON_EXTRACT(data, '$.{}'), {}) AS ExtValue
764 FROM ext_data
765 WHERE extId = '{}'
766 )
767 SELECT
768 game.id,
769 OrderedExt.ExtValue,
770 game.title,
771 ROW_NUMBER() OVER (ORDER BY OrderedExt.ExtValue, game.title, game.id) AS rn
772 FROM game",
773 ext_order.key, ext_order.default.to_string(), ext_order.ext_id),
774 None => match search.order.column {
775 GameSearchSortable::CUSTOM => "
776 WITH OrderedIDs AS (
777 SELECT
778 id,
779 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS RowNum
780 FROM custom_id_order
781 )
782 SELECT game.id, OrderedIDs.RowNum, game.title, ROW_NUMBER() OVER (ORDER BY OrderedIDs.RowNum, game.title, game.id) AS rn FROM game".to_owned(),
783 _ => format!("SELECT game.id, {}, game.title, ROW_NUMBER() OVER (ORDER BY {} COLLATE NOCASE {}, game.title {}, game.id) AS rn FROM game", order_column, order_column, order_direction, order_direction)
784 }
785 };
786
787 let adjusted_order_column = match &search.ext_order {
789 Some(_) => "ExtValue",
790 None => order_column
791 };
792
793 let (mut query, mut params) = build_search_query(search, &selection);
794
795 query = format!(
797 "SELECT game.id, {}, game.title FROM ({}) game WHERE rn % ? = 0",
798 adjusted_order_column, query
799 );
800 params.push(SearchParam::String(page_size.to_string()));
801
802 let params_as_refs: Vec<&dyn rusqlite::ToSql> =
803 params.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
804
805 let mut keyset = vec![];
806 debug_println!(
807 "search index query - \n{}",
808 format_query(&query, params.clone())
809 );
810 let mut stmt = conn.prepare(&query)?;
811 let page_tuple_iter = stmt.query_map(params_as_refs.as_slice(), |row| {
812 let order_val = match row.get::<_, Option<Value>>(1)? {
813 Some(value) => value,
814 None => Value::Text("".to_string()), };
816 Ok(PageTuple {
817 id: row.get(0)?,
818 order_val: match order_val {
819 Value::Text(v) => serde_json::Value::String(v),
820 Value::Integer(v) => serde_json::Value::Number(v.into()),
821 Value::Real(v) => serde_json::Value::Number(
822 serde_json::Number::from_f64(v).unwrap_or_else(|| serde_json::Number::from(0))
823 ),
824 _ => serde_json::Value::Null
825 },
826 title: row.get(2)?,
827 })
828 })?;
829 for page_tuple in page_tuple_iter {
830 keyset.push(page_tuple?);
831 }
832 Ok(keyset)
833}
834
835pub fn search_count(conn: &Connection, search: &GameSearch) -> Result<i64> {
836 rusqlite::vtab::array::load_module(conn)?;
838
839 let mut selection = COUNT_QUERY.to_owned();
840 if let Some(ext_order) = &search.ext_order {
841 selection = format!("WITH OrderedExt AS (
842 SELECT
843 gameId AS id,
844 COALESCE(JSON_EXTRACT(data, '$.{}'), {}) AS ExtValue
845 FROM ext_data
846 WHERE extId = '{}'
847 ) ", ext_order.key, ext_order.default.to_string(), ext_order.ext_id)
848 + &selection;
849 } else if search.order.column == GameSearchSortable::CUSTOM {
850 selection = "WITH OrderedIDs AS (
851 SELECT
852 id,
853 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS RowNum
854 FROM custom_id_order
855 ) "
856 .to_owned()
857 + &selection;
858 }
859
860 let (query, params) = build_search_query(search, &selection);
861 debug_println!(
862 "search count query - \n{}",
863 format_query(&query, params.clone())
864 );
865
866 let params_as_refs: Vec<&dyn rusqlite::ToSql> =
867 params.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
868
869 let count_result = conn
870 .query_row(&query, params_as_refs.as_slice(), |row| {
871 row.get::<_, i64>(0)
872 })
873 .optional()?;
874
875 match count_result {
876 Some(count) => Ok(count),
877 None => Ok(0),
878 }
879}
880
881pub fn search_custom<T, F>(
882 conn: &Connection,
883 search: &GameSearch,
884 selection: &str,
885 game_map_closure: F,
886) -> Result<Vec<T>>
887where
888 F: Fn(&rusqlite::Row<'_>) -> Result<T>,
889{
890 rusqlite::vtab::array::load_module(conn)?;
892
893 let (query, params) = build_search_query(search, selection);
894 debug_println!("search query - \n{}", format_query(&query, params.clone()));
895
896 let params_as_refs: Vec<&dyn rusqlite::ToSql> =
898 params.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
899
900 let mut results = Vec::new();
901
902 let mut stmt = conn.prepare(query.as_str())?;
903 let row_iter = stmt.query_map(params_as_refs.as_slice(), game_map_closure)?;
904
905 for result in row_iter {
906 results.push(result?);
907 }
908
909 Ok(results)
910}
911
912pub fn search(conn: &Connection, search: &GameSearch) -> Result<Vec<Game>> {
914 let mut selection = match search.slim {
915 true => SLIM_RESULTS_QUERY.to_owned(),
916 false => RESULTS_QUERY.to_owned(),
917 };
918 if let Some(ext_order) = &search.ext_order {
919 selection = format!("WITH OrderedExt AS (
920 SELECT
921 gameId AS id,
922 COALESCE(JSON_EXTRACT(data, '$.{}'), {}) AS ExtValue
923 FROM ext_data
924 WHERE extId = '{}'
925 ) ", ext_order.key, ext_order.default.to_string(), ext_order.ext_id)
926 + &selection;
927 } else if search.order.column == GameSearchSortable::CUSTOM {
928 selection = "WITH OrderedIDs AS (
929 SELECT
930 id,
931 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS RowNum
932 FROM custom_id_order
933 ) "
934 .to_owned()
935 + &selection;
936 }
937
938 let game_map_closure = match search.slim {
939 true => |row: &rusqlite::Row<'_>| -> Result<Game> {
940 Ok(Game {
941 id: row.get(0)?,
942 title: row.get(1)?,
943 series: row.get(2)?,
944 developer: row.get(3)?,
945 publisher: row.get(4)?,
946 platforms: row.get(5)?,
947 primary_platform: row.get(6)?,
948 tags: row.get(7)?,
949 library: row.get(8)?,
950 logo_path: row.get(9)?,
951 screenshot_path: row.get(10)?,
952 owner: row.get(11)?,
953 ..Default::default()
954 })
955 },
956 false => |row: &rusqlite::Row<'_>| -> Result<Game> {
957 Ok(Game {
958 id: row.get(0)?,
959 title: row.get(1)?,
960 alternate_titles: row.get(2)?,
961 series: row.get(3)?,
962 developer: row.get(4)?,
963 publisher: row.get(5)?,
964 platforms: row.get(6)?,
965 primary_platform: row.get(7)?,
966 date_added: row.get(8)?,
967 date_modified: row.get(9)?,
968 legacy_broken: row.get(10)?,
969 legacy_extreme: row.get(11)?,
970 play_mode: row.get(12)?,
971 status: row.get(13)?,
972 notes: row.get(14)?,
973 tags: row.get(15)?,
974 source: row.get(16)?,
975 legacy_application_path: row.get(17)?,
976 legacy_launch_command: row.get(18)?,
977 release_date: row.get(19)?,
978 version: row.get(20)?,
979 original_description: row.get(21)?,
980 language: row.get(22)?,
981 active_data_id: row.get(23)?,
982 active_data_on_disk: row.get(24)?,
983 last_played: row.get(25)?,
984 playtime: row.get(26)?,
985 active_game_config_id: row.get(27)?,
986 active_game_config_owner: row.get(28)?,
987 archive_state: row.get(29)?,
988 library: row.get(30)?,
989 play_counter: row.get(31)?,
990 detailed_platforms: None,
991 detailed_tags: None,
992 game_data: None,
993 add_apps: None,
994 logo_path: row.get(32)?,
995 screenshot_path: row.get(33)?,
996 ruffle_support: row.get(34)?,
997 owner: row.get(35)?,
998 ext_data: None,
999 })
1000 },
1001 };
1002
1003 let mut games = search_custom(conn, search, selection.as_str(), game_map_closure)?;
1004
1005 for game in &mut games {
1006 if search.load_relations.platforms {
1007 game.detailed_platforms = get_game_platforms(conn, &game.id)?.into();
1008 }
1009 if search.load_relations.tags {
1010 game.detailed_tags = get_game_tags(conn, &game.id)?.into();
1011 }
1012 if search.load_relations.game_data {
1013 game.game_data = Some(get_game_data(conn, &game.id)?);
1014 }
1015 if search.load_relations.add_apps {
1016 game.add_apps = Some(get_game_add_apps(conn, &game.id)?);
1017 }
1018 if search.load_relations.ext_data {
1019 game.ext_data = Some(find_ext_data(conn, &game.id)?);
1020 }
1021 }
1022
1023 Ok(games)
1024}
1025
1026pub fn search_random(conn: &Connection, mut s: GameSearch, count: i64) -> Result<Vec<Game>> {
1027 s.limit = count;
1028 s.order.column = GameSearchSortable::RANDOM;
1029
1030 if let Some(tags) = &s.with_tag_filter {
1032 if tags.len() > 0 {
1033 let mut filtered_search = GameSearch::default();
1034 filtered_search.limit = 999999999;
1035 filtered_search.filter.exact_blacklist.tags = Some(tags.to_vec());
1036 filtered_search.filter.match_any = true;
1037 new_tag_filter_index(conn, &mut filtered_search)?;
1038 }
1039 }
1040
1041 search(conn, &s)
1042}
1043
1044fn build_search_query(search: &GameSearch, selection: &str) -> (String, Vec<SearchParam>) {
1045 let mut query = String::from(selection);
1046
1047 if search.ext_order.is_some() {
1048 query.push_str(" INNER JOIN OrderedExt ON game.id = OrderedExt.id");
1049 } else if search.order.column == GameSearchSortable::CUSTOM {
1050 query.push_str(" INNER JOIN OrderedIDs ON game.id = OrderedIDs.id");
1051 }
1052
1053 let order_column = match search.ext_order {
1055 Some(_) => "OrderedExt.ExtValue",
1056 None => match search.order.column {
1057 GameSearchSortable::TITLE => "game.title",
1058 GameSearchSortable::DEVELOPER => "game.developer",
1059 GameSearchSortable::PUBLISHER => "game.publisher",
1060 GameSearchSortable::SERIES => "game.series",
1061 GameSearchSortable::PLATFORM => "game.platformName",
1062 GameSearchSortable::DATEADDED => "game.dateAdded",
1063 GameSearchSortable::DATEMODIFIED => "game.dateModified",
1064 GameSearchSortable::RELEASEDATE => "game.releaseDate",
1065 GameSearchSortable::LASTPLAYED => "game.lastPlayed",
1066 GameSearchSortable::PLAYTIME => "game.playtime",
1067 GameSearchSortable::CUSTOM => "OrderedIDs.RowNum",
1068 _ => "unknown",
1069 }
1070 };
1071 let order_direction = match search.order.direction {
1072 GameSearchDirection::ASC => "ASC",
1073 GameSearchDirection::DESC => "DESC",
1074 };
1075
1076 let mut params: Vec<SearchParam> = vec![];
1078 let where_clause = build_filter_query(&search.filter, &mut params);
1079
1080 if let Some(tags) = &search.with_tag_filter {
1082 if tags.len() > 0 {
1083 query.push_str(" INNER JOIN tag_filter_index ON game.id = tag_filter_index.id");
1084 }
1085 }
1086
1087 if let Some(offset) = search.offset.clone() {
1089 let offset_val = match offset.value {
1090 serde_json::Value::Number(number) => SearchParam::Float64(number.as_f64().unwrap_or(0.into())),
1091 val => SearchParam::String(val.as_str().unwrap_or("").to_owned()),
1092 };
1093 if search.order.column == GameSearchSortable::CUSTOM {
1094 let offset_clause = format!(" WHERE OrderedIDs.RowNum > ?");
1095 query.push_str(&offset_clause);
1096 params.insert(0, offset_val);
1097 } else {
1098 let offset_clause = match search.order.direction {
1099 GameSearchDirection::ASC => {
1100 format!(
1101 " WHERE ({} COLLATE NOCASE, game.title, game.id) > (?, ?, ?)",
1102 order_column
1103 )
1104 }
1105 GameSearchDirection::DESC => {
1106 format!(
1107 " WHERE ({} COLLATE NOCASE, game.title, game.id) < (?, ?, ?)",
1108 order_column
1109 )
1110 }
1111 };
1112 query.push_str(&offset_clause);
1113
1114 params.insert(0, SearchParam::String(offset.game_id.clone()));
1116 params.insert(0, SearchParam::String(offset.title.clone()));
1117 params.insert(0, offset_val);
1118 }
1119 }
1120
1121 if where_clause.len() > 0 && where_clause != "()" {
1123 let start_clause = match search.offset {
1125 Some(_) => " AND (",
1126 None => " WHERE (",
1127 };
1128 query.push_str(start_clause);
1129 query.push_str(&where_clause);
1130 query.push_str(")");
1131 }
1132
1133 if search.order.column == GameSearchSortable::RANDOM {
1134 query.push_str(" ORDER BY RANDOM()");
1135 let limit_query = format!(" LIMIT {}", search.limit);
1136 query.push_str(&limit_query);
1137 } else {
1138 if search.order.column == GameSearchSortable::CUSTOM {
1139 query.push_str(" ORDER BY OrderedIDs.RowNum");
1140 } else {
1141 query.push_str(
1142 format!(
1143 " ORDER BY {} COLLATE NOCASE {}, game.title {}",
1144 order_column, order_direction, order_direction
1145 )
1146 .as_str(),
1147 );
1148 }
1149 let limit_query = format!(" LIMIT {}", search.limit);
1150 query.push_str(&limit_query);
1151 }
1152
1153 (query, params)
1154}
1155
1156fn build_filter_query(filter: &GameFilter, params: &mut Vec<SearchParam>) -> String {
1157 let mut where_clauses = Vec::new();
1158
1159 if filter.subfilters.len() > 0 {
1160 for subfilter in filter.subfilters.iter() {
1161 let new_clause = build_filter_query(subfilter, params);
1162 if new_clause != "" {
1163 where_clauses.push(format!("({})", new_clause));
1164 }
1165 }
1166 }
1167
1168 let mut add_clause =
1169 |field_name: &str, values: &Option<Vec<String>>, exact: bool, blacklist: bool| {
1170 if let Some(value_list) = values {
1171 let comparator = match (blacklist, exact) {
1172 (true, true) => "!=",
1173 (true, false) => "NOT LIKE",
1174 (false, true) => "=",
1175 (false, false) => "LIKE",
1176 };
1177
1178 if exact && filter.match_any {
1180 let comparator = match blacklist {
1181 true => "NOT IN",
1182 false => "IN",
1183 };
1184 where_clauses.push(format!("game.{} {} rarray(?)", field_name, comparator));
1185 params.push(SearchParam::StringVec(value_list.clone()));
1186 } else if blacklist {
1187 let mut inner_clauses = vec![];
1188 for value in value_list {
1189 inner_clauses.push(format!("game.{} {} ?", field_name, comparator));
1190 if exact {
1191 params.push(SearchParam::String(value.clone()));
1192 } else {
1193 let p = format!("%{}%", value);
1194 params.push(SearchParam::String(p));
1195 }
1196 }
1197 where_clauses.push(format!("({})", inner_clauses.join(" AND ")));
1198 } else {
1199 for value in value_list {
1200 where_clauses.push(format!("game.{} {} ?", field_name, comparator));
1201 if exact {
1202 params.push(SearchParam::String(value.clone()));
1203 } else {
1204 let p = format!("%{}%", value);
1205 params.push(SearchParam::String(p));
1206 }
1207 }
1208 }
1209 }
1210 };
1211
1212 exact_whitelist_clause!(add_clause, "library", &filter.exact_whitelist.library);
1214 exact_whitelist_clause!(add_clause, "developer", &filter.exact_whitelist.developer);
1215 exact_whitelist_clause!(add_clause, "publisher", &filter.exact_whitelist.publisher);
1216 exact_whitelist_clause!(add_clause, "series", &filter.exact_whitelist.series);
1217 exact_whitelist_clause!(add_clause, "playMode", &filter.exact_whitelist.play_mode);
1218 exact_whitelist_clause!(add_clause, "status", &filter.exact_whitelist.status);
1219 exact_whitelist_clause!(add_clause, "notes", &filter.exact_whitelist.notes);
1220 exact_whitelist_clause!(add_clause, "source", &filter.exact_whitelist.source);
1221 exact_whitelist_clause!(
1222 add_clause,
1223 "originalDescription",
1224 &filter.exact_whitelist.original_description
1225 );
1226 exact_whitelist_clause!(add_clause, "language", &filter.exact_whitelist.language);
1227 exact_whitelist_clause!(
1228 add_clause,
1229 "ruffleSupport",
1230 &filter.exact_whitelist.ruffle_support
1231 );
1232 exact_whitelist_clause!(add_clause, "owner", &filter.exact_whitelist.owner);
1233
1234 exact_blacklist_clause!(add_clause, "library", &filter.exact_blacklist.library);
1236 exact_blacklist_clause!(add_clause, "developer", &filter.exact_blacklist.developer);
1237 exact_blacklist_clause!(add_clause, "publisher", &filter.exact_blacklist.publisher);
1238 exact_blacklist_clause!(add_clause, "series", &filter.exact_blacklist.series);
1239 exact_blacklist_clause!(add_clause, "playMode", &filter.exact_blacklist.play_mode);
1240 exact_blacklist_clause!(add_clause, "status", &filter.exact_blacklist.status);
1241 exact_blacklist_clause!(add_clause, "notes", &filter.exact_blacklist.notes);
1242 exact_blacklist_clause!(add_clause, "source", &filter.exact_blacklist.source);
1243 exact_blacklist_clause!(
1244 add_clause,
1245 "originalDescription",
1246 &filter.exact_blacklist.original_description
1247 );
1248 exact_blacklist_clause!(add_clause, "language", &filter.exact_blacklist.language);
1249 exact_blacklist_clause!(
1250 add_clause,
1251 "ruffleSupport",
1252 &filter.exact_blacklist.ruffle_support
1253 );
1254 exact_blacklist_clause!(add_clause, "owner", &filter.exact_blacklist.owner);
1255
1256 whitelist_clause!(add_clause, "library", &filter.whitelist.library);
1258 whitelist_clause!(add_clause, "developer", &filter.whitelist.developer);
1259 whitelist_clause!(add_clause, "publisher", &filter.whitelist.publisher);
1260 whitelist_clause!(add_clause, "series", &filter.whitelist.series);
1261 whitelist_clause!(add_clause, "playMode", &filter.whitelist.play_mode);
1262 whitelist_clause!(add_clause, "status", &filter.whitelist.status);
1263 whitelist_clause!(add_clause, "notes", &filter.whitelist.notes);
1264 whitelist_clause!(add_clause, "source", &filter.whitelist.source);
1265 whitelist_clause!(
1266 add_clause,
1267 "originalDescription",
1268 &filter.whitelist.original_description
1269 );
1270 whitelist_clause!(add_clause, "language", &filter.whitelist.language);
1271 whitelist_clause!(
1272 add_clause,
1273 "ruffleSupport",
1274 &filter.whitelist.ruffle_support
1275 );
1276 whitelist_clause!(add_clause, "owner", &filter.whitelist.owner);
1277
1278 blacklist_clause!(add_clause, "library", &filter.blacklist.library);
1280 blacklist_clause!(add_clause, "developer", &filter.blacklist.developer);
1281 blacklist_clause!(add_clause, "publisher", &filter.blacklist.publisher);
1282 blacklist_clause!(add_clause, "series", &filter.blacklist.series);
1283 blacklist_clause!(add_clause, "playMode", &filter.blacklist.play_mode);
1284 blacklist_clause!(add_clause, "status", &filter.blacklist.status);
1285 blacklist_clause!(add_clause, "notes", &filter.blacklist.notes);
1286 blacklist_clause!(add_clause, "source", &filter.blacklist.source);
1287 blacklist_clause!(
1288 add_clause,
1289 "originalDescription",
1290 &filter.blacklist.original_description
1291 );
1292 blacklist_clause!(add_clause, "language", &filter.blacklist.language);
1293 blacklist_clause!(
1294 add_clause,
1295 "ruffleSupport",
1296 &filter.blacklist.ruffle_support
1297 );
1298 blacklist_clause!(add_clause, "owner", &filter.blacklist.owner);
1299
1300 let mut id_clause = |values: &Option<Vec<String>>, exact: bool, blacklist: bool| {
1301 if let Some(value_list) = values {
1302 if exact {
1303 let comparator = match blacklist {
1305 true => "NOT IN",
1306 false => "IN",
1307 };
1308 where_clauses.push(format!("(game.id {} rarray(?) OR game.id {} (SELECT id FROM game_redirect WHERE sourceId IN rarray(?)))", comparator, comparator));
1309 params.push(SearchParam::StringVec(value_list.clone()));
1310 params.push(SearchParam::StringVec(value_list.clone()));
1311 } else {
1312 for value in value_list {
1313 if value.len() == 36 {
1314 let comparator = match blacklist {
1315 true => "!=",
1316 false => "=",
1317 };
1318 where_clauses.push(format!("(game.id {} ? OR game.id {} (SELECT id FROM game_redirect WHERE sourceId = ? LIMIT 1))", comparator, comparator));
1319
1320 params.push(SearchParam::String(value.clone()));
1321 params.push(SearchParam::String(value.clone()));
1322 } else {
1323 let comparator = match blacklist {
1324 true => "NOT LIKE",
1325 false => "LIKE",
1326 };
1327 where_clauses.push(format!("(game.id {} ?)", comparator));
1328 let p = format!("%{}%", value);
1329 params.push(SearchParam::String(p));
1330 }
1331 }
1332 }
1333 }
1334 };
1335
1336 id_clause(&filter.exact_whitelist.id, true, false);
1337 id_clause(&filter.exact_blacklist.id, true, true);
1338 id_clause(&filter.whitelist.id, false, false);
1339 id_clause(&filter.blacklist.id, false, false);
1340
1341 let mut add_tagged_clause =
1342 |tag_name: &str, values: &Option<Vec<String>>, exact: bool, blacklist: bool| {
1343 if let Some(value_list) = values {
1344 let comparator = match blacklist {
1345 true => "NOT IN",
1346 false => "IN",
1347 };
1348
1349 if exact && filter.match_any {
1351 params.push(SearchParam::StringVec(value_list.clone()));
1353
1354 let tag_query = format!(
1355 "game.id {} (SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1356 SELECT {}Id FROM {}_alias WHERE name IN rarray(?)))",
1357 comparator, tag_name, tag_name, tag_name, tag_name, tag_name
1358 );
1359
1360 where_clauses.push(tag_query);
1361 } else {
1362 let mut inner_tag_queries = vec![];
1363
1364 if exact {
1366 for value in value_list {
1367 inner_tag_queries.push("name = ?");
1368 params.push(SearchParam::String(value.clone()));
1369 }
1370 } else {
1371 for value in value_list {
1372 inner_tag_queries.push("name LIKE ?");
1373 let p = format!("%{}%", value);
1374 params.push(SearchParam::String(p));
1375 }
1376 }
1377
1378 let tag_query = match (blacklist, filter.match_any) {
1380 (false, false) => {
1381 if inner_tag_queries.len() == 1 {
1382 format!(
1383 "game.id {} (SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1384 SELECT {}Id FROM {}_alias WHERE {})
1385 )",
1386 comparator,
1387 tag_name,
1388 tag_name,
1389 tag_name,
1390 tag_name,
1391 tag_name,
1392 inner_tag_queries[0]
1393 )
1394 } else {
1395 let mut q = format!(
1396 "SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1397 SELECT {}Id FROM {}_alias WHERE {}
1398 )",
1399 tag_name,
1400 tag_name,
1401 tag_name,
1402 tag_name,
1403 tag_name,
1404 inner_tag_queries[0]
1405 );
1406 for inner_tag_query in inner_tag_queries.iter().skip(1) {
1407 let part = format!(
1408 " AND gameId IN (
1409 SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1410 SELECT {}Id FROM {}_alias WHERE {}
1411 )
1412 )",
1413 tag_name,
1414 tag_name,
1415 tag_name,
1416 tag_name,
1417 tag_name,
1418 inner_tag_query
1419 );
1420 q.push_str(&part);
1421 }
1422 format!("game.id {} ({})", comparator, q)
1423 }
1424 }
1425 (true, false) => format!(
1428 "game.id {} (SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1429 SELECT {}Id FROM {}_alias WHERE ({})))",
1430 comparator,
1431 tag_name,
1432 tag_name,
1433 tag_name,
1434 tag_name,
1435 tag_name,
1436 inner_tag_queries.join(" OR ")
1437 ),
1438 (true, true) | (false, true) => format!(
1439 "game.id {} (SELECT gameId FROM game_{}s_{} WHERE {}Id IN (
1440 SELECT {}Id FROM {}_alias WHERE name IN {}))",
1441 comparator,
1442 tag_name,
1443 tag_name,
1444 tag_name,
1445 tag_name,
1446 tag_name,
1447 inner_tag_queries.join(" OR ")
1448 ),
1449 };
1450
1451 where_clauses.push(tag_query);
1452 }
1453 }
1454 };
1455
1456 add_tagged_clause("tag", &filter.whitelist.tags, false, false);
1458 add_tagged_clause("tag", &filter.blacklist.tags, false, true);
1459 add_tagged_clause("tag", &filter.exact_whitelist.tags, true, false);
1460 add_tagged_clause("tag", &filter.exact_blacklist.tags, true, true);
1461
1462 add_tagged_clause("platform", &filter.whitelist.platforms, false, false);
1463 add_tagged_clause("platform", &filter.blacklist.platforms, false, true);
1464 add_tagged_clause("platform", &filter.exact_whitelist.platforms, true, false);
1465 add_tagged_clause("platform", &filter.exact_blacklist.platforms, true, true);
1466
1467 let mut add_multi_clause =
1468 |field_names: Vec<&str>, filter: &Option<Vec<String>>, exact: bool, blacklist: bool| {
1469 if let Some(value_list) = filter {
1470 let comparator = match (blacklist, exact) {
1471 (true, true) => "!=",
1472 (true, false) => "NOT LIKE",
1473 (false, true) => "=",
1474 (false, false) => "LIKE",
1475 };
1476
1477 if blacklist {
1478 let mut inner_clauses = vec![];
1479 for value in value_list {
1480 let mut value_clauses = vec![];
1481 for field_name in field_names.clone() {
1482 value_clauses.push(format!("game.{} {} ?", field_name, comparator));
1483 if exact {
1484 params.push(SearchParam::String(value.clone()));
1485 } else {
1486 let p = format!("%{}%", value);
1487 params.push(SearchParam::String(p));
1488 }
1489 }
1490 inner_clauses.push(format!("({})", &value_clauses.join(" OR ")));
1491 }
1492 where_clauses.push(format!("({})", inner_clauses.join(" OR ")));
1493 } else {
1494 for value in value_list {
1495 let mut value_clauses = vec![];
1496 for field_name in field_names.clone() {
1497 value_clauses.push(format!("game.{} {} ?", field_name, comparator));
1498 if exact {
1499 params.push(SearchParam::String(value.clone()));
1500 } else {
1501 let p = format!("%{}%", value);
1502 params.push(SearchParam::String(p));
1503 }
1504 }
1505 where_clauses.push(format!("({})", &value_clauses.join(" OR ")));
1506 }
1507 }
1508 }
1509 };
1510
1511 add_multi_clause(
1513 vec!["title", "alternateTitles"],
1514 &filter.whitelist.title,
1515 false,
1516 false,
1517 );
1518 add_multi_clause(
1519 vec![
1520 "title",
1521 "alternateTitles",
1522 "developer",
1523 "publisher",
1524 "series",
1525 ],
1526 &filter.whitelist.generic,
1527 false,
1528 false,
1529 );
1530
1531 add_multi_clause(
1533 vec!["title", "alternateTitles"],
1534 &filter.blacklist.title,
1535 false,
1536 true,
1537 );
1538 add_multi_clause(
1539 vec![
1540 "title",
1541 "alternateTitles",
1542 "developer",
1543 "publisher",
1544 "series",
1545 ],
1546 &filter.blacklist.generic,
1547 false,
1548 true,
1549 );
1550
1551 let mut add_joint_game_data_clause =
1552 |field_name: &str,
1553 game_field_name: &str,
1554 filter: &Option<Vec<String>>,
1555 exact: bool,
1556 blacklist: bool| {
1557 if let Some(value_list) = filter {
1558 let comparator = match (blacklist, exact) {
1559 (true, true) => "!=",
1560 (true, false) => "NOT LIKE",
1561 (false, true) => "=",
1562 (false, false) => "LIKE",
1563 };
1564
1565 if blacklist {
1566 let mut inner_clauses = vec![];
1567 for value in value_list {
1568 let mut value_clauses = vec![];
1569 value_clauses.push(format!("game.{} {} ?", game_field_name, comparator));
1570 if exact {
1571 params.push(SearchParam::String(value.clone()));
1572 } else {
1573 let p = format!("%{}%", value);
1574 params.push(SearchParam::String(p));
1575 }
1576
1577 value_clauses.push(format!(
1578 "game.id IN (SELECT gameId FROM game_data WHERE {} {} ?)",
1579 field_name, comparator
1580 ));
1581 if exact {
1582 params.push(SearchParam::String(value.clone()));
1583 } else {
1584 let p = format!("%{}%", value);
1585 params.push(SearchParam::String(p));
1586 }
1587 inner_clauses.push(format!("({})", &value_clauses.join(" AND ")));
1588 }
1589 where_clauses.push(format!("({})", inner_clauses.join(" OR ")));
1590 } else {
1591 for value in value_list {
1592 let mut value_clauses = vec![];
1593 value_clauses.push(format!("game.{} {} ?", game_field_name, comparator));
1594 if exact {
1595 params.push(SearchParam::String(value.clone()));
1596 } else {
1597 let p = format!("%{}%", value);
1598 params.push(SearchParam::String(p));
1599 }
1600
1601 value_clauses.push(format!(
1602 "game.id IN (SELECT gameId FROM game_data WHERE {} {} ?)",
1603 field_name, comparator
1604 ));
1605 if exact {
1606 params.push(SearchParam::String(value.clone()));
1607 } else {
1608 let p = format!("%{}%", value);
1609 params.push(SearchParam::String(p));
1610 }
1611 where_clauses.push(format!("({})", &value_clauses.join(" OR ")));
1612 }
1613 }
1614 }
1615 };
1616
1617 add_joint_game_data_clause(
1618 "applicationPath",
1619 "applicationPath",
1620 &filter.whitelist.application_path,
1621 false,
1622 false,
1623 );
1624 add_joint_game_data_clause(
1625 "applicationPath",
1626 "applicationPath",
1627 &filter.blacklist.application_path,
1628 false,
1629 true,
1630 );
1631 add_joint_game_data_clause(
1632 "applicationPath",
1633 "applicationPath",
1634 &filter.exact_whitelist.application_path,
1635 true,
1636 false,
1637 );
1638 add_joint_game_data_clause(
1639 "applicationPath",
1640 "applicationPath",
1641 &filter.exact_blacklist.application_path,
1642 true,
1643 true,
1644 );
1645
1646 add_joint_game_data_clause(
1647 "launchCommand",
1648 "launchCommand",
1649 &filter.whitelist.launch_command,
1650 false,
1651 false,
1652 );
1653 add_joint_game_data_clause(
1654 "launchCommand",
1655 "launchCommand",
1656 &filter.blacklist.launch_command,
1657 false,
1658 true,
1659 );
1660 add_joint_game_data_clause(
1661 "launchCommand",
1662 "launchCommand",
1663 &filter.exact_whitelist.launch_command,
1664 true,
1665 false,
1666 );
1667 add_joint_game_data_clause(
1668 "launchCommand",
1669 "launchCommand",
1670 &filter.exact_blacklist.launch_command,
1671 true,
1672 true,
1673 );
1674
1675 let mut add_compare_tag_clause = |field_name: &str,
1677 comparator: KeyChar,
1678 filter: &Option<i64>| {
1679 if let Some(f) = filter {
1680 if *f == 0 {
1681 match comparator {
1682 KeyChar::EQUALS => {
1683 where_clauses.push(format!(
1685 "game.id NOT IN (SELECT gameId FROM game_{}s_{})",
1686 field_name, field_name
1687 ));
1688 }
1689 KeyChar::LOWER => (),
1690 KeyChar::HIGHER => {
1691 where_clauses.push(format!(
1693 "game.id IN (SELECT gameId FROM game_{}s_{})",
1694 field_name, field_name
1695 ));
1696 }
1697 KeyChar::MATCHES => (),
1698 }
1699 } else {
1700 match comparator {
1701 KeyChar::MATCHES => (),
1702 KeyChar::LOWER => {
1703 where_clauses.push(format!("game.id NOT IN (SELECT gameId FROM game_{}s_{} GROUP BY gameId HAVING COUNT(gameId) >= ?)", field_name, field_name));
1704 params.push(SearchParam::Integer64(f.clone()));
1705 }
1706 KeyChar::HIGHER => {
1707 where_clauses.push(format!("game.id IN (SELECT gameId FROM game_{}s_{} GROUP BY gameId HAVING COUNT(gameId) > ?)", field_name, field_name));
1708 params.push(SearchParam::Integer64(f.clone()));
1709 }
1710 KeyChar::EQUALS => {
1711 where_clauses.push(format!("game.id IN (SELECT gameId FROM game_{}s_{} GROUP BY gameId HAVING COUNT(gameId) = ?)", field_name, field_name));
1712 params.push(SearchParam::Integer64(f.clone()));
1713 }
1714 }
1715 }
1716 }
1717 };
1718
1719 add_compare_tag_clause("tag", KeyChar::LOWER, &filter.lower_than.tags);
1720 add_compare_tag_clause("tag", KeyChar::HIGHER, &filter.higher_than.tags);
1721 add_compare_tag_clause("tag", KeyChar::EQUALS, &filter.equal_to.tags);
1722
1723 add_compare_tag_clause("platform", KeyChar::LOWER, &filter.lower_than.platforms);
1724 add_compare_tag_clause("platform", KeyChar::HIGHER, &filter.higher_than.platforms);
1725 add_compare_tag_clause("platform", KeyChar::EQUALS, &filter.equal_to.platforms);
1726
1727 let mut add_compare_add_app_clause = |comparator: KeyChar, filter: &Option<i64>| {
1729 if let Some(f) = filter {
1730 if *f == 0 {
1731 match comparator {
1732 KeyChar::EQUALS => {
1733 where_clauses.push(
1735 "game.id NOT IN (SELECT parentGameId FROM additional_app)".to_string(),
1736 );
1737 }
1738 KeyChar::LOWER => (),
1739 KeyChar::HIGHER => {
1740 where_clauses.push(
1742 "game.id IN (SELECT parentGameId FROM additional_app)".to_string(),
1743 );
1744 }
1745 KeyChar::MATCHES => (),
1746 }
1747 } else {
1748 match comparator {
1749 KeyChar::MATCHES => (),
1750 KeyChar::LOWER => {
1751 where_clauses.push("game.id NOT IN (SELECT parentGameId FROM additional_app GROUP BY parentGameId HAVING COUNT(parentGameId) >= ?)".to_string());
1752 params.push(SearchParam::Integer64(f.clone()));
1753 }
1754 KeyChar::HIGHER => {
1755 where_clauses.push("game.id IN (SELECT parentGameId FROM additional_app GROUP BY parentGameId HAVING COUNT(parentGameId) > ?)".to_string());
1756 params.push(SearchParam::Integer64(f.clone()));
1757 }
1758 KeyChar::EQUALS => {
1759 where_clauses.push("game.id IN (SELECT parentGameId FROM additional_app GROUP BY parentGameId HAVING COUNT(parentGameId) = ?)".to_string());
1760 params.push(SearchParam::Integer64(f.clone()));
1761 }
1762 }
1763 }
1764 }
1765 };
1766
1767 add_compare_add_app_clause(KeyChar::LOWER, &filter.lower_than.add_apps);
1768 add_compare_add_app_clause(KeyChar::HIGHER, &filter.higher_than.add_apps);
1769 add_compare_add_app_clause(KeyChar::EQUALS, &filter.equal_to.add_apps);
1770
1771 let mut add_compare_game_data_clause = |comparator: KeyChar, filter: &Option<i64>| {
1772 if let Some(f) = filter {
1773 if *f <= 0 {
1774 match comparator {
1775 KeyChar::EQUALS => {
1776 where_clauses
1778 .push("game.id NOT IN (SELECT gameId FROM game_data)".to_string());
1779 }
1780 KeyChar::LOWER => (),
1781 KeyChar::HIGHER => {
1782 where_clauses.push("game.id IN (SELECT gameId FROM game_data)".to_string());
1784 }
1785 KeyChar::MATCHES => (),
1786 }
1787 } else {
1788 match comparator {
1789 KeyChar::MATCHES => (),
1790 KeyChar::LOWER => {
1791 where_clauses.push("game.id NOT IN (SELECT gameId FROM game_data GROUP BY gameId HAVING COUNT(gameId) >= ?)".to_string());
1792 params.push(SearchParam::Integer64(f.clone()));
1793 }
1794 KeyChar::HIGHER => {
1795 where_clauses.push("game.id IN (SELECT gameId FROM game_data GROUP BY gameId HAVING COUNT(gameId) > ?)".to_string());
1796 params.push(SearchParam::Integer64(f.clone()));
1797 }
1798 KeyChar::EQUALS => {
1799 where_clauses.push("game.id IN (SELECT gameId FROM game_data GROUP BY gameId HAVING COUNT(gameId) = ?)".to_string());
1800 params.push(SearchParam::Integer64(f.clone()));
1801 }
1802 }
1803 }
1804 }
1805 };
1806
1807 add_compare_game_data_clause(KeyChar::LOWER, &filter.lower_than.game_data);
1808 add_compare_game_data_clause(KeyChar::HIGHER, &filter.higher_than.game_data);
1809 add_compare_game_data_clause(KeyChar::EQUALS, &filter.equal_to.game_data);
1810
1811 let mut add_compare_dates_clause =
1812 |date_field: &str, comparator: KeyChar, filter: &Option<String>| {
1813 if let Some(f) = filter {
1814 match comparator {
1815 KeyChar::MATCHES => (),
1816 KeyChar::LOWER => {
1817 where_clauses.push(format!("date(game.{}) < ?", date_field));
1818 params.push(SearchParam::String(f.clone()));
1819 }
1820 KeyChar::HIGHER => {
1821 where_clauses.push(format!("date(game.{}) >= ?", date_field));
1823 params.push(SearchParam::String(f.clone()));
1824 }
1825 KeyChar::EQUALS => {
1826 where_clauses.push(format!("date(game.{}) LIKE ?", date_field));
1827 let p = f.clone() + "%";
1828 params.push(SearchParam::String(p));
1829 }
1830 }
1831 }
1832 };
1833
1834 add_compare_dates_clause("dateAdded", KeyChar::LOWER, &filter.lower_than.date_added);
1835 add_compare_dates_clause("dateAdded", KeyChar::HIGHER, &filter.higher_than.date_added);
1836 add_compare_dates_clause("dateAdded", KeyChar::EQUALS, &filter.equal_to.date_added);
1837
1838 add_compare_dates_clause(
1839 "dateModified",
1840 KeyChar::LOWER,
1841 &filter.lower_than.date_modified,
1842 );
1843 add_compare_dates_clause(
1844 "dateModified",
1845 KeyChar::HIGHER,
1846 &filter.higher_than.date_modified,
1847 );
1848 add_compare_dates_clause(
1849 "dateModified",
1850 KeyChar::EQUALS,
1851 &filter.equal_to.date_modified,
1852 );
1853
1854 add_compare_dates_clause("lastPlayed", KeyChar::LOWER, &filter.lower_than.last_played);
1855 add_compare_dates_clause(
1856 "lastPlayed",
1857 KeyChar::HIGHER,
1858 &filter.higher_than.last_played,
1859 );
1860 add_compare_dates_clause("lastPlayed", KeyChar::EQUALS, &filter.equal_to.last_played);
1861
1862 let mut add_compare_dates_string_clause =
1863 |date_field: &str, comparator: KeyChar, filter: &Option<String>| {
1864 if let Some(f) = filter {
1865 match comparator {
1866 KeyChar::MATCHES => (),
1867 KeyChar::LOWER => {
1868 where_clauses.push(format!("game.{} < ?", date_field));
1869 params.push(SearchParam::String(f.clone()));
1870 }
1871 KeyChar::HIGHER => {
1872 where_clauses.push(format!("game.{} >= ?", date_field));
1874 params.push(SearchParam::String(f.clone()));
1875 }
1876 KeyChar::EQUALS => {
1877 where_clauses.push(format!("game.{} LIKE ?", date_field));
1878 let p = f.clone() + "%";
1879 params.push(SearchParam::String(p));
1880 }
1881 }
1882 }
1883 };
1884
1885 add_compare_dates_string_clause(
1886 "releaseDate",
1887 KeyChar::LOWER,
1888 &filter.lower_than.release_date,
1889 );
1890 add_compare_dates_string_clause(
1891 "releaseDate",
1892 KeyChar::HIGHER,
1893 &filter.higher_than.release_date,
1894 );
1895 add_compare_dates_string_clause(
1896 "releaseDate",
1897 KeyChar::EQUALS,
1898 &filter.equal_to.release_date,
1899 );
1900
1901 let mut add_compare_counter_clause =
1902 |counter: &str, comparator: KeyChar, filter: &Option<i64>| {
1903 if let Some(f) = filter {
1904 match comparator {
1905 KeyChar::MATCHES => (),
1906 KeyChar::LOWER => {
1907 where_clauses.push(format!("game.{} < ?", counter));
1908 params.push(SearchParam::Integer64(f.clone()));
1909 }
1910 KeyChar::HIGHER => {
1911 where_clauses.push(format!("game.{} > ?", counter));
1912 params.push(SearchParam::Integer64(f.clone()));
1913 }
1914 KeyChar::EQUALS => {
1915 where_clauses.push(format!("game.{} = ?", counter));
1916 params.push(SearchParam::Integer64(f.clone()));
1917 }
1918 }
1919 }
1920 };
1921
1922 add_compare_counter_clause("playtime", KeyChar::LOWER, &filter.lower_than.playtime);
1923 add_compare_counter_clause("playtime", KeyChar::HIGHER, &filter.higher_than.playtime);
1924 add_compare_counter_clause("playtime", KeyChar::EQUALS, &filter.equal_to.playtime);
1925
1926 add_compare_counter_clause("playCounter", KeyChar::LOWER, &filter.lower_than.playcount);
1927 add_compare_counter_clause(
1928 "playCounter",
1929 KeyChar::HIGHER,
1930 &filter.higher_than.playcount,
1931 );
1932 add_compare_counter_clause("playCounter", KeyChar::EQUALS, &filter.equal_to.playcount);
1933
1934 if let Some(val) = filter.bool_comp.installed {
1936 where_clauses.push(
1937 "game.id IN (SELECT gameId FROM game_data WHERE game_data.presentOnDisk = ?)"
1938 .to_owned(),
1939 );
1940 params.push(SearchParam::Boolean(val));
1941 }
1942
1943 let mut ext_add_clause = |values: &Option<HashMap<String, HashMap<String, Vec<String>>>>,
1946 exact: bool,
1947 blacklist: bool| {
1948 if let Some(value_list) = values {
1949 let comparator = match (blacklist, exact) {
1950 (true, true) => "!=",
1951 (true, false) => "NOT LIKE",
1952 (false, true) => "=",
1953 (false, false) => "LIKE",
1954 };
1955
1956 if exact && filter.match_any {
1958 let comparator = match blacklist {
1959 true => "NOT IN",
1960 false => "IN",
1961 };
1962 for (ext_id, comp) in value_list {
1963 for (key, value_list) in comp {
1964 where_clauses.push(
1965 format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') {} rarray(?))", key, comparator)
1966 );
1967 params.push(SearchParam::String(ext_id.clone()));
1968 params.push(SearchParam::StringVec(value_list.clone()));
1969 }
1970 }
1971 } else if blacklist {
1972 let mut inner_clauses = vec![];
1973 for (ext_id, comp) in value_list {
1974 for (key, value_list) in comp {
1975 for value in value_list {
1976 inner_clauses.push(
1977 format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') {} ?)", key, comparator)
1978 );
1979 params.push(SearchParam::String(ext_id.clone()));
1980 if exact {
1981 params.push(SearchParam::String(value.clone()));
1982 } else {
1983 let p = format!("%{}%", value);
1984 params.push(SearchParam::String(p));
1985 }
1986 }
1987 }
1988 }
1989 where_clauses.push(format!("({})", inner_clauses.join(" AND ")));
1990 } else {
1991 for (ext_id, comp) in value_list {
1992 for (key, value_list) in comp {
1993 for value in value_list {
1994 where_clauses.push(
1995 format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') {} ?)", key, comparator)
1996 );
1997 params.push(SearchParam::String(ext_id.clone()));
1998 if exact {
1999 params.push(SearchParam::String(value.clone()));
2000 } else {
2001 let p = format!("%{}%", value);
2002 params.push(SearchParam::String(p));
2003 }
2004 }
2005 }
2006 }
2007 }
2008 }
2009 };
2010
2011 ext_add_clause(&filter.whitelist.ext, false, false);
2014 ext_add_clause(&filter.blacklist.ext, false, true);
2015 ext_add_clause(&filter.exact_whitelist.ext, true, false);
2016 ext_add_clause(&filter.exact_blacklist.ext, true, true);
2017
2018 let mut ext_add_compare =
2019 |comparator: KeyChar, value: &Option<HashMap<String, HashMap<String, i64>>>| {
2020 if let Some(value_list) = value {
2021 for (ext_id, values) in value_list {
2022 for (key, f) in values {
2023 match comparator {
2024 KeyChar::EQUALS | KeyChar::MATCHES => {
2025 where_clauses.push(format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') = ?)", key).to_owned());
2026 params.push(SearchParam::String(ext_id.clone()));
2027 params.push(SearchParam::Integer64(f.clone()));
2028 },
2029 KeyChar::LOWER => {
2030 where_clauses.push(format!("game.id NOT IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') >= ?)", key).to_owned());
2031 params.push(SearchParam::String(ext_id.clone()));
2032 params.push(SearchParam::Integer64(f.clone()));
2033 }
2034 KeyChar::HIGHER => {
2035 where_clauses.push(format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') > ?)", key).to_owned());
2036 params.push(SearchParam::String(ext_id.clone()));
2037 params.push(SearchParam::Integer64(f.clone()));
2038 }
2039 }
2040 }
2041 }
2042 }
2043 };
2044
2045 ext_add_compare(KeyChar::EQUALS, &filter.equal_to.ext);
2048 ext_add_compare(KeyChar::LOWER, &filter.lower_than.ext);
2049 ext_add_compare(KeyChar::HIGHER, &filter.higher_than.ext);
2050
2051 if let Some(value_list) = &filter.bool_comp.ext {
2054 for (ext_id, comp) in value_list {
2055 for (key, value) in comp {
2056 where_clauses.push(
2057 format!("game.id IN (SELECT gameId FROM ext_data WHERE extId = ? AND JSON_EXTRACT(data, '$.{}') = ?)", key).to_owned()
2058 );
2059 params.push(SearchParam::String(ext_id.clone()));
2060 params.push(SearchParam::Boolean(value.clone()));
2061 }
2062 }
2063 }
2064
2065 where_clauses = where_clauses.into_iter().filter(|s| s != "()").collect();
2068
2069 if filter.match_any {
2070 where_clauses.join(" OR ")
2071 } else {
2072 where_clauses.join(" AND ")
2073 }
2074}
2075
2076fn format_query(query: &str, substitutions: Vec<SearchParam>) -> String {
2077 let mut formatted_query = String::new();
2078 let mut trim_mode = false;
2079 let mut indent = 0;
2080 let mut substitution_iter = substitutions.iter();
2081 let mut skip_drop = false;
2082
2083 for (idx, ch) in query.chars().enumerate() {
2084 match ch {
2085 '(' => {
2086 if idx + 1 < query.len() {
2087 let next: String = query.chars().skip(idx + 1).take(1).collect();
2088 if vec![")", "*"].contains(&next.as_str()) {
2089 formatted_query.push(ch);
2090 skip_drop = true;
2091 continue;
2092 }
2093 }
2094 indent += 4;
2095 trim_mode = true;
2096 formatted_query.push(ch);
2097 formatted_query.push('\n');
2098 }
2099 ')' => {
2100 if skip_drop {
2101 skip_drop = false;
2102 formatted_query.push(ch);
2103 continue;
2104 }
2105 trim_mode = false;
2106 indent -= 4;
2107 formatted_query.push('\n');
2108 let spaces = " ".repeat(indent);
2109 formatted_query.push_str(&spaces);
2110 formatted_query.push(ch);
2111 }
2112 '?' => {
2113 if let Some(subst) = substitution_iter.next() {
2114 let wrapped_subst = format!("'{}'", subst);
2115 formatted_query.push_str(&wrapped_subst);
2116 } else {
2117 formatted_query.push(ch);
2119 }
2120 }
2121 ' ' => {
2122 if !trim_mode {
2123 formatted_query.push(ch);
2124 }
2125 }
2126 '\n' => trim_mode = true,
2127 _ => {
2128 if trim_mode {
2129 let spaces = " ".repeat(indent);
2130 formatted_query.push_str(&spaces);
2131 trim_mode = false;
2132 }
2133 formatted_query.push(ch);
2134 }
2135 }
2136 }
2137
2138 formatted_query
2139}
2140
2141pub fn new_custom_id_order(conn: &Connection, custom_id_order: Vec<String>) -> Result<()> {
2142 let new_order = custom_id_order.join(";");
2143 let current_order = conn.query_row("SELECT IFNULL(string_agg(id, ';'), ''), ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS RowNum FROM custom_id_order ORDER BY RowNum", (), |row| row.get::<_, String>(0))?;
2144 if current_order != new_order {
2145 conn.execute("DELETE FROM custom_id_order", ())?;
2146 let mut stmt = conn.prepare("INSERT INTO custom_id_order (id) VALUES (?)")?;
2147 for id in custom_id_order {
2148 stmt.execute(params![id])?;
2149 }
2150 }
2151 Ok(())
2152}
2153
2154const REPLACEMENT: &str =
2156 "UIOWHDYUAWDGBAWYUODIGAWYUIDIAWGHDYUI8AWGHDUIAWDHNAWUIODHJNAWIOUDHJNAWOUIDAJNWMLDK";
2157
2158pub fn new_tag_filter_index(conn: &Connection, search: &mut GameSearch) -> Result<()> {
2159 rusqlite::vtab::array::load_module(conn)?;
2161
2162 search.limit = 9999999999999999;
2163 search.filter = GameFilter::default();
2164 search.filter.match_any = true;
2165
2166 if let Some(t) = search.with_tag_filter.clone() {
2167 if t.len() > 0 {
2168 search.filter.exact_blacklist.tags = Some(t);
2169 search.with_tag_filter = None;
2170 }
2171 }
2172
2173 if search.filter.exact_blacklist.tags.is_none()
2174 || search.filter.exact_blacklist.tags.clone().unwrap().len() == 0
2175 {
2176 return Ok(());
2177 }
2178
2179 let mut tags = search.filter.exact_blacklist.tags.clone().unwrap();
2180 tags.sort();
2181 let tags_key = tags.join(";");
2182
2183 let tag_filter_info = conn
2185 .query_row("SELECT key, dirty FROM tag_filter_index_info", (), |row| {
2186 Ok(TagFilterInfo {
2187 key: row.get(0)?,
2188 dirty: row.get(1)?,
2189 })
2190 })
2191 .optional()?;
2192
2193 match tag_filter_info {
2194 Some(info) => {
2195 if !info.dirty && tags_key == info.key {
2197 return Ok(());
2198 }
2199 }
2200 None => {
2201 }
2203 }
2204
2205 debug_println!("filtering {} tags", tags.len());
2206
2207 conn.execute("DELETE FROM tag_filter_index", ())?; let (query, params) = build_search_query(search, TAG_FILTER_INDEX_QUERY);
2210
2211 let params_as_refs: Vec<&dyn rusqlite::ToSql> =
2213 params.iter().map(|s| s as &dyn rusqlite::ToSql).collect();
2214
2215 debug_println!(
2216 "new filtered tag query - \n{}",
2217 format_query(&query, params.clone())
2218 );
2219
2220 let mut stmt = conn.prepare(query.as_str())?;
2221 stmt.execute(params_as_refs.as_slice())?;
2222
2223 tags.sort();
2224
2225 conn.execute("DELETE FROM tag_filter_index_info", ())?; conn.execute(
2227 "INSERT INTO tag_filter_index_info (key, dirty) VALUES (?, 0)",
2228 params![tags_key],
2229 )?;
2230
2231 Ok(())
2232}
2233
2234pub fn mark_index_dirty(conn: &Connection) -> Result<()> {
2235 conn.execute("UPDATE tag_filter_index_info SET dirty = 1", ())?;
2236 Ok(())
2237}
2238
2239#[cfg_attr(feature = "napi", napi)]
2240#[cfg_attr(not(feature = "napi"), derive(Clone))]
2241#[derive(Debug)]
2242pub enum ElementType {
2243 MODIFIER,
2244 KEY,
2245 KEYCHAR,
2246 VALUE,
2247}
2248
2249#[cfg_attr(feature = "napi", napi(object))]
2250#[derive(Debug, Clone)]
2251pub struct ElementPosition {
2252 pub element: ElementType,
2253 pub value: String,
2254 pub start: i32,
2255 pub end: i32,
2256}
2257
2258#[cfg_attr(feature = "napi", napi(object))]
2259#[derive(Debug, Clone)]
2260pub struct ParsedInput {
2261 pub search: GameSearch,
2262 pub positions: Vec<ElementPosition>,
2263}
2264
2265pub fn parse_user_input(input: &str, ext_searchables: Option<&HashMap<String, ExtSearchableRegistered>>) -> ParsedInput {
2266 let ext_searchables = match ext_searchables {
2267 Some(e) => e,
2268 None => &HashMap::new()
2269 };
2270
2271 let mut search = GameSearch::default();
2272 let mut filter = ForcedGameFilter::default();
2273
2274 let mut capturing_quotes = false;
2275 let mut working_key = String::new();
2276 let mut working_value = String::new();
2277 let mut working_key_char: Option<KeyChar> = None;
2278 let mut negative = false;
2279
2280 let mut positions = Vec::new();
2281 let mut current_pos = 0;
2282
2283 for raw_token in input.split(" ") {
2284 let mut token = raw_token.to_owned();
2286 let mut token_start = current_pos.try_into().unwrap_or(0);
2287 let mut _t = "".to_owned();
2288 debug_println!("token {}", token);
2289 if !capturing_quotes && token.len() > 1 {
2292 if token.starts_with("-") {
2294 negative = true;
2295
2296 token = token.strip_prefix("-").unwrap().to_owned();
2297 positions.push(ElementPosition {
2298 element: ElementType::MODIFIER,
2299 value: "-".to_owned(),
2300 start: token_start,
2301 end: token_start + 1,
2302 });
2303 token_start += 1;
2304 }
2305
2306 if token.len() > 1 {
2307 debug_println!("checking token start");
2308 let ch = token.chars().next().unwrap();
2310 debug_println!("start char: {}", ch);
2311 match ch {
2312 '#' => {
2313 token = token.strip_prefix('#').unwrap().to_owned();
2314 working_key = "tag".to_owned();
2315 positions.push(ElementPosition {
2316 element: ElementType::MODIFIER,
2317 value: "#".to_owned(),
2318 start: token_start,
2319 end: token_start + 1,
2320 });
2321 token_start += 1;
2322 }
2323 '!' => {
2324 token = token.strip_prefix('!').unwrap().to_owned();
2325 working_key = "platform".to_owned();
2326 positions.push(ElementPosition {
2327 element: ElementType::MODIFIER,
2328 value: "!".to_owned(),
2329 start: token_start,
2330 end: token_start + 1,
2331 });
2332 token_start += 1;
2333 }
2334 '@' => {
2335 token = token.strip_prefix('@').unwrap().to_owned();
2336 working_key = "developer".to_owned();
2337 positions.push(ElementPosition {
2338 element: ElementType::MODIFIER,
2339 value: "@".to_owned(),
2340 start: token_start,
2341 end: token_start + 1,
2342 });
2343 token_start += 1;
2344 }
2345 _ => (),
2346 }
2347 }
2348 }
2349
2350 if token.starts_with('"') {
2351 token = token.strip_prefix('"').unwrap().to_owned();
2352 capturing_quotes = true;
2354 }
2355
2356 if capturing_quotes {
2357 if working_value == "" {
2359 working_value = token.to_owned();
2361 } else {
2362 working_value.push_str(&format!(" {}", token));
2364 }
2365 }
2366
2367 if token.ends_with('"') && capturing_quotes {
2368 capturing_quotes = false;
2370 working_value = working_value.strip_suffix('"').unwrap().to_owned();
2372 }
2373
2374 if capturing_quotes {
2375 current_pos += raw_token.len() + 1;
2377 continue;
2378 }
2379
2380 if working_value == "" {
2381 working_key_char = earliest_key_char(&token);
2383
2384 if let Some(kc) = working_key_char.clone() {
2386 let s: String = kc.into();
2387 let token_parts = token.split(&s).collect::<Vec<&str>>();
2388 if token_parts.len() > 1 {
2389 debug_println!("key {:?}", &token_parts[0]);
2391 working_key = token_parts[0].to_owned();
2392 token = token_parts
2393 .into_iter()
2394 .skip(1)
2395 .collect::<Vec<&str>>()
2396 .join(&s);
2397 debug_println!("value {:?}", &token);
2398 positions.push(ElementPosition {
2399 element: ElementType::KEY,
2400 value: working_key.clone(),
2401 start: token_start,
2402 end: token_start + working_key.len().try_into().unwrap_or(0),
2403 });
2404 token_start += working_key.len().try_into().unwrap_or(0);
2405 } else {
2406 token = token_parts[0].to_owned();
2407 }
2408 }
2409
2410 if token.starts_with('"') && token.ends_with('"') {
2412 if token.len() == 2 {
2414 if working_key != "" {
2415 working_value = REPLACEMENT.to_owned();
2417 }
2418 } else {
2419 token = token.strip_prefix('"').unwrap_or_else(|| "").to_owned();
2421 token = token.strip_suffix('"').unwrap_or_else(|| "").to_owned();
2422 working_value = token.to_owned();
2423 }
2424 } else {
2425 if token.starts_with('"') {
2426 token = token.strip_prefix('"').unwrap().to_owned();
2428 capturing_quotes = true;
2429 working_value = token.to_owned();
2430 continue;
2431 }
2432 working_value = token.to_owned();
2433 }
2434 }
2435
2436 if working_value != "" {
2437 let mut exact = false;
2438 if working_key != "" {
2439 if working_value == REPLACEMENT {
2440 working_value = "".to_owned();
2442 exact = true;
2443 } else {
2444 if let Some(kc) = &working_key_char {
2445 match kc {
2446 KeyChar::EQUALS => exact = true,
2447 _ => (),
2448 }
2449 }
2450 }
2451 }
2452
2453 debug_println!(
2454 "key: {}, value: {}, negative: {}, exact: {}",
2455 working_key,
2456 working_value,
2457 negative,
2458 exact,
2459 );
2460
2461 let mut list = match (negative, exact) {
2462 (true, false) => filter.blacklist.clone(),
2463 (false, false) => filter.whitelist.clone(),
2464 (true, true) => filter.exact_blacklist.clone(),
2465 (false, true) => filter.exact_whitelist.clone(),
2466 };
2467 let value = working_value.clone();
2468
2469 if let Some(kc) = &working_key_char {
2470 positions.push(ElementPosition {
2471 element: ElementType::KEYCHAR,
2472 value: kc.to_owned().into(),
2473 start: token_start,
2474 end: token_start + 1,
2475 });
2476 token_start += 1;
2477 }
2478
2479 positions.push(ElementPosition {
2481 element: ElementType::VALUE,
2482 value: working_value.clone(),
2483 start: token_start,
2484 end: token_start + working_value.len().try_into().unwrap_or(0),
2485 });
2486
2487 let mut processed: bool = true;
2489
2490 match working_key.to_lowercase().as_str() {
2491 "installed" => {
2492 let mut value = !(working_value.to_lowercase() == "no"
2493 && working_value.to_lowercase() == "false"
2494 && working_value.to_lowercase() == "0");
2495 if negative {
2496 value = !value;
2497 }
2498
2499 filter.bool_comp.installed = Some(value);
2500 }
2501 _ => {
2502 if let Some(ext_searchable) = ext_searchables.get(working_key.to_lowercase().as_str()) {
2504 if ext_searchable.value_type == ExtSearchableType::Boolean {
2505 let mut value = !(working_value.to_lowercase() == "no"
2506 && working_value.to_lowercase() == "false"
2507 && working_value.to_lowercase() == "0");
2508 if negative {
2509 value = !value;
2510 }
2511
2512 let mut inner_filter = filter.bool_comp.ext.unwrap_or_default();
2514 let ext_filter = inner_filter.insert_or_get(ext_searchable.ext_id.clone());
2516 ext_filter.insert(ext_searchable.key.clone(), value);
2517 filter.bool_comp.ext = Some(inner_filter);
2518 } else {
2519 processed = false;
2520 }
2521 } else {
2522 processed = false;
2523 }
2524 }
2525 }
2526
2527 if !processed {
2529 if let Some(kc) = &working_key_char {
2530 processed = true;
2531 match kc {
2532 KeyChar::LOWER => {
2533 let value = coerce_to_i64(&working_value);
2534 match working_key.to_lowercase().as_str() {
2535 "tags" => filter.lower_than.tags = Some(value),
2536 "platforms" => filter.lower_than.platforms = Some(value),
2537 "dateadded" | "da" => {
2538 filter.lower_than.date_added = Some(working_value.clone())
2539 }
2540 "datemodified" | "dm" => {
2541 filter.lower_than.date_modified = Some(working_value.clone())
2542 }
2543 "releasedate" | "rd" => {
2544 filter.lower_than.release_date = Some(working_value.clone())
2545 }
2546 "gamedata" | "gd" => filter.lower_than.game_data = Some(value),
2547 "addapps" | "aa" => filter.lower_than.add_apps = Some(value),
2548 "playtime" | "pt" => filter.lower_than.playtime = Some(value),
2549 "playcount" | "pc" => filter.lower_than.playcount = Some(value),
2550 "lastplayed" | "lp" => {
2551 filter.lower_than.last_played = Some(working_value.clone())
2552 }
2553 _ => {
2554 if let Some(ext_searchable) = ext_searchables.get(working_key.to_lowercase().as_str()) {
2556 if ext_searchable.value_type == ExtSearchableType::Number {
2557 let mut inner_filter = filter.lower_than.ext.unwrap_or_default();
2559 let ext_filter = inner_filter.insert_or_get(ext_searchable.ext_id.clone());
2561 ext_filter.insert(ext_searchable.key.clone(), value);
2562 filter.lower_than.ext = Some(inner_filter);
2563 } else {
2564 processed = false;
2565 }
2566 } else {
2567 processed = false;
2568 }
2569 }
2570 }
2571 }
2572 KeyChar::HIGHER => {
2573 let value = coerce_to_i64(&working_value);
2574 match working_key.to_lowercase().as_str() {
2575 "tags" => filter.higher_than.tags = Some(value),
2576 "platforms" => filter.higher_than.platforms = Some(value),
2577 "dateadded" | "da" => {
2578 filter.higher_than.date_added = Some(working_value.clone())
2579 }
2580 "datemodified" | "dm" => {
2581 filter.higher_than.date_modified = Some(working_value.clone())
2582 }
2583 "releasedate" | "rd" => {
2584 filter.higher_than.release_date = Some(working_value.clone())
2585 }
2586 "gamedata" | "gd" => filter.higher_than.game_data = Some(value),
2587 "addapps" | "aa" => filter.higher_than.add_apps = Some(value),
2588 "playtime" | "pt" => filter.higher_than.playtime = Some(value),
2589 "playcount" | "pc" => filter.higher_than.playcount = Some(value),
2590 "lastplayed" | "lp" => {
2591 filter.higher_than.last_played = Some(working_value.clone())
2592 }
2593 _ => {
2594 if let Some(ext_searchable) = ext_searchables.get(working_key.to_lowercase().as_str()) {
2596 if ext_searchable.value_type == ExtSearchableType::Number {
2597 let mut inner_filter = filter.higher_than.ext.unwrap_or_default();
2599 let ext_filter = inner_filter.insert_or_get(ext_searchable.ext_id.clone());
2601 ext_filter.insert(ext_searchable.key.clone(), value);
2602 filter.higher_than.ext = Some(inner_filter);
2603 } else {
2604 processed = false;
2605 }
2606 } else {
2607 processed = false;
2608 }
2609 }
2610 }
2611 }
2612 KeyChar::MATCHES | KeyChar::EQUALS => {
2613 let value = coerce_to_i64(&working_value);
2614 match working_key.to_lowercase().as_str() {
2615 "tags" => filter.equal_to.tags = Some(value),
2616 "platforms" => filter.equal_to.platforms = Some(value),
2617 "dateadded" | "da" => {
2618 filter.equal_to.date_added = Some(working_value.clone())
2619 }
2620 "datemodified" | "dm" => {
2621 filter.equal_to.date_modified = Some(working_value.clone())
2622 }
2623 "releasedate" | "rd" => {
2624 filter.equal_to.release_date = Some(working_value.clone())
2625 }
2626 "gamedata" | "gd" => filter.equal_to.game_data = Some(value),
2627 "addapps" | "aa" => filter.equal_to.add_apps = Some(value),
2628 "playtime" | "pt" => filter.equal_to.playtime = Some(value),
2629 "playcount" | "pc" => filter.equal_to.playcount = Some(value),
2630 "lastplayed" | "lp" => {
2631 filter.equal_to.last_played = Some(working_value.clone())
2632 }
2633 _ => {
2634 if let Some(ext_searchable) = ext_searchables.get(working_key.to_lowercase().as_str()) {
2636 if ext_searchable.value_type == ExtSearchableType::Number {
2637 let mut inner_filter = filter.equal_to.ext.unwrap_or_default();
2639 let ext_filter = inner_filter.insert_or_get(ext_searchable.ext_id.clone());
2641 ext_filter.insert(ext_searchable.key.clone(), value);
2642 filter.equal_to.ext = Some(inner_filter);
2643 } else {
2644 processed = false;
2645 }
2646 } else {
2647 processed = false;
2648 }
2649 }
2650 }
2651 }
2652 }
2653 }
2654 }
2655
2656 if !processed {
2658 match working_key.to_lowercase().as_str() {
2660 "id" => list.id.push(value),
2661 "lib" | "library" => list.library.push(value),
2662 "title" => list.title.push(value),
2663 "dev" | "developer" => list.developer.push(value),
2664 "pub" | "publisher" => list.publisher.push(value),
2665 "series" => list.series.push(value),
2666 "tag" => list.tags.push(value),
2667 "plat" | "platform" => list.platforms.push(value),
2668 "mode" | "playmode" => list.play_mode.push(value),
2669 "status" => list.status.push(value),
2670 "note" | "notes" => list.notes.push(value),
2671 "src" | "source" => list.source.push(value),
2672 "od" | "desc" | "description" | "originaldescription" => {
2673 list.original_description.push(value)
2674 }
2675 "lang" | "language" => list.language.push(value),
2676 "ap" | "path" | "app" | "applicationpath" => list.application_path.push(value),
2677 "lc" | "launchcommand" => list.launch_command.push(value),
2678 "ruffle" | "rufflesupport" => list.ruffle_support.push(value.to_lowercase()),
2679 "owner" => list.owner.push(value),
2680 _ => {
2681 let processed = if let Some(ext_searchable) = ext_searchables.get(working_key.to_lowercase().as_str()) {
2682 if ext_searchable.value_type == ExtSearchableType::String {
2683 let ext_filter = list.ext.insert_or_get(ext_searchable.ext_id.clone());
2685 let ext_list = ext_filter.insert_or_get(ext_searchable.key.clone());
2686 ext_list.push(value.clone());
2687
2688 true
2689 } else {
2690 false
2691 }
2692 } else {
2693 false
2694 };
2695 if !processed {
2696 let value = match &working_key_char {
2698 Some(kc) => {
2699 let ks: String = kc.clone().into();
2700 let full_value = working_key.clone() + &ks + &value;
2701 full_value
2702 }
2703 None => value,
2704 };
2705
2706 list.generic.push(value);
2707 }
2708 },
2709 }
2710
2711 match (negative, exact) {
2712 (true, false) => filter.blacklist = list,
2713 (false, false) => filter.whitelist = list,
2714 (true, true) => filter.exact_blacklist = list,
2715 (false, true) => filter.exact_whitelist = list,
2716 }
2717 }
2718
2719 negative = false;
2720 working_value.clear();
2721 working_key.clear();
2722 }
2723 current_pos += raw_token.len() + 1;
2724 }
2725
2726 search.filter = (&filter).into();
2727
2728 ParsedInput { search, positions }
2729}
2730
2731#[derive(Debug, Clone, PartialEq)]
2732enum KeyChar {
2733 MATCHES,
2734 LOWER,
2735 HIGHER,
2736 EQUALS,
2737}
2738
2739impl Into<String> for KeyChar {
2740 fn into(self) -> String {
2741 match self {
2742 KeyChar::MATCHES => ":".to_owned(),
2743 KeyChar::LOWER => "<".to_owned(),
2744 KeyChar::HIGHER => ">".to_owned(),
2745 KeyChar::EQUALS => "=".to_owned(),
2746 }
2747 }
2748}
2749
2750const KEY_CHARS: [&str; 4] = [":", "<", ">", "="];
2751
2752fn earliest_key_char(s: &str) -> Option<KeyChar> {
2753 let mut earliest_pos = None;
2754 let mut earliest_key_char = None;
2755
2756 for key_char in KEY_CHARS {
2757 if let Some(pos) = s.find(key_char) {
2758 if earliest_pos.is_none() || pos < earliest_pos.unwrap() {
2759 earliest_pos = Some(pos);
2760 earliest_key_char = Some(key_char);
2761 }
2762 }
2763 }
2764
2765 match earliest_key_char {
2766 Some(ekc) => match ekc {
2767 ":" => Some(KeyChar::MATCHES),
2768 "<" => Some(KeyChar::LOWER),
2769 ">" => Some(KeyChar::HIGHER),
2770 "=" => Some(KeyChar::EQUALS),
2771 _ => None,
2772 },
2773 None => None,
2774 }
2775}
2776
2777fn coerce_to_i64(input: &str) -> i64 {
2778 let insert_plus_re = Regex::new(r"(\d+)([yMwdhm])(?=\d)").unwrap();
2786 let mut processed_input = insert_plus_re
2787 .replace_all(&input, |caps: &Captures| {
2788 format!("{}{}+", &caps[1], &caps[2])
2789 })
2790 .to_string();
2791
2792 let time_units = vec![
2793 ("y", 31_536_000), ("M", 2_592_000), ("w", 604_800), ("d", 86_400), ("h", 3_600), ("m", 60), ("s", 1), ];
2801
2802 for (unit, seconds) in time_units {
2804 let pattern = format!(r"(\d+){}", unit);
2805 let re = Regex::new(&pattern).unwrap();
2806 processed_input = re
2807 .replace_all(&processed_input, |caps: &Captures| {
2808 let time_value: i64 = caps[1].parse().unwrap_or(0); (time_value * seconds).to_string() })
2811 .to_string();
2812 }
2813
2814 match meval::eval_str(&processed_input) {
2816 Ok(num) => num as i64,
2817 Err(_) => 0,
2818 }
2819}