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,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
|
+
}
|