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,86 @@
1
+ import { EngineFunction } from "../../types/search-engine-interface.js";
2
+
3
+ // Public Invidious instances - user can configure their own
4
+ const DEFAULT_INSTANCES = [
5
+ "https://invidious.nerdvpn.de",
6
+ "https://invidious.private.coffee",
7
+ "https://inv.nadeko.net",
8
+ ];
9
+
10
+ export const invidious: EngineFunction = async (
11
+ query: string,
12
+ page: number | undefined
13
+ ) => {
14
+ const baseUrl = DEFAULT_INSTANCES[0];
15
+ const params = new URLSearchParams({
16
+ q: query,
17
+ page: String(page || 1),
18
+ });
19
+
20
+ const response = await fetch(`${baseUrl}/api/v1/search?${params}`, {
21
+ headers: {
22
+ "User-Agent":
23
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
24
+ },
25
+ });
26
+ if (!response.ok) return [];
27
+ const json = await response.json();
28
+ const results = [] as any[];
29
+
30
+ if (!json || !Array.isArray(json)) {
31
+ return results;
32
+ }
33
+
34
+ for (const result of json) {
35
+ if (result.type !== "video") {
36
+ continue;
37
+ }
38
+
39
+ const videoId = result.videoId;
40
+ if (!videoId) continue;
41
+
42
+ const url = `${baseUrl}/watch?v=${videoId}`;
43
+
44
+ // Find thumbnail
45
+ const thumbs = result.videoThumbnails || [];
46
+ let thumbnail = "";
47
+ const sdThumb = thumbs.find((t: any) => t.quality === "sddefault");
48
+ if (sdThumb) {
49
+ thumbnail = sdThumb.url;
50
+ // Handle partial URLs
51
+ if (thumbnail && !thumbnail.startsWith("http")) {
52
+ thumbnail = baseUrl + thumbnail;
53
+ }
54
+ }
55
+
56
+ // Format duration
57
+ const lengthSeconds = result.lengthSeconds || 0;
58
+ const hours = Math.floor(lengthSeconds / 3600);
59
+ const minutes = Math.floor((lengthSeconds % 3600) / 60);
60
+ const seconds = lengthSeconds % 60;
61
+ const length =
62
+ hours > 0
63
+ ? `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
64
+ : `${minutes}:${seconds.toString().padStart(2, "0")}`;
65
+
66
+ const content = [
67
+ result.description || "",
68
+ result.author ? `By: ${result.author}` : "",
69
+ result.viewCount ? `Views: ${result.viewCount.toLocaleString()}` : "",
70
+ length ? `Duration: ${length}` : "",
71
+ ]
72
+ .filter(Boolean)
73
+ .join(" | ");
74
+
75
+ results.push({
76
+ url,
77
+ title: result.title || "",
78
+ content,
79
+ engine: "invidious",
80
+ thumbnail,
81
+ iframe_src: `${baseUrl}/embed/${videoId}`,
82
+ });
83
+ }
84
+
85
+ return results;
86
+ };
@@ -0,0 +1,76 @@
1
+ import { EngineFunction } from "../../types/search-engine-interface.js";
2
+
3
+ export const peertube: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const baseUrl = "https://peer.tube";
8
+ const start = ((page || 1) - 1) * 10;
9
+ const params = new URLSearchParams({
10
+ search: query,
11
+ searchTarget: "search-index",
12
+ resultType: "videos",
13
+ start: String(start),
14
+ count: "10",
15
+ sort: "-match",
16
+ nsfw: "false",
17
+ });
18
+
19
+ const response = await fetch(`${baseUrl}/api/v1/search/videos?${params}`, {
20
+ headers: {
21
+ "User-Agent":
22
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
23
+ },
24
+ });
25
+ if (!response.ok) return [];
26
+ const json = await response.json();
27
+ const results = [] as any[];
28
+
29
+ if (!json || !json.data) {
30
+ return results;
31
+ }
32
+
33
+ for (const result of json.data) {
34
+ const channel = result.channel || {};
35
+ const account = result.account || {};
36
+
37
+ // Format duration
38
+ const duration = result.duration || 0;
39
+ const hours = Math.floor(duration / 3600);
40
+ const minutes = Math.floor((duration % 3600) / 60);
41
+ const seconds = duration % 60;
42
+ const length =
43
+ hours > 0
44
+ ? `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
45
+ : `${minutes}:${seconds.toString().padStart(2, "0")}`;
46
+
47
+ const metadata = [
48
+ channel.displayName,
49
+ `${channel.name}@${channel.host}`,
50
+ result.tags ? result.tags.join(", ") : "",
51
+ ]
52
+ .filter(Boolean)
53
+ .join(" | ");
54
+
55
+ const content = [
56
+ result.description || "",
57
+ `Views: ${result.views?.toLocaleString() || 0}`,
58
+ length ? `Duration: ${length}` : "",
59
+ metadata,
60
+ ]
61
+ .filter(Boolean)
62
+ .join("\n");
63
+
64
+ results.push({
65
+ url: result.url,
66
+ title: result.name,
67
+ content,
68
+ engine: "peertube",
69
+ thumbnail: result.thumbnailUrl || result.previewUrl,
70
+ iframe_src: result.embedUrl,
71
+ author: account.displayName,
72
+ });
73
+ }
74
+
75
+ return results;
76
+ };
@@ -0,0 +1,67 @@
1
+ import { EngineFunction } from "../../types/search-engine-interface.js";
2
+
3
+ export const vimeo: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const params = new URLSearchParams({
8
+ q: query,
9
+ page: String(page || 1),
10
+ });
11
+ const response = await fetch(`https://vimeo.com/search?${params}`, {
12
+ headers: {
13
+ "User-Agent":
14
+ "Mozilla/5.0 (X1; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
15
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
16
+ "Accept-Language": "en-US,en;q=0.5",
17
+ },
18
+ });
19
+ if (!response.ok) return [];
20
+ const html = await response.text();
21
+ const results = [] as any[];
22
+
23
+ if (!html || typeof html !== "string") {
24
+ return results;
25
+ }
26
+
27
+ try {
28
+ // Extract JSON data from the page
29
+ const dataMatch = html.match(/var data = ({.*?});/s);
30
+ if (!dataMatch) {
31
+ return results;
32
+ }
33
+
34
+ const parsedData = JSON.parse(dataMatch[1]);
35
+ const filteredData = parsedData?.filtered?.data || [];
36
+
37
+ for (const resultItem of filteredData) {
38
+ const type = resultItem.type;
39
+ const result = resultItem[type];
40
+
41
+ if (!result) continue;
42
+
43
+ const videoId = result.uri?.split("/").pop();
44
+ if (!videoId) continue;
45
+
46
+ const url = `https://vimeo.com/${videoId}`;
47
+ const title = result.name || "";
48
+ const thumbnail =
49
+ result.pictures?.sizes?.slice(-1)[0]?.link || "";
50
+ const publishedDate = result.created_time || "";
51
+
52
+ results.push({
53
+ url,
54
+ title,
55
+ content: publishedDate
56
+ ? `Published: ${publishedDate.split("T")[0]}`
57
+ : "",
58
+ thumbnail,
59
+ engine: "vimeo",
60
+ });
61
+ }
62
+ } catch (e) {
63
+ console.error("Error parsing Vimeo response:", e);
64
+ }
65
+
66
+ return results;
67
+ };
@@ -0,0 +1,78 @@
1
+ import { EngineFunction } from "../../types/search-engine-interface.js";
2
+
3
+ // Multiple Invidious instances for fallback
4
+ const INVIDIOUS_INSTANCES = [
5
+ "https://inv.nadeko.net",
6
+ "https://invidious.privacyredirect.com",
7
+ "https://yewtu.be",
8
+ "https://invidious.nerdvpn.de",
9
+ "https://inv.riverside.rocks",
10
+ ];
11
+
12
+ async function tryInvidiousInstances(
13
+ query: string,
14
+ page: number | undefined
15
+ ): Promise<any> {
16
+ for (const instance of INVIDIOUS_INSTANCES) {
17
+ try {
18
+ const params = new URLSearchParams({
19
+ q: query,
20
+ page: String(page || 1),
21
+ type: "video",
22
+ });
23
+ const response = await fetch(`${instance}/api/v1/search?${params}`, {
24
+ headers: {
25
+ "User-Agent": "HonoxSearX/1.0",
26
+ },
27
+ signal: AbortSignal.timeout(5000),
28
+ });
29
+
30
+ if (!response.ok) continue;
31
+ const json = await response.json();
32
+ const results = [] as any[];
33
+
34
+ if (Array.isArray(json)) {
35
+ json.forEach((item: any) => {
36
+ if (item.type === "video") {
37
+ const duration = item.lengthSeconds
38
+ ? `${Math.floor(item.lengthSeconds / 60)}:${String(item.lengthSeconds % 60).padStart(2, "0")}`
39
+ : "";
40
+ const views = item.viewCount
41
+ ? item.viewCount.toLocaleString()
42
+ : "";
43
+ const published = item.publishedText || "";
44
+
45
+ results.push({
46
+ url: `https://www.youtube.com/watch?v=${item.videoId}`,
47
+ title: item.title,
48
+ content: `${item.description || ""} | ${duration} | ${views} views | ${published}`.trim(),
49
+ thumbnail:
50
+ item.videoThumbnails && item.videoThumbnails.length > 0
51
+ ? item.videoThumbnails[0].url
52
+ : undefined,
53
+ engine: "youtube",
54
+ });
55
+ }
56
+ });
57
+ }
58
+
59
+ // If successful, return the result
60
+ if (results.length > 0) {
61
+ return results;
62
+ }
63
+ } catch (error) {
64
+ // Try next instance
65
+ continue;
66
+ }
67
+ }
68
+
69
+ // All instances failed, return empty array
70
+ return [];
71
+ }
72
+
73
+ export const youtube: EngineFunction = async (
74
+ query: string,
75
+ page: number | undefined
76
+ ) => {
77
+ return await tryInvidiousInstances(query, page);
78
+ };
@@ -0,0 +1,38 @@
1
+ import { pipeline, type TextGenerationSingle } from "@huggingface/transformers";
2
+
3
+ /**
4
+ * Generates the next words for a given prompt using a text generation model.
5
+ *
6
+ * The DistilGPT2 model is a distilled, smaller version of OpenAI's GPT-2, featuring
7
+ * around 82 million parameters (~80MB), and available in Hugging Face Transformers
8
+ * for efficient next word (next-token) prediction tasks in English text. It is
9
+ * well-suited for sequence generation and can be used as a next word prediction
10
+ * model with code similar to GPT2, but offers faster and lighter inference compared
11
+ * GPT-2 (124M parameter) model, preserving most capabilities but running
12
+ * significantly faster and requiring less memory.
13
+ * @see https://huggingface.co/Xenova/distilgpt2
14
+ * @param {string} prompt - The input prompt to complete.
15
+ * @param {Object} [options] - Generation options.
16
+ * @param {number} [options.maxTokens=16] - Maximum number of new tokens to generate.
17
+ * @param {string} [options.model='Xenova/distilgpt2'] - The model identifier to use.
18
+ * @returns {Promise<string>} - The generated text completions.
19
+ */
20
+ export async function predictNextWordsWithSmallLocalModel(
21
+ prompt: string,
22
+ { model = "Xenova/distilgpt2", maxTokens = 16, modelParams = {} } = {}
23
+ ): Promise<string> {
24
+ if (!prompt) throw new Error("Prompt is required");
25
+ return (
26
+ await (
27
+ await pipeline("text-generation", model, {
28
+ dtype: "fp16",
29
+ device: "cpu",
30
+ ...modelParams,
31
+ })
32
+ )(prompt, { max_new_tokens: maxTokens })
33
+ )
34
+ .map((res) => (res as TextGenerationSingle).generated_text)
35
+ .join(" ")
36
+ .replace(prompt, "")
37
+ .trim();
38
+ }