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