summaryrefslogtreecommitdiff
path: root/qml_plugin/src/web/duckduckgo.rs
diff options
context:
space:
mode:
authorlittleblack111 <littleblack11111@gmail.com>2026-07-31 21:59:42 +0800
committerlittleblack111 <littleblack11111@gmail.com>2026-07-31 22:23:29 +0800
commit1d6da847b7075b4babf3409b9445db0b54677893 (patch)
tree1dd6141f2450bef7d35cd3bd36331b5b258f5e85 /qml_plugin/src/web/duckduckgo.rs
parent586f32bb99c7539f2d89ac617b86a62166b30993 (diff)
qml_plugin: duckduckgo
Diffstat (limited to 'qml_plugin/src/web/duckduckgo.rs')
-rw-r--r--qml_plugin/src/web/duckduckgo.rs127
1 files changed, 127 insertions, 0 deletions
diff --git a/qml_plugin/src/web/duckduckgo.rs b/qml_plugin/src/web/duckduckgo.rs
new file mode 100644
index 0000000..f80e4c8
--- /dev/null
+++ b/qml_plugin/src/web/duckduckgo.rs
@@ -0,0 +1,127 @@
+use std::ops::{Deref, DerefMut};
+
+use duckduckgo::{
+ browser::Browser,
+ params::{SafeSearch, SearchParams, Toggle},
+};
+use reqwest::Url;
+use serde::Deserialize;
+
+const DUCKDUCKGO: &str = "https://duckduckgo.com";
+
+pub struct DuckDuckGoContent {
+ title: String,
+ description_html: String,
+ preview_image: Option<Url>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Infobox {
+ pub content: Vec<InfoboxContent>,
+}
+
+impl Deref for Infobox {
+ type Target = Vec<InfoboxContent>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.content
+ }
+}
+
+impl DerefMut for Infobox {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.content
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub struct InfoboxContent {
+ pub data_type: String,
+ pub label: String,
+ pub value: InfoboxValue,
+ pub wiki_order: String, // Kept as String since the JSON had "101" in quotes
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(untagged)]
+pub enum InfoboxValue {
+ Entity(EntityValue),
+ String(String),
+}
+
+#[derive(Debug, Deserialize)]
+pub struct EntityValue {
+ #[serde(rename = "entity-type")]
+ pub entity_type: String,
+ pub id: String,
+ #[serde(rename = "numeric-id")]
+ pub numeric_id: u32,
+}
+
+pub async fn query(input: &str) -> anyhow::Result<DuckDuckGoContent> {
+ let resp = Browser::new()
+ .get_api_response(
+ input,
+ Some(
+ &SearchParams::new()
+ .safe_search(SafeSearch::Off)
+ .full_urls(Toggle::On),
+ ),
+ )
+ .await?;
+
+ let infobox = match resp
+ .info_box
+ .map(serde_json::from_value::<Infobox>)
+ {
+ Some(Ok(o)) => Some(o),
+ _ => None,
+ };
+
+ let alt_desc = resp
+ .entity
+ .unwrap_or(
+ match infobox {
+ Some(i) => i
+ .iter()
+ .find(|i| i.data_type == "wd_description" || i.data_type == "official_website")
+ .map_or(
+ String::new(),
+ |i| {
+ if i.data_type == "official_website" {
+ i.label
+ .clone()
+ } else {
+ match i.value {
+ InfoboxValue::Entity(ref entity_value) => unreachable!(),
+ InfoboxValue::String(ref s) => s.to_string(),
+ }
+ }
+ },
+ ),
+ // TODO: find better way to get title from infobox
+ None => String::new(),
+ },
+ );
+
+ Ok(
+ DuckDuckGoContent {
+ title: resp
+ .heading
+ .unwrap_or(
+ resp.definition
+ .unwrap_or(alt_desc.clone()),
+ ),
+ description_html: resp
+ .answer
+ .unwrap_or(
+ resp.r#abstract
+ .unwrap_or(alt_desc),
+ ),
+ // TODO: try also Results[*].Icon.URL and RelatedTopics[*].Icon.URL
+ preview_image: resp
+ .image
+ .and_then(|i| Url::parse(&(DUCKDUCKGO.to_owned() + &i)).ok()),
+ },
+ )
+}