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.
Files changed (111) hide show
  1. package/README.md +307 -0
  2. package/demo/index.ts +38 -0
  3. package/demo/openapi.ts +402 -0
  4. package/demo/routes/autocomplete.ts +124 -0
  5. package/demo/routes/search.ts +61 -0
  6. package/docs/AUTOCOMPLETE.md +296 -0
  7. package/docs/CATEGORY_SEARCH.md +265 -0
  8. package/package.json +32 -0
  9. package/src/autocomplete/autocomplete-ai-next-word-predictor.ts +47 -0
  10. package/src/autocomplete/autocomplete-search-engine-backends.ts +231 -0
  11. package/src/category-registry.ts +6 -0
  12. package/src/config/search-engine-constants.ts +216 -0
  13. package/src/constants.ts +6 -0
  14. package/src/engine-descriptions.ts +7 -0
  15. package/src/engine-status.ts +7 -0
  16. package/src/engine.ts +6 -0
  17. package/src/registry/search-engine-category-registry.ts +200 -0
  18. package/src/registry/search-engine-descriptions.ts +121 -0
  19. package/src/registry/search-engine-status-tracker.ts +240 -0
  20. package/src/result-container.ts +8 -0
  21. package/src/search/search-engines-registry-list.ts +205 -0
  22. package/src/search/search-query-executor.ts +168 -0
  23. package/src/search/search-result-container.ts +421 -0
  24. package/src/search-web-types.ts +7 -0
  25. package/src/search.ts +10 -0
  26. package/src/sources/academic/arxiv.ts +72 -0
  27. package/src/sources/academic/core.ts +65 -0
  28. package/src/sources/academic/crossref.ts +75 -0
  29. package/src/sources/academic/doaj.ts +63 -0
  30. package/src/sources/academic/google_scholar.ts +53 -0
  31. package/src/sources/academic/openalex.ts +75 -0
  32. package/src/sources/academic/pubmed.ts +96 -0
  33. package/src/sources/academic/semantic_scholar.ts +77 -0
  34. package/src/sources/academic/wikidata.ts +44 -0
  35. package/src/sources/general/baidu.ts +59 -0
  36. package/src/sources/general/bing.ts +30 -0
  37. package/src/sources/general/brave.ts +54 -0
  38. package/src/sources/general/duckduckgo.ts +49 -0
  39. package/src/sources/general/google.ts +68 -0
  40. package/src/sources/general/mojeek.ts +56 -0
  41. package/src/sources/general/qwant.ts +37 -0
  42. package/src/sources/general/startpage.ts +44 -0
  43. package/src/sources/general/yahoo.ts +41 -0
  44. package/src/sources/general/yandex.ts +56 -0
  45. package/src/sources/images/bing_images.ts +68 -0
  46. package/src/sources/images/deviantart.ts +71 -0
  47. package/src/sources/images/flickr.ts +132 -0
  48. package/src/sources/images/google_images.ts +101 -0
  49. package/src/sources/images/imgur.ts +58 -0
  50. package/src/sources/images/openclipart.ts +52 -0
  51. package/src/sources/images/pixabay.ts +56 -0
  52. package/src/sources/images/unsplash.ts +38 -0
  53. package/src/sources/images/wallhaven.ts +50 -0
  54. package/src/sources/it/crates.ts +41 -0
  55. package/src/sources/it/dockerhub.ts +47 -0
  56. package/src/sources/it/github.ts +37 -0
  57. package/src/sources/it/gitlab.ts +55 -0
  58. package/src/sources/it/npm.ts +36 -0
  59. package/src/sources/it/packagist.ts +43 -0
  60. package/src/sources/it/pypi.ts +30 -0
  61. package/src/sources/it/rubygems.ts +43 -0
  62. package/src/sources/it/stackoverflow.ts +39 -0
  63. package/src/sources/maps/apple_maps.ts +105 -0
  64. package/src/sources/maps/openstreetmap.ts +34 -0
  65. package/src/sources/maps/photon.ts +77 -0
  66. package/src/sources/news/bing_news.ts +95 -0
  67. package/src/sources/news/google_news.ts +80 -0
  68. package/src/sources/news/hackernews.ts +94 -0
  69. package/src/sources/news/yahoo_news.ts +77 -0
  70. package/src/sources/shopping/ebay.ts +96 -0
  71. package/src/sources/social/mastodon.ts +46 -0
  72. package/src/sources/social/medium.ts +52 -0
  73. package/src/sources/social/reddit.ts +48 -0
  74. package/src/sources/social/soundcloud.ts +64 -0
  75. package/src/sources/social/twitter.ts +56 -0
  76. package/src/sources/specialized/annas_archive.ts +97 -0
  77. package/src/sources/specialized/archive.ts +48 -0
  78. package/src/sources/specialized/genius.ts +43 -0
  79. package/src/sources/specialized/goodreads.ts +62 -0
  80. package/src/sources/specialized/imdb.ts +55 -0
  81. package/src/sources/specialized/openlibrary.ts +59 -0
  82. package/src/sources/specialized/wikipedia.ts +37 -0
  83. package/src/sources/specialized/wttr.ts +98 -0
  84. package/src/sources/torrents/1337x.ts +43 -0
  85. package/src/sources/torrents/eztv.ts +47 -0
  86. package/src/sources/torrents/kickass.ts +63 -0
  87. package/src/sources/torrents/nyaa.ts +53 -0
  88. package/src/sources/torrents/solidtorrents.ts +68 -0
  89. package/src/sources/torrents/thepiratebay.ts +57 -0
  90. package/src/sources/torrents/yts.ts +54 -0
  91. package/src/sources/videos/bing_videos.ts +91 -0
  92. package/src/sources/videos/dailymotion.ts +101 -0
  93. package/src/sources/videos/invidious.ts +86 -0
  94. package/src/sources/videos/peertube.ts +76 -0
  95. package/src/sources/videos/vimeo.ts +67 -0
  96. package/src/sources/videos/youtube.ts +78 -0
  97. package/src/suggest-next-words/autocomplete-ai.ts +38 -0
  98. package/src/suggest-next-words/autocomplete-search-engines.ts +384 -0
  99. package/src/suggest-next-words/misspelled-typos-8k.json +1 -0
  100. package/src/types/search-engine-interface.ts +27 -0
  101. package/src/types/search-result-types.ts +405 -0
  102. package/test/api.test.ts +128 -0
  103. package/test/autocomplete-ai.test.ts +20 -0
  104. package/test/autocomplete-engines.test.ts +131 -0
  105. package/test/engine-health-suite.test.ts +350 -0
  106. package/test/search.test.ts +69 -0
  107. package/test/sources-unit.test.ts +1152 -0
  108. package/test/sources.test.ts +182 -0
  109. package/test/test-utils.ts +81 -0
  110. package/tsconfig.json +16 -0
  111. 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
+ };