search-web-api 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +307 -0
- package/demo/index.ts +38 -0
- package/demo/openapi.ts +402 -0
- package/demo/routes/autocomplete.ts +124 -0
- package/demo/routes/search.ts +61 -0
- package/docs/AUTOCOMPLETE.md +296 -0
- package/docs/CATEGORY_SEARCH.md +265 -0
- package/package.json +32 -0
- package/src/autocomplete/autocomplete-ai-next-word-predictor.ts +47 -0
- package/src/autocomplete/autocomplete-search-engine-backends.ts +231 -0
- package/src/category-registry.ts +6 -0
- package/src/config/search-engine-constants.ts +216 -0
- package/src/constants.ts +6 -0
- package/src/engine-descriptions.ts +7 -0
- package/src/engine-status.ts +7 -0
- package/src/engine.ts +6 -0
- package/src/registry/search-engine-category-registry.ts +200 -0
- package/src/registry/search-engine-descriptions.ts +121 -0
- package/src/registry/search-engine-status-tracker.ts +240 -0
- package/src/result-container.ts +8 -0
- package/src/search/search-engines-registry-list.ts +205 -0
- package/src/search/search-query-executor.ts +168 -0
- package/src/search/search-result-container.ts +421 -0
- package/src/search-web-types.ts +7 -0
- package/src/search.ts +10 -0
- package/src/sources/academic/arxiv.ts +72 -0
- package/src/sources/academic/core.ts +65 -0
- package/src/sources/academic/crossref.ts +75 -0
- package/src/sources/academic/doaj.ts +63 -0
- package/src/sources/academic/google_scholar.ts +53 -0
- package/src/sources/academic/openalex.ts +75 -0
- package/src/sources/academic/pubmed.ts +96 -0
- package/src/sources/academic/semantic_scholar.ts +77 -0
- package/src/sources/academic/wikidata.ts +44 -0
- package/src/sources/general/baidu.ts +59 -0
- package/src/sources/general/bing.ts +30 -0
- package/src/sources/general/brave.ts +54 -0
- package/src/sources/general/duckduckgo.ts +49 -0
- package/src/sources/general/google.ts +68 -0
- package/src/sources/general/mojeek.ts +56 -0
- package/src/sources/general/qwant.ts +37 -0
- package/src/sources/general/startpage.ts +44 -0
- package/src/sources/general/yahoo.ts +41 -0
- package/src/sources/general/yandex.ts +56 -0
- package/src/sources/images/bing_images.ts +68 -0
- package/src/sources/images/deviantart.ts +71 -0
- package/src/sources/images/flickr.ts +132 -0
- package/src/sources/images/google_images.ts +101 -0
- package/src/sources/images/imgur.ts +58 -0
- package/src/sources/images/openclipart.ts +52 -0
- package/src/sources/images/pixabay.ts +56 -0
- package/src/sources/images/unsplash.ts +38 -0
- package/src/sources/images/wallhaven.ts +50 -0
- package/src/sources/it/crates.ts +41 -0
- package/src/sources/it/dockerhub.ts +47 -0
- package/src/sources/it/github.ts +37 -0
- package/src/sources/it/gitlab.ts +55 -0
- package/src/sources/it/npm.ts +36 -0
- package/src/sources/it/packagist.ts +43 -0
- package/src/sources/it/pypi.ts +30 -0
- package/src/sources/it/rubygems.ts +43 -0
- package/src/sources/it/stackoverflow.ts +39 -0
- package/src/sources/maps/apple_maps.ts +105 -0
- package/src/sources/maps/openstreetmap.ts +34 -0
- package/src/sources/maps/photon.ts +77 -0
- package/src/sources/news/bing_news.ts +95 -0
- package/src/sources/news/google_news.ts +80 -0
- package/src/sources/news/hackernews.ts +94 -0
- package/src/sources/news/yahoo_news.ts +77 -0
- package/src/sources/shopping/ebay.ts +96 -0
- package/src/sources/social/mastodon.ts +46 -0
- package/src/sources/social/medium.ts +52 -0
- package/src/sources/social/reddit.ts +48 -0
- package/src/sources/social/soundcloud.ts +64 -0
- package/src/sources/social/twitter.ts +56 -0
- package/src/sources/specialized/annas_archive.ts +97 -0
- package/src/sources/specialized/archive.ts +48 -0
- package/src/sources/specialized/genius.ts +43 -0
- package/src/sources/specialized/goodreads.ts +62 -0
- package/src/sources/specialized/imdb.ts +55 -0
- package/src/sources/specialized/openlibrary.ts +59 -0
- package/src/sources/specialized/wikipedia.ts +37 -0
- package/src/sources/specialized/wttr.ts +98 -0
- package/src/sources/torrents/1337x.ts +43 -0
- package/src/sources/torrents/eztv.ts +47 -0
- package/src/sources/torrents/kickass.ts +63 -0
- package/src/sources/torrents/nyaa.ts +53 -0
- package/src/sources/torrents/solidtorrents.ts +68 -0
- package/src/sources/torrents/thepiratebay.ts +57 -0
- package/src/sources/torrents/yts.ts +54 -0
- package/src/sources/videos/bing_videos.ts +91 -0
- package/src/sources/videos/dailymotion.ts +101 -0
- package/src/sources/videos/invidious.ts +86 -0
- package/src/sources/videos/peertube.ts +76 -0
- package/src/sources/videos/vimeo.ts +67 -0
- package/src/sources/videos/youtube.ts +78 -0
- package/src/suggest-next-words/autocomplete-ai.ts +38 -0
- package/src/suggest-next-words/autocomplete-search-engines.ts +384 -0
- package/src/suggest-next-words/misspelled-typos-8k.json +1 -0
- package/src/types/search-engine-interface.ts +27 -0
- package/src/types/search-result-types.ts +405 -0
- package/test/api.test.ts +128 -0
- package/test/autocomplete-ai.test.ts +20 -0
- package/test/autocomplete-engines.test.ts +131 -0
- package/test/engine-health-suite.test.ts +350 -0
- package/test/search.test.ts +69 -0
- package/test/sources-unit.test.ts +1152 -0
- package/test/sources.test.ts +182 -0
- package/test/test-utils.ts +81 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const stackoverflow: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
order: "desc",
|
|
9
|
+
sort: "relevance",
|
|
10
|
+
q: query,
|
|
11
|
+
site: "stackoverflow",
|
|
12
|
+
page: String(page || 1),
|
|
13
|
+
pagesize: "10",
|
|
14
|
+
});
|
|
15
|
+
const response = await fetch(
|
|
16
|
+
`https://api.stackexchange.com/2.3/search?${params}`,
|
|
17
|
+
{
|
|
18
|
+
headers: {
|
|
19
|
+
"User-Agent": "HonoxSearX/1.0",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
if (!response.ok) return [];
|
|
24
|
+
const data = await response.json();
|
|
25
|
+
const results: any[] = [];
|
|
26
|
+
|
|
27
|
+
if (data && data.items) {
|
|
28
|
+
data.items.forEach((item: any) => {
|
|
29
|
+
results.push({
|
|
30
|
+
url: item.link,
|
|
31
|
+
title: item.title,
|
|
32
|
+
content: item.tags ? `Tags: ${item.tags.join(", ")}` : "",
|
|
33
|
+
engine: "stackoverflow",
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return results;
|
|
39
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
// Token management for Apple Maps API
|
|
4
|
+
let tokenCache = {
|
|
5
|
+
value: "",
|
|
6
|
+
lastUpdated: 0,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
async function obtainToken(): Promise<string> {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
|
|
12
|
+
// Token expires every 30 minutes (1800 seconds)
|
|
13
|
+
if (tokenCache.value && now - tokenCache.lastUpdated < 1800000) {
|
|
14
|
+
return tokenCache.value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Get DuckDuckGo's MapKit token
|
|
19
|
+
const tokenResponse = await fetch(
|
|
20
|
+
"https://duckduckgo.com/local.js?get_mk_token=1"
|
|
21
|
+
);
|
|
22
|
+
const tokenText = await tokenResponse.text();
|
|
23
|
+
|
|
24
|
+
// Exchange for actual Apple Maps token
|
|
25
|
+
const actualTokenResponse = await fetch(
|
|
26
|
+
"https://cdn.apple-mapkit.com/ma/bootstrap?apiVersion=2&mkjsVersion=5.72.53&poi=1",
|
|
27
|
+
{
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${tokenText}`,
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
const actualTokenData = await actualTokenResponse.json();
|
|
34
|
+
|
|
35
|
+
tokenCache.value = actualTokenData.authInfo.access_token;
|
|
36
|
+
tokenCache.lastUpdated = now;
|
|
37
|
+
return tokenCache.value;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const apple_maps: EngineFunction = async (
|
|
44
|
+
query: string,
|
|
45
|
+
page: number | undefined
|
|
46
|
+
) => {
|
|
47
|
+
const token = await obtainToken();
|
|
48
|
+
|
|
49
|
+
if (!token) {
|
|
50
|
+
throw new Error("Failed to obtain Apple Maps API token");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const params = new URLSearchParams({
|
|
54
|
+
q: query,
|
|
55
|
+
lang: "en",
|
|
56
|
+
mkjsVersion: "5.72.53",
|
|
57
|
+
});
|
|
58
|
+
const response = await fetch(
|
|
59
|
+
`https://api.apple-mapkit.com/v1/search?${params}`,
|
|
60
|
+
{
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${token}`,
|
|
63
|
+
"User-Agent":
|
|
64
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
if (!response.ok) return [];
|
|
69
|
+
const json = await response.json();
|
|
70
|
+
|
|
71
|
+
if (!json || !json.results) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return json.results.map((result: any) => {
|
|
76
|
+
const lat = result.center?.lat;
|
|
77
|
+
const lng = result.center?.lng;
|
|
78
|
+
|
|
79
|
+
const addressParts = [
|
|
80
|
+
result.subThoroughfare,
|
|
81
|
+
result.thoroughfare,
|
|
82
|
+
result.locality,
|
|
83
|
+
result.postCode,
|
|
84
|
+
result.country,
|
|
85
|
+
].filter(Boolean);
|
|
86
|
+
|
|
87
|
+
const content = [
|
|
88
|
+
addressParts.join(", "),
|
|
89
|
+
result.poiCategory ? `Type: ${result.poiCategory}` : "",
|
|
90
|
+
lat && lng ? `Coordinates: ${lat}, ${lng}` : "",
|
|
91
|
+
result.telephone ? `Phone: ${result.telephone}` : "",
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join("\n");
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
url: result.placecardUrl || result.urls?.[0] || "",
|
|
98
|
+
title: result.name,
|
|
99
|
+
content,
|
|
100
|
+
engine: "apple_maps",
|
|
101
|
+
latitude: lat,
|
|
102
|
+
longitude: lng,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const openstreetmap: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
q: query,
|
|
9
|
+
format: "json",
|
|
10
|
+
addressdetails: "1",
|
|
11
|
+
limit: "10",
|
|
12
|
+
});
|
|
13
|
+
const response = await fetch(
|
|
14
|
+
`https://nominatim.openstreetmap.org/search?${params}`,
|
|
15
|
+
{
|
|
16
|
+
headers: {
|
|
17
|
+
"User-Agent": "HonoxSearX/1.0",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
if (!response.ok) return [];
|
|
22
|
+
const json = await response.json();
|
|
23
|
+
|
|
24
|
+
if (!Array.isArray(json)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return json.map((item: any) => ({
|
|
29
|
+
url: `https://www.openstreetmap.org/${item.osm_type}/${item.osm_id}`,
|
|
30
|
+
title: item.display_name,
|
|
31
|
+
content: `Type: ${item.type}, Class: ${item.class}`,
|
|
32
|
+
engine: "openstreetmap",
|
|
33
|
+
}));
|
|
34
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const photon: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
q: query,
|
|
9
|
+
limit: "10",
|
|
10
|
+
lang: "en",
|
|
11
|
+
});
|
|
12
|
+
const response = await fetch(
|
|
13
|
+
`https://photon.komoot.io/api/?${params}`,
|
|
14
|
+
{
|
|
15
|
+
headers: {
|
|
16
|
+
"User-Agent":
|
|
17
|
+
"Mozilla/5.0 (X1; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
if (!response.ok) return [];
|
|
22
|
+
const json = await response.json();
|
|
23
|
+
const features = json?.features || [];
|
|
24
|
+
|
|
25
|
+
return features
|
|
26
|
+
.map((feature: any) => {
|
|
27
|
+
const properties = feature.properties;
|
|
28
|
+
|
|
29
|
+
if (!properties) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const name = properties.name || "";
|
|
34
|
+
const osmType = properties.osm_type;
|
|
35
|
+
|
|
36
|
+
// Map OSM type to string
|
|
37
|
+
let osmTypeStr = "";
|
|
38
|
+
if (osmType === "N") osmTypeStr = "node";
|
|
39
|
+
else if (osmType === "W") osmTypeStr = "way";
|
|
40
|
+
else if (osmType === "R") osmTypeStr = "relation";
|
|
41
|
+
else return null;
|
|
42
|
+
|
|
43
|
+
const osmId = properties.osm_id;
|
|
44
|
+
const url = `https://openstreetmap.org/${osmTypeStr}/${osmId}`;
|
|
45
|
+
|
|
46
|
+
// Build address
|
|
47
|
+
const addressParts: string[] = [];
|
|
48
|
+
if (properties.street) addressParts.push(properties.street);
|
|
49
|
+
if (properties.housenumber)
|
|
50
|
+
addressParts.push(properties.housenumber);
|
|
51
|
+
if (properties.city) addressParts.push(properties.city);
|
|
52
|
+
if (properties.postcode) addressParts.push(properties.postcode);
|
|
53
|
+
if (properties.country) addressParts.push(properties.country);
|
|
54
|
+
|
|
55
|
+
// Get coordinates
|
|
56
|
+
const geometry = feature.geometry;
|
|
57
|
+
const coordinates = geometry?.coordinates || [];
|
|
58
|
+
const lon = coordinates[0];
|
|
59
|
+
const lat = coordinates[1];
|
|
60
|
+
|
|
61
|
+
const content = [
|
|
62
|
+
addressParts.join(", "),
|
|
63
|
+
properties.type ? `Type: ${properties.type}` : "",
|
|
64
|
+
coordinates.length === 2 ? `Coordinates: ${lat}, ${lon}` : "",
|
|
65
|
+
]
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join("\n");
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
url,
|
|
71
|
+
title: name,
|
|
72
|
+
content,
|
|
73
|
+
engine: "photon",
|
|
74
|
+
};
|
|
75
|
+
})
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const bing_news: EngineFunction = async (
|
|
5
|
+
q: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
q,
|
|
10
|
+
InfiniteScroll: "1",
|
|
11
|
+
first: String(((page || 1) - 1) * 10 + 1),
|
|
12
|
+
SFX: String((page || 1) - 1),
|
|
13
|
+
form: "PTFTNR",
|
|
14
|
+
setlang: "en",
|
|
15
|
+
cc: "US",
|
|
16
|
+
});
|
|
17
|
+
const response = await fetch(
|
|
18
|
+
`https://www.bing.com/news/infinitescrollajax?${params}`,
|
|
19
|
+
{
|
|
20
|
+
headers: {
|
|
21
|
+
Accept:
|
|
22
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
23
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
if (!response.ok) return [];
|
|
28
|
+
const html = await response.text();
|
|
29
|
+
const { document } = parseHTML(html);
|
|
30
|
+
const results: any[] = [];
|
|
31
|
+
|
|
32
|
+
// Parse news items from Bing News
|
|
33
|
+
document
|
|
34
|
+
.querySelectorAll('div.newsitem, div[class*="newsitem"]')
|
|
35
|
+
.forEach((el) => {
|
|
36
|
+
const element = el;
|
|
37
|
+
|
|
38
|
+
const link = element.querySelector('a.title, a[class*="title"]');
|
|
39
|
+
if (!link) return;
|
|
40
|
+
|
|
41
|
+
const url = link.getAttribute("href");
|
|
42
|
+
const title = link.textContent?.trim() || "";
|
|
43
|
+
const content =
|
|
44
|
+
element
|
|
45
|
+
.querySelector('div.snippet, div[class*="snippet"]')
|
|
46
|
+
?.textContent?.trim() || "";
|
|
47
|
+
|
|
48
|
+
if (!url || !title) {
|
|
49
|
+
return; // continue to next iteration
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Extract metadata (source, time, author)
|
|
53
|
+
const metadata: string[] = [];
|
|
54
|
+
const source = element.querySelector(
|
|
55
|
+
'div.source, div[class*="source"]'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (source) {
|
|
59
|
+
const ariaLabel = source
|
|
60
|
+
.querySelector("span[aria-label]")
|
|
61
|
+
?.getAttribute("aria-label");
|
|
62
|
+
const author = link.getAttribute("data-author");
|
|
63
|
+
|
|
64
|
+
if (ariaLabel) {
|
|
65
|
+
metadata.push(ariaLabel);
|
|
66
|
+
}
|
|
67
|
+
if (author) {
|
|
68
|
+
metadata.push(author);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract thumbnail
|
|
73
|
+
let thumbnail =
|
|
74
|
+
element
|
|
75
|
+
.querySelector('a.imagelink img, a[class*="imagelink"] img')
|
|
76
|
+
?.getAttribute("src") || undefined;
|
|
77
|
+
if (thumbnail && !thumbnail.startsWith("https://www.bing.com")) {
|
|
78
|
+
thumbnail = "https://www.bing.com/" + thumbnail;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const metadataStr = metadata.length > 0 ? metadata.join(" | ") : "";
|
|
82
|
+
const fullContent = metadataStr
|
|
83
|
+
? `${metadataStr}\n${content}`
|
|
84
|
+
: content;
|
|
85
|
+
|
|
86
|
+
results.push({
|
|
87
|
+
url,
|
|
88
|
+
title,
|
|
89
|
+
content: fullContent,
|
|
90
|
+
engine: "bing_news",
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return results;
|
|
95
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const google_news: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
q: query,
|
|
10
|
+
hl: "en",
|
|
11
|
+
gl: "US",
|
|
12
|
+
ceid: "US:en",
|
|
13
|
+
});
|
|
14
|
+
const response = await fetch(
|
|
15
|
+
`https://news.google.com/search?${params}`,
|
|
16
|
+
{
|
|
17
|
+
headers: {
|
|
18
|
+
"User-Agent":
|
|
19
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
20
|
+
Cookie: "CONSENT=YES+; SOCS=CAESBQgYEgAgAA==",
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
if (!response.ok) return [];
|
|
25
|
+
const htmlString = await response.text();
|
|
26
|
+
const results: any[] = [];
|
|
27
|
+
|
|
28
|
+
if (!htmlString || typeof htmlString !== "string") {
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { document } = parseHTML(htmlString);
|
|
33
|
+
|
|
34
|
+
document.querySelectorAll("div.xrnccd").forEach((element) => {
|
|
35
|
+
const elElem = element;
|
|
36
|
+
|
|
37
|
+
// Extract the article link
|
|
38
|
+
const href = elElem.querySelector("article a")?.getAttribute("href");
|
|
39
|
+
if (!href) return;
|
|
40
|
+
|
|
41
|
+
// Decode the Google News internal link
|
|
42
|
+
try {
|
|
43
|
+
const hrefParts = href.split("/");
|
|
44
|
+
const encodedUrl = hrefParts[hrefParts.length - 1].split("?")[0];
|
|
45
|
+
const decodedBytes = Buffer.from(encodedUrl + "====", "base64url");
|
|
46
|
+
const decodedStr = decodedBytes.toString("utf-8");
|
|
47
|
+
const httpIndex = decodedStr.indexOf("http");
|
|
48
|
+
if (httpIndex === -1) return;
|
|
49
|
+
const actualUrl = decodedStr.slice(httpIndex).split("\xd2")[0];
|
|
50
|
+
|
|
51
|
+
const title =
|
|
52
|
+
elElem.querySelector("article h3")?.textContent?.trim() || "";
|
|
53
|
+
const pubDate =
|
|
54
|
+
elElem.querySelector("article time")?.textContent?.trim() || "";
|
|
55
|
+
const pubOrigin =
|
|
56
|
+
elElem
|
|
57
|
+
.querySelector("article a[data-n-tid]")
|
|
58
|
+
?.textContent?.trim() || "";
|
|
59
|
+
const thumbnail =
|
|
60
|
+
elElem.previousElementSibling
|
|
61
|
+
?.querySelector("figure img")
|
|
62
|
+
?.getAttribute("src") || undefined;
|
|
63
|
+
|
|
64
|
+
const content = [pubOrigin, pubDate].filter(Boolean).join(" / ");
|
|
65
|
+
|
|
66
|
+
results.push({
|
|
67
|
+
url: actualUrl,
|
|
68
|
+
title,
|
|
69
|
+
content,
|
|
70
|
+
engine: "google_news",
|
|
71
|
+
thumbnail,
|
|
72
|
+
});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Skip malformed URLs
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return results;
|
|
80
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const hackernews: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const pageno = page || 1;
|
|
8
|
+
const resultsPerPage = 30;
|
|
9
|
+
|
|
10
|
+
let searchType = "search";
|
|
11
|
+
let queryParams: Record<string, any> = {
|
|
12
|
+
page: pageno - 1,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (!query || query.trim() === "") {
|
|
16
|
+
// If search query is empty, show results from HN's front page
|
|
17
|
+
searchType = "search_by_date";
|
|
18
|
+
queryParams.tags = "front_page";
|
|
19
|
+
} else {
|
|
20
|
+
queryParams = {
|
|
21
|
+
query,
|
|
22
|
+
page: pageno - 1,
|
|
23
|
+
hitsPerPage: resultsPerPage,
|
|
24
|
+
minWordSizefor1Typo: 4,
|
|
25
|
+
minWordSizefor2Typos: 8,
|
|
26
|
+
advancedSyntax: true,
|
|
27
|
+
ignorePlurals: false,
|
|
28
|
+
minProximity: 7,
|
|
29
|
+
numericFilters: "[]",
|
|
30
|
+
tagFilters: '["story",[]]',
|
|
31
|
+
typoTolerance: true,
|
|
32
|
+
queryType: "prefixLast",
|
|
33
|
+
restrictSearchableAttributes:
|
|
34
|
+
'["title","comment_text","url","story_text","author"]',
|
|
35
|
+
getRankingInfo: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const searchParams = new URLSearchParams();
|
|
40
|
+
Object.entries(queryParams).forEach(([key, value]) => {
|
|
41
|
+
searchParams.append(key, String(value));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const response = await fetch(
|
|
45
|
+
`https://hn.algolia.com/api/v1/${searchType}?${searchParams.toString()}`,
|
|
46
|
+
{
|
|
47
|
+
headers: {
|
|
48
|
+
"User-Agent":
|
|
49
|
+
"Mozilla/5.0 (X1; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
if (!response.ok) return [];
|
|
54
|
+
const jsonData = await response.json();
|
|
55
|
+
const results: any[] = [];
|
|
56
|
+
|
|
57
|
+
if (!jsonData?.hits) {
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const hit of jsonData.hits) {
|
|
62
|
+
const objectId = hit.objectID;
|
|
63
|
+
const points = hit.points || 0;
|
|
64
|
+
const numComments = hit.num_comments || 0;
|
|
65
|
+
|
|
66
|
+
// Extract content from various possible fields
|
|
67
|
+
let content = hit.url || hit.comment_text || hit.story_text || "";
|
|
68
|
+
|
|
69
|
+
// Strip HTML tags from content if present
|
|
70
|
+
if (content) {
|
|
71
|
+
content = content.replace(/<[^>]*>/g, "").trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Build metadata string
|
|
75
|
+
let metadata = "";
|
|
76
|
+
if (points !== 0 || numComments !== 0) {
|
|
77
|
+
metadata = `points: ${points} | comments: ${numComments}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Combine content and metadata
|
|
81
|
+
const fullContent = metadata ? `${metadata}\n${content}` : content;
|
|
82
|
+
|
|
83
|
+
results.push({
|
|
84
|
+
title: hit.title || `author: ${hit.author}`,
|
|
85
|
+
url: `https://news.ycombinator.com/item?id=${objectId}`,
|
|
86
|
+
content: fullContent,
|
|
87
|
+
points,
|
|
88
|
+
numComments,
|
|
89
|
+
engine: "hackernews",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return results;
|
|
94
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const yahoo_news: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const url = `https://news.search.yahoo.com/search?${new URLSearchParams({
|
|
9
|
+
p: query,
|
|
10
|
+
}).toString()}&b=${((page || 1) - 1) * 10 + 1}`;
|
|
11
|
+
|
|
12
|
+
const response = await fetch(url, {
|
|
13
|
+
headers: {
|
|
14
|
+
"User-Agent":
|
|
15
|
+
"Mozilla/5.0 (X1; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
16
|
+
Accept:
|
|
17
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
18
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) return [];
|
|
22
|
+
const html = await response.text();
|
|
23
|
+
const { document } = parseHTML(html);
|
|
24
|
+
const results: any[] = [];
|
|
25
|
+
|
|
26
|
+
// Parse results from Yahoo News search
|
|
27
|
+
document
|
|
28
|
+
.querySelectorAll('ol.searchCenterMiddle li, ol[class*="reg"] li')
|
|
29
|
+
.forEach((el) => {
|
|
30
|
+
const element = el;
|
|
31
|
+
|
|
32
|
+
const link = element.querySelector("h4 a, .fz-16 a, .title a");
|
|
33
|
+
let url = link?.getAttribute("href");
|
|
34
|
+
|
|
35
|
+
if (!url) {
|
|
36
|
+
return; // continue to next iteration
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Parse Yahoo's redirect URL to get the real URL
|
|
40
|
+
// Yahoo URLs are like: https://r.search.yahoo.com/_ylt=...;_ylu=.../RU=.../RK=.../RS=...
|
|
41
|
+
if (url.includes("r.search.yahoo.com") || url.includes("/RU=")) {
|
|
42
|
+
try {
|
|
43
|
+
const urlObj = new URL(url);
|
|
44
|
+
const ruMatch = url.match(/\/RU=([^/]+)/);
|
|
45
|
+
if (ruMatch) {
|
|
46
|
+
url = decodeURIComponent(ruMatch[1]);
|
|
47
|
+
} else if (urlObj.searchParams.has("url")) {
|
|
48
|
+
url = urlObj.searchParams.get("url") || url;
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// If parsing fails, use URL as-is
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const title = link?.textContent?.trim() || "";
|
|
56
|
+
const content =
|
|
57
|
+
element
|
|
58
|
+
.querySelector('p, .compText, div[class*="desc"]')
|
|
59
|
+
?.textContent?.trim() || "";
|
|
60
|
+
const thumbnail =
|
|
61
|
+
element.querySelector("img")?.getAttribute("data-src") ||
|
|
62
|
+
element.querySelector("img")?.getAttribute("src") ||
|
|
63
|
+
"";
|
|
64
|
+
|
|
65
|
+
if (url && title) {
|
|
66
|
+
results.push({
|
|
67
|
+
url,
|
|
68
|
+
title,
|
|
69
|
+
content,
|
|
70
|
+
thumbnail,
|
|
71
|
+
engine: "yahoo_news",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return results;
|
|
77
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* eBay Shopping Search Engine
|
|
6
|
+
*
|
|
7
|
+
* Searches eBay marketplace for products, including:
|
|
8
|
+
* - Product listings
|
|
9
|
+
* - Prices and shipping info
|
|
10
|
+
* - Images and thumbnails
|
|
11
|
+
* - Source country information
|
|
12
|
+
*/
|
|
13
|
+
export const ebay: EngineFunction = async (
|
|
14
|
+
query: string,
|
|
15
|
+
page: number | undefined
|
|
16
|
+
) => {
|
|
17
|
+
const params = new URLSearchParams({
|
|
18
|
+
_nkw: query,
|
|
19
|
+
_sacat: String(page || 1),
|
|
20
|
+
});
|
|
21
|
+
const response = await fetch(
|
|
22
|
+
`https://www.ebay.com/sch/i.html?${params}`,
|
|
23
|
+
{
|
|
24
|
+
headers: {
|
|
25
|
+
Accept:
|
|
26
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
27
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
if (!response.ok) return [];
|
|
32
|
+
const html = await response.text();
|
|
33
|
+
const { document } = parseHTML(html);
|
|
34
|
+
const results: any[] = [];
|
|
35
|
+
|
|
36
|
+
// XPath equivalent: //li[contains(@class, "s-item")]
|
|
37
|
+
const resultels = document.querySelectorAll(
|
|
38
|
+
'li.s-item, li[class*="s-item"]'
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
for (const el of Array.from(resultels)) {
|
|
42
|
+
try {
|
|
43
|
+
// Extract data from DOM
|
|
44
|
+
const linkel = el.querySelector("a.s-item__link");
|
|
45
|
+
const url = linkel?.getAttribute("href");
|
|
46
|
+
|
|
47
|
+
const titleel = el.querySelector("h3.s-item__title");
|
|
48
|
+
const title = titleel?.textContent?.trim();
|
|
49
|
+
|
|
50
|
+
const contentel = el.querySelector('div[span="SECONDARY_INFO"]');
|
|
51
|
+
const content = contentel?.textContent?.trim();
|
|
52
|
+
|
|
53
|
+
const priceel = el.querySelector(
|
|
54
|
+
"div.s-item__detail span.s-item__price, span.s-item__price"
|
|
55
|
+
);
|
|
56
|
+
const price = priceel?.textContent?.trim();
|
|
57
|
+
|
|
58
|
+
const shippingel = el.querySelector(
|
|
59
|
+
'span.s-item__shipping, span[class*="s-item__shipping"]'
|
|
60
|
+
);
|
|
61
|
+
const shipping = shippingel?.textContent?.trim();
|
|
62
|
+
|
|
63
|
+
const locationel = el.querySelector(
|
|
64
|
+
'span.s-item__location, span[class*="s-item__location"]'
|
|
65
|
+
);
|
|
66
|
+
const source_country = locationel?.textContent?.trim();
|
|
67
|
+
|
|
68
|
+
const thumbnailel = el.querySelector("img.s-item__image-img");
|
|
69
|
+
const thumbnail = thumbnailel?.getAttribute("src");
|
|
70
|
+
|
|
71
|
+
// Skip if no title
|
|
72
|
+
if (!title || title === "") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result: any = {
|
|
77
|
+
url: url || "",
|
|
78
|
+
title,
|
|
79
|
+
content: content || "",
|
|
80
|
+
price,
|
|
81
|
+
shipping,
|
|
82
|
+
thumbnail,
|
|
83
|
+
source_country,
|
|
84
|
+
template: "products",
|
|
85
|
+
engine: "ebay",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
results.push(result);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Skip malformed results
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return results;
|
|
96
|
+
};
|