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,46 @@
1
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
2
+
3
+ export const mastodon: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const params = new URLSearchParams({
8
+ q: query,
9
+ resolve: "false",
10
+ type: "accounts",
11
+ limit: "40",
12
+ });
13
+ const response = await fetch(
14
+ `https://mastodon.social/api/v2/search?${params}`
15
+ );
16
+ if (!response.ok) return [];
17
+ const data = await response.json();
18
+ const results: EngineResult[] = [];
19
+ const accounts = data.accounts || [];
20
+
21
+ for (const account of accounts) {
22
+ const url = account.uri || account.url;
23
+ const username = account.username || "";
24
+ const displayName = account.display_name || username;
25
+ const followersCount = account.followers_count || 0;
26
+ const note = account.note || "";
27
+
28
+ // Strip HTML tags from note
29
+ const cleanNote = note.replace(/<[^>]*>/g, "").trim();
30
+
31
+ const title = `${displayName} (@${username})`;
32
+ const content = `Followers: ${followersCount}\n${cleanNote}`;
33
+
34
+ const thumbnail = account.avatar || account.avatar_static;
35
+
36
+ results.push({
37
+ url,
38
+ title,
39
+ content,
40
+ thumbnail,
41
+ engine: "mastodon",
42
+ });
43
+ }
44
+
45
+ return results;
46
+ };
@@ -0,0 +1,52 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
3
+
4
+ export const medium: EngineFunction = async (
5
+ query: string,
6
+ page: number | undefined
7
+ ) => {
8
+ const params = new URLSearchParams({ q: query });
9
+ const response = await fetch(`https://medium.com/search?${params}`, {
10
+ headers: {
11
+ "User-Agent":
12
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
13
+ },
14
+ });
15
+ if (!response.ok) return [];
16
+ const html = await response.text();
17
+ const { document } = parseHTML(html);
18
+ const results: EngineResult[] = [];
19
+
20
+ // Medium uses dynamic content, so we'll parse what we can from the initial HTML
21
+ document.querySelectorAll("article").forEach((el) => {
22
+ const element = el;
23
+ const titleLink = element.querySelector("h2 a, h3 a");
24
+ const title = titleLink?.textContent?.trim() || "";
25
+ const href = titleLink?.getAttribute("href");
26
+ const url = href
27
+ ? href.startsWith("http")
28
+ ? href
29
+ : `https://medium.com${href}`
30
+ : "";
31
+ const content = element.querySelector("p")?.textContent?.trim() || "";
32
+ const author =
33
+ element
34
+ .querySelector('a[rel="author"]')
35
+ ?.textContent?.trim() || "";
36
+ const readTime =
37
+ element
38
+ .querySelector('[aria-label*="read"]')
39
+ ?.textContent?.trim() || "";
40
+
41
+ if (url && title) {
42
+ results.push({
43
+ url,
44
+ title,
45
+ content: `${content} | By ${author || "Unknown"} | ${readTime || ""}`,
46
+ engine: "medium",
47
+ });
48
+ }
49
+ });
50
+
51
+ return results;
52
+ };
@@ -0,0 +1,48 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
3
+
4
+ export const reddit: EngineFunction = async (
5
+ query: string,
6
+ page: number | undefined
7
+ ) => {
8
+ const params = new URLSearchParams({
9
+ q: query,
10
+ sort: "relevance",
11
+ t: "all",
12
+ });
13
+ const response = await fetch(
14
+ `https://old.reddit.com/search?${params}`,
15
+ {
16
+ headers: {
17
+ "User-Agent":
18
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
19
+ },
20
+ }
21
+ );
22
+ if (!response.ok) return [];
23
+ const html = await response.text();
24
+ const { document } = parseHTML(html);
25
+ const results: EngineResult[] = [];
26
+
27
+ document.querySelectorAll(".search-result").forEach((el) => {
28
+ const titleLink = el.querySelector("a.search-title");
29
+ const title = titleLink?.textContent?.trim() || "";
30
+ const link = titleLink?.getAttribute("href");
31
+ const content =
32
+ el.querySelector(".search-result-body")?.textContent?.trim() ||
33
+ "";
34
+
35
+ if (title && link) {
36
+ results.push({
37
+ title,
38
+ url: link.startsWith("http")
39
+ ? link
40
+ : `https://old.reddit.com${link}`,
41
+ content: content || "",
42
+ engine: "reddit",
43
+ });
44
+ }
45
+ });
46
+
47
+ return results;
48
+ };
@@ -0,0 +1,64 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
3
+
4
+ export const soundcloud: EngineFunction = async (
5
+ query: string,
6
+ page: number | undefined
7
+ ) => {
8
+ const params = new URLSearchParams({ q: query });
9
+ const response = await fetch(`https://soundcloud.com/search?${params}`, {
10
+ headers: {
11
+ "User-Agent":
12
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
13
+ },
14
+ });
15
+ if (!response.ok) return [];
16
+ const html = await response.text();
17
+ const { document } = parseHTML(html);
18
+ const results: EngineResult[] = [];
19
+
20
+ // SoundCloud uses dynamic content, so we'll try to extract from initial HTML
21
+ document
22
+ .querySelectorAll("article, .searchList__item")
23
+ .forEach((el) => {
24
+ const element = el;
25
+ const titleLink = element.querySelector(
26
+ 'a[itemprop="url"], h2 a'
27
+ );
28
+ const title =
29
+ element
30
+ .querySelector('[itemprop="name"], h2')
31
+ ?.textContent?.trim() || "";
32
+ const artist =
33
+ element
34
+ .querySelector('[itemprop="byArtist"], .soundTitle__username')
35
+ ?.textContent?.trim() || "";
36
+ const href = titleLink?.getAttribute("href");
37
+ const url = href
38
+ ? href.startsWith("http")
39
+ ? href
40
+ : `https://soundcloud.com${href}`
41
+ : "";
42
+ const plays =
43
+ element
44
+ .querySelector(".sc-ministats-plays, .soundStats__plays")
45
+ ?.textContent?.trim() || "";
46
+ const duration =
47
+ element
48
+ .querySelector(
49
+ '.soundTitle__tagContent time, [itemprop="duration"]'
50
+ )
51
+ ?.textContent?.trim() || "";
52
+
53
+ if (url && title) {
54
+ results.push({
55
+ url,
56
+ title: `${title}${artist ? " - " + artist : ""}`,
57
+ content: `${plays ? "Plays: " + plays : ""} ${duration ? "| Duration: " + duration : ""}`,
58
+ engine: "soundcloud",
59
+ });
60
+ }
61
+ });
62
+
63
+ return results;
64
+ };
@@ -0,0 +1,56 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
3
+
4
+ export const twitter: EngineFunction = async (
5
+ query: string,
6
+ page: number | undefined
7
+ ) => {
8
+ const params = new URLSearchParams({
9
+ f: "tweets",
10
+ q: query,
11
+ });
12
+ const response = await fetch(`https://nitter.net/search?${params}`, {
13
+ headers: {
14
+ "User-Agent":
15
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
16
+ },
17
+ });
18
+ if (!response.ok) return [];
19
+ const html = await response.text();
20
+ const { document } = parseHTML(html);
21
+ const results: EngineResult[] = [];
22
+
23
+ document.querySelectorAll(".timeline-item").forEach((el) => {
24
+ const element = el;
25
+ const tweetLink = element.querySelector(".tweet-link");
26
+ const username =
27
+ element.querySelector(".username")?.textContent?.trim() || "";
28
+ const fullname =
29
+ element.querySelector(".fullname")?.textContent?.trim() || "";
30
+ const content =
31
+ element.querySelector(".tweet-content")?.textContent?.trim() ||
32
+ "";
33
+ const timestamp =
34
+ element
35
+ .querySelector(".tweet-date a")
36
+ ?.textContent?.trim() || "";
37
+ const stats =
38
+ element.querySelector(".tweet-stats")?.textContent?.trim() || "";
39
+
40
+ const href = tweetLink?.getAttribute("href");
41
+ const url = href
42
+ ? `https://twitter.com${href.replace("/i/web", "")}`
43
+ : "";
44
+
45
+ if (url && content) {
46
+ results.push({
47
+ url,
48
+ title: `${fullname} (@${username})`,
49
+ content: `${content} | ${timestamp} | ${stats}`,
50
+ engine: "twitter",
51
+ });
52
+ }
53
+ });
54
+
55
+ return results;
56
+ };
@@ -0,0 +1,97 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
3
+
4
+ const baseDomains = [
5
+ "annas-archive.gl",
6
+ "annas-archive.gd",
7
+ "annas-archive.pk",
8
+ ];
9
+
10
+ export const annas_archive: EngineFunction = async (
11
+ query: string,
12
+ page: number | undefined,
13
+ baseDomain: number = 0,
14
+ ) => {
15
+ const params = new URLSearchParams({
16
+ q: query,
17
+ page: String(page || 1),
18
+ });
19
+
20
+ let response: Response;
21
+ try {
22
+ response = await fetch(
23
+ `https://${baseDomains[baseDomain]}/search?${params}`,
24
+ {
25
+ headers: {
26
+ "User-Agent":
27
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
28
+ },
29
+ signal: AbortSignal.timeout(10000),
30
+ }
31
+ );
32
+ } catch (error) {
33
+ if (baseDomain + 1 < baseDomains.length) {
34
+ return await annas_archive(query, page, baseDomain + 1);
35
+ }
36
+ throw error;
37
+ }
38
+
39
+ if (!response.ok) {
40
+ if (baseDomain + 1 < baseDomains.length) {
41
+ return await annas_archive(query, page, baseDomain + 1);
42
+ }
43
+ return [];
44
+ }
45
+
46
+ const html = await response.text();
47
+ const results: EngineResult[] = [];
48
+
49
+ if (!html || typeof html !== "string") {
50
+ return results;
51
+ }
52
+
53
+ const { document } = parseHTML(html);
54
+
55
+ document
56
+ .querySelectorAll("main div.js-aarecord-list-outer > div")
57
+ .forEach((element) => {
58
+ const elElem = element;
59
+
60
+ const href = elElem.querySelector("a")?.getAttribute("href");
61
+ if (!href) return;
62
+
63
+ const url = "https://" + baseDomains[baseDomain] + href;
64
+ const title =
65
+ elElem.querySelector('a[href^="/md5"]')?.textContent?.trim() ||
66
+ "";
67
+ const author =
68
+ elElem.querySelector('a[href^="/search"]')?.textContent?.trim() ||
69
+ "";
70
+ const publisher =
71
+ elElem
72
+ .querySelectorAll('a[href^="/search"]')[1]
73
+ ?.textContent?.trim() || "";
74
+ const description =
75
+ elElem.querySelector("div.relative")?.textContent?.trim() || "";
76
+ const thumbnail =
77
+ elElem.querySelector("img")?.getAttribute("src") || undefined;
78
+
79
+ const content = [
80
+ description,
81
+ author ? `Author: ${author}` : "",
82
+ publisher ? `Publisher: ${publisher}` : "",
83
+ ]
84
+ .filter(Boolean)
85
+ .join("\n");
86
+
87
+ results.push({
88
+ url,
89
+ title,
90
+ content,
91
+ engine: "annas_archive",
92
+ thumbnail,
93
+ });
94
+ });
95
+
96
+ return results;
97
+ };
@@ -0,0 +1,48 @@
1
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
2
+
3
+ export const archive: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const params = new URLSearchParams({
8
+ q: query,
9
+ "fl[]": "identifier,title,description,mediatype,downloads",
10
+ "sort[]": "",
11
+ rows: "50",
12
+ page: String(page || 1),
13
+ output: "json",
14
+ });
15
+ const response = await fetch(
16
+ `https://archive.org/advancedsearch.php?${params}`,
17
+ {
18
+ headers: {
19
+ "User-Agent":
20
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
21
+ },
22
+ }
23
+ );
24
+ if (!response.ok) return [];
25
+ const data = await response.json();
26
+ const results: EngineResult[] = [];
27
+
28
+ if (data && data.response && data.response.docs) {
29
+ data.response.docs.forEach((doc: any) => {
30
+ const identifier = doc.identifier;
31
+ const title = doc.title || identifier;
32
+ const description = doc.description || "No description available";
33
+ const mediatype = doc.mediatype || "unknown";
34
+ const downloads = doc.downloads || 0;
35
+ const url = `https://archive.org/details/${identifier}`;
36
+
37
+ results.push({
38
+ url,
39
+ title,
40
+ content: `${description} | Type: ${mediatype} | Downloads: ${downloads}`,
41
+ thumbnail: `https://archive.org/services/img/${identifier}`,
42
+ engine: "archive",
43
+ });
44
+ });
45
+ }
46
+
47
+ return results;
48
+ };
@@ -0,0 +1,43 @@
1
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
2
+
3
+ export const genius: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const params = new URLSearchParams({
8
+ per_page: "5",
9
+ q: query,
10
+ });
11
+ const response = await fetch(
12
+ `https://genius.com/api/search/multi?${params}`,
13
+ {
14
+ headers: {
15
+ "User-Agent":
16
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
17
+ },
18
+ }
19
+ );
20
+ if (!response.ok) return [];
21
+ const data = await response.json();
22
+ const results: EngineResult[] = [];
23
+
24
+ if (data && data.response && data.response.sections) {
25
+ data.response.sections.forEach((section: any) => {
26
+ if (section.type === "song" || section.type === "lyric") {
27
+ section.hits.forEach((hit: any) => {
28
+ if (hit.result) {
29
+ results.push({
30
+ url: hit.result.url,
31
+ title: hit.result.full_title,
32
+ content: `Artist: ${hit.result.artist_names}`,
33
+ thumbnail: hit.result.song_art_image_thumbnail_url,
34
+ engine: "genius",
35
+ });
36
+ }
37
+ });
38
+ }
39
+ });
40
+ }
41
+
42
+ return results;
43
+ };
@@ -0,0 +1,62 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
3
+
4
+ export const goodreads: EngineFunction = async (
5
+ query: string,
6
+ page: number | undefined
7
+ ) => {
8
+ const params = new URLSearchParams({
9
+ q: query,
10
+ page: String(page || 1),
11
+ });
12
+ const response = await fetch(
13
+ `https://www.goodreads.com/search?${params}`,
14
+ {
15
+ headers: {
16
+ "User-Agent":
17
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
18
+ },
19
+ }
20
+ );
21
+ if (!response.ok) return [];
22
+ const html = await response.text();
23
+ const results: EngineResult[] = [];
24
+
25
+ if (!html || typeof html !== "string") {
26
+ return results;
27
+ }
28
+
29
+ const { document } = parseHTML(html);
30
+
31
+ document.querySelectorAll("table tr").forEach((element) => {
32
+ const rowElem = element;
33
+
34
+ const $link = rowElem.querySelector("a.bookTitle");
35
+ const href = $link?.getAttribute("href");
36
+ const title = $link?.textContent?.trim() || "";
37
+
38
+ if (!href || !title) return;
39
+
40
+ const thumbnail =
41
+ rowElem.querySelector("img.bookCover")?.getAttribute("src") ||
42
+ undefined;
43
+ const author =
44
+ rowElem.querySelector("a.authorName")?.textContent?.trim() || "";
45
+ const info =
46
+ rowElem.querySelector("span.uitext")?.textContent?.trim() || "";
47
+
48
+ const content = [info, author ? `Author: ${author}` : ""]
49
+ .filter(Boolean)
50
+ .join(" | ");
51
+
52
+ results.push({
53
+ url: `https://www.goodreads.com${href}`,
54
+ title,
55
+ content,
56
+ engine: "goodreads",
57
+ thumbnail,
58
+ });
59
+ });
60
+
61
+ return results;
62
+ };
@@ -0,0 +1,55 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
3
+
4
+ export const imdb: EngineFunction = async (
5
+ query: string,
6
+ page: number | undefined
7
+ ) => {
8
+ const params = new URLSearchParams({
9
+ q: query,
10
+ s: "all",
11
+ });
12
+ const response = await fetch(
13
+ `https://www.imdb.com/find/?${params}`,
14
+ {
15
+ headers: {
16
+ "User-Agent":
17
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
18
+ "Accept-Language": "en-US,en;q=0.9",
19
+ },
20
+ }
21
+ );
22
+ if (!response.ok) return [];
23
+ const html = await response.text();
24
+ const { document } = parseHTML(html);
25
+ const results: EngineResult[] = [];
26
+
27
+ document
28
+ .querySelectorAll(".ipc-metadata-list-summary-item")
29
+ .forEach((el) => {
30
+ const element = el;
31
+ const link = element.querySelector(
32
+ "a.ipc-metadata-list-summary-item__t"
33
+ );
34
+ const url = `https://www.imdb.com${link?.getAttribute("href")}`;
35
+ const title = link?.textContent?.trim() || "";
36
+ const content =
37
+ element
38
+ .querySelector(".ipc-metadata-list-summary-item__li")
39
+ ?.textContent?.trim() || "";
40
+ const thumbnail =
41
+ element.querySelector("img")?.getAttribute("src") || "";
42
+
43
+ if (url && title) {
44
+ results.push({
45
+ url,
46
+ title,
47
+ content,
48
+ thumbnail,
49
+ engine: "imdb",
50
+ });
51
+ }
52
+ });
53
+
54
+ return results;
55
+ };
@@ -0,0 +1,59 @@
1
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
2
+
3
+ export const openlibrary: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const params = new URLSearchParams({
8
+ q: query,
9
+ page: String(page || 1),
10
+ limit: "10",
11
+ fields: "*",
12
+ });
13
+ const response = await fetch(
14
+ `https://openlibrary.org/search.json?${params}`,
15
+ {
16
+ headers: {
17
+ "User-Agent":
18
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
19
+ },
20
+ signal: AbortSignal.timeout(10000),
21
+ }
22
+ );
23
+ if (!response.ok) return [];
24
+ const data = await response.json();
25
+ const results: EngineResult[] = [];
26
+
27
+ if (!data || !data.docs) {
28
+ return results;
29
+ }
30
+
31
+ for (const item of data.docs) {
32
+ const thumbnail = item.lending_identifier_s
33
+ ? `https://archive.org/services/img/${item.lending_identifier_s}`
34
+ : "";
35
+
36
+ const authors = item.author_name ? item.author_name.join(", ") : "";
37
+ const publishYear = item.first_publish_year || "";
38
+ const isbn = item.isbn ? item.isbn.slice(0, 3).join(", ") : "";
39
+
40
+ const content = [
41
+ item.first_sentence ? item.first_sentence.join(" / ") : "",
42
+ authors ? `Authors: ${authors}` : "",
43
+ publishYear ? `First published: ${publishYear}` : "",
44
+ isbn ? `ISBN: ${isbn}` : "",
45
+ ]
46
+ .filter(Boolean)
47
+ .join("\n");
48
+
49
+ results.push({
50
+ url: `https://openlibrary.org${item.key}`,
51
+ title: item.title,
52
+ content,
53
+ engine: "openlibrary",
54
+ thumbnail,
55
+ });
56
+ }
57
+
58
+ return results;
59
+ };
@@ -0,0 +1,37 @@
1
+ import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
2
+
3
+ export const wikipedia: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const params = new URLSearchParams({
8
+ action: "query",
9
+ list: "search",
10
+ srsearch: query,
11
+ format: "json",
12
+ sroffset: String(((page || 1) - 1) * 10),
13
+ });
14
+ const response = await fetch(
15
+ `https://en.wikipedia.org/w/api.php?${params}`,
16
+ {
17
+ headers: {
18
+ "User-Agent": "HonoxSearX/1.0 (mailto:admin@example.com)",
19
+ },
20
+ }
21
+ );
22
+ if (!response.ok) return [];
23
+ const data = await response.json();
24
+
25
+ const results: EngineResult[] = (
26
+ data.query?.search || []
27
+ ).map((item: any) => ({
28
+ url: `https://en.wikipedia.org/wiki/${encodeURIComponent(
29
+ item.title.replace(/ /g, "_")
30
+ )}`,
31
+ title: item.title,
32
+ content: item.snippet.replace(/<[^>]+>/g, ""),
33
+ engine: "wikipedia",
34
+ }));
35
+
36
+ return results;
37
+ };