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,49 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const duckduckgo: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page?: number
|
|
7
|
+
) => {
|
|
8
|
+
const response = await fetch("https://duckduckgo.com/html", {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: {
|
|
11
|
+
"User-Agent":
|
|
12
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
13
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
14
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
15
|
+
},
|
|
16
|
+
body: `q=${encodeURIComponent(query)}&b=&kl=us-en`,
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) return [];
|
|
19
|
+
const htmlString = await response.text();
|
|
20
|
+
const { document } = parseHTML(htmlString);
|
|
21
|
+
|
|
22
|
+
const snippets = document.querySelectorAll(".result");
|
|
23
|
+
console.log(`DDG: Found ${snippets.length} results`);
|
|
24
|
+
|
|
25
|
+
const data = Array.from(snippets)
|
|
26
|
+
.map((element) => {
|
|
27
|
+
const link = element.querySelector(".result__title a");
|
|
28
|
+
const url = link?.getAttribute("href") || "";
|
|
29
|
+
const title = link?.textContent?.trim() || "";
|
|
30
|
+
const content =
|
|
31
|
+
element.querySelector(".result__snippet")?.textContent?.trim() ||
|
|
32
|
+
"";
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
url,
|
|
36
|
+
title,
|
|
37
|
+
content,
|
|
38
|
+
engine: "duckduckgo",
|
|
39
|
+
};
|
|
40
|
+
})
|
|
41
|
+
.filter((r) => r.url && r.title);
|
|
42
|
+
|
|
43
|
+
console.log(`DDG: Extracted ${data.length} valid results`);
|
|
44
|
+
if (data.length === 0 && htmlString.length > 0) {
|
|
45
|
+
console.log("DDG: HTML sample:", htmlString.substring(0, 1000));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return data;
|
|
49
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const google: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
q: query,
|
|
10
|
+
start: String(((page || 1) - 1) * 10),
|
|
11
|
+
gbv: "1",
|
|
12
|
+
});
|
|
13
|
+
const response = await fetch(`https://www.google.com/search?${params}`, {
|
|
14
|
+
headers: {
|
|
15
|
+
"User-Agent":
|
|
16
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
17
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
18
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
19
|
+
Cookie: "CONSENT=YES+; SOCS=CAESBQgYEgAgAA==",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) return [];
|
|
23
|
+
const htmlString = await response.text();
|
|
24
|
+
|
|
25
|
+
if (!htmlString || typeof htmlString !== "string") {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { document } = parseHTML(htmlString);
|
|
30
|
+
|
|
31
|
+
// Google's classes change frequently. Let's try to be more robust.
|
|
32
|
+
const snippets = document.querySelectorAll(".Gx5Zad.fP1Qef.xpd.EtOod.pkphOe");
|
|
33
|
+
console.log(`Google: Found ${snippets.length} snippets with main selector`);
|
|
34
|
+
|
|
35
|
+
const data = Array.from(snippets)
|
|
36
|
+
.map((element) => {
|
|
37
|
+
const link = element.querySelector("a");
|
|
38
|
+
const url = link?.getAttribute("href");
|
|
39
|
+
|
|
40
|
+
let realUrl = url;
|
|
41
|
+
if (url && url.startsWith("/url?q=")) {
|
|
42
|
+
realUrl = url.split("/url?q=")[1].split("&")[0];
|
|
43
|
+
realUrl = decodeURIComponent(realUrl);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const title =
|
|
47
|
+
element.querySelector(".BNeawe.vvjwJb.AP7Wnd")?.textContent?.trim() ||
|
|
48
|
+
"";
|
|
49
|
+
const content =
|
|
50
|
+
element.querySelector(".BNeawe.s3v9rd.AP7Wnd")?.textContent?.trim() ||
|
|
51
|
+
"";
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
url: realUrl,
|
|
55
|
+
title,
|
|
56
|
+
content,
|
|
57
|
+
engine: "google",
|
|
58
|
+
};
|
|
59
|
+
})
|
|
60
|
+
.filter((r) => r.url && r.title);
|
|
61
|
+
|
|
62
|
+
console.log(`Google: Extracted ${data.length} valid results`);
|
|
63
|
+
if (data.length === 0 && htmlString.length > 0) {
|
|
64
|
+
console.log("Google: HTML sample:", htmlString.substring(0, 1000));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return data;
|
|
68
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const mojeek: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
q: query,
|
|
10
|
+
s: String(10 * ((page || 1) - 1)),
|
|
11
|
+
safe: "0",
|
|
12
|
+
});
|
|
13
|
+
const response = await fetch(`https://www.mojeek.com/search?${params}`, {
|
|
14
|
+
headers: {
|
|
15
|
+
"User-Agent":
|
|
16
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
|
17
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
18
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
19
|
+
"Referer": "https://www.mojeek.com/",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) return [];
|
|
23
|
+
const htmlString = await response.text();
|
|
24
|
+
|
|
25
|
+
if (!htmlString || typeof htmlString !== "string") {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { document } = parseHTML(htmlString);
|
|
30
|
+
|
|
31
|
+
const snippets = document.querySelectorAll("ul.results-standard li");
|
|
32
|
+
console.log(`Mojeek: Found ${snippets.length} snippets`);
|
|
33
|
+
|
|
34
|
+
const data = Array.from(snippets)
|
|
35
|
+
.map((element) => {
|
|
36
|
+
const mainLink = element.querySelector("a.ob");
|
|
37
|
+
const url = mainLink?.getAttribute("href") || "";
|
|
38
|
+
const title = element.querySelector("h2 a")?.textContent?.trim() || "";
|
|
39
|
+
const content = element.querySelector("p.s")?.textContent?.trim() || "";
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
url,
|
|
43
|
+
title,
|
|
44
|
+
content,
|
|
45
|
+
engine: "mojeek",
|
|
46
|
+
};
|
|
47
|
+
})
|
|
48
|
+
.filter((r) => r.url && r.title);
|
|
49
|
+
|
|
50
|
+
console.log(`Mojeek: Extracted ${data.length} valid results`);
|
|
51
|
+
if (data.length === 0 && htmlString.length > 0) {
|
|
52
|
+
console.log("Mojeek: HTML sample:", htmlString.substring(0, 1000));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return data;
|
|
56
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const qwant: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
q: query,
|
|
9
|
+
count: "10",
|
|
10
|
+
offset: String(((page || 1) - 1) * 10),
|
|
11
|
+
locale: "en_US",
|
|
12
|
+
});
|
|
13
|
+
const response = await fetch(`https://api.qwant.com/v3/search/web?${params}`, {
|
|
14
|
+
headers: {
|
|
15
|
+
"User-Agent":
|
|
16
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) return [];
|
|
20
|
+
const jsonData = await response.json();
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
!jsonData ||
|
|
24
|
+
!jsonData.data ||
|
|
25
|
+
!jsonData.data.result ||
|
|
26
|
+
!jsonData.data.result.items
|
|
27
|
+
) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return jsonData.data.result.items.map((item: any) => ({
|
|
32
|
+
url: item.url,
|
|
33
|
+
title: item.title,
|
|
34
|
+
content: item.desc,
|
|
35
|
+
engine: "qwant",
|
|
36
|
+
}));
|
|
37
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const startpage: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const formData = new URLSearchParams();
|
|
9
|
+
formData.append("query", query);
|
|
10
|
+
formData.append("page", String(page || 1));
|
|
11
|
+
|
|
12
|
+
const response = await fetch("https://www.startpage.com/sp/search", {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
"User-Agent":
|
|
16
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
17
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
18
|
+
},
|
|
19
|
+
body: formData.toString(),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) return [];
|
|
22
|
+
const htmlString = await response.text();
|
|
23
|
+
const { document } = parseHTML(htmlString);
|
|
24
|
+
|
|
25
|
+
return Array.from(
|
|
26
|
+
document.querySelectorAll(".w-gl__result")
|
|
27
|
+
)
|
|
28
|
+
.map((element) => {
|
|
29
|
+
const link = element.querySelector(".w-gl__result-title");
|
|
30
|
+
const url = link?.getAttribute("href") || "";
|
|
31
|
+
const title = link?.querySelector("h3")?.textContent?.trim() || "";
|
|
32
|
+
const content =
|
|
33
|
+
element.querySelector(".w-gl__description")?.textContent?.trim() ||
|
|
34
|
+
"";
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
url,
|
|
38
|
+
title,
|
|
39
|
+
content,
|
|
40
|
+
engine: "startpage",
|
|
41
|
+
};
|
|
42
|
+
})
|
|
43
|
+
.filter((r) => r.url && r.title);
|
|
44
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const yahoo: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
p: query,
|
|
10
|
+
b: String(((page || 1) - 1) * 7 + 1),
|
|
11
|
+
});
|
|
12
|
+
const response = await fetch(`https://search.yahoo.com/search?${params}`, {
|
|
13
|
+
headers: {
|
|
14
|
+
"User-Agent":
|
|
15
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/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.9",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) return [];
|
|
22
|
+
const htmlString = await response.text();
|
|
23
|
+
const { document } = parseHTML(htmlString);
|
|
24
|
+
|
|
25
|
+
return Array.from(document.querySelectorAll(".algo-sr"))
|
|
26
|
+
.map((element) => {
|
|
27
|
+
const link = element.querySelector("a");
|
|
28
|
+
const url = link?.getAttribute("href") || "";
|
|
29
|
+
const title = link?.textContent?.trim() || "";
|
|
30
|
+
const content =
|
|
31
|
+
element.querySelector(".compText")?.textContent?.trim() || "";
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
url,
|
|
35
|
+
title,
|
|
36
|
+
content,
|
|
37
|
+
engine: "yahoo",
|
|
38
|
+
};
|
|
39
|
+
})
|
|
40
|
+
.filter((r) => r.url && r.title);
|
|
41
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const yandex: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
tmpl_version: "releases",
|
|
10
|
+
text: query,
|
|
11
|
+
web: "1",
|
|
12
|
+
frame: "1",
|
|
13
|
+
searchid: "3131712",
|
|
14
|
+
lang: "en",
|
|
15
|
+
...((page || 1) > 1 ? { p: String((page || 1) - 1) } : {}),
|
|
16
|
+
});
|
|
17
|
+
const response = await fetch(`https://yandex.com/search/site/?${params}`, {
|
|
18
|
+
headers: {
|
|
19
|
+
"User-Agent":
|
|
20
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
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
|
+
Cookie: "yp=1716337604.sp.family%3A0#1685406411.szm.1:1920x1080:1920x999",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) return [];
|
|
28
|
+
const htmlString = await response.text();
|
|
29
|
+
const { document } = parseHTML(htmlString);
|
|
30
|
+
|
|
31
|
+
return Array.from(
|
|
32
|
+
document.querySelectorAll('li.serp-item, li[class*="serp-item"]')
|
|
33
|
+
)
|
|
34
|
+
.map((element) => {
|
|
35
|
+
const link = element.querySelector(
|
|
36
|
+
'a.b-serp-item__title-link, a[class*="title-link"]'
|
|
37
|
+
);
|
|
38
|
+
const url = link?.getAttribute("href") || "";
|
|
39
|
+
const title =
|
|
40
|
+
link?.querySelector("span, h3")?.textContent?.trim() || "";
|
|
41
|
+
const content =
|
|
42
|
+
element
|
|
43
|
+
.querySelector(
|
|
44
|
+
'div.b-serp-item__text, div[class*="text"]'
|
|
45
|
+
)
|
|
46
|
+
?.textContent?.trim() || "";
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
url,
|
|
50
|
+
title,
|
|
51
|
+
content,
|
|
52
|
+
engine: "yandex",
|
|
53
|
+
};
|
|
54
|
+
})
|
|
55
|
+
.filter((r) => r.url && r.title);
|
|
56
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const bing_images: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
q: query,
|
|
10
|
+
async: "1",
|
|
11
|
+
first: String(((page || 1) - 1) * 35 + 1),
|
|
12
|
+
count: "35",
|
|
13
|
+
});
|
|
14
|
+
const response = await fetch(
|
|
15
|
+
`https://www.bing.com/images/async?${params}`,
|
|
16
|
+
{
|
|
17
|
+
headers: {
|
|
18
|
+
"User-Agent":
|
|
19
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
20
|
+
Accept:
|
|
21
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
22
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
if (!response.ok) return [];
|
|
27
|
+
const html = await response.text();
|
|
28
|
+
const { document } = parseHTML(html);
|
|
29
|
+
|
|
30
|
+
return Array.from(
|
|
31
|
+
document.querySelectorAll(
|
|
32
|
+
'ul.dgControl_list li, li[class*="dgControl"]'
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
.map((element) => {
|
|
36
|
+
const metadataStr = element
|
|
37
|
+
.querySelector("a.iusc")
|
|
38
|
+
?.getAttribute("m");
|
|
39
|
+
if (!metadataStr) return null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const metadata = JSON.parse(metadataStr);
|
|
43
|
+
const title =
|
|
44
|
+
element.querySelector("div.infnmpt a")?.textContent?.trim() ||
|
|
45
|
+
metadata.t ||
|
|
46
|
+
"";
|
|
47
|
+
const imgFormat =
|
|
48
|
+
element
|
|
49
|
+
.querySelector("div.imgpt div span")
|
|
50
|
+
?.textContent?.trim() || "";
|
|
51
|
+
const source =
|
|
52
|
+
element
|
|
53
|
+
.querySelector("div.imgpt div.lnkw a")
|
|
54
|
+
?.textContent?.trim() || "";
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
url: metadata.purl,
|
|
58
|
+
title,
|
|
59
|
+
content: metadata.desc || source || "",
|
|
60
|
+
thumbnail: metadata.turl,
|
|
61
|
+
engine: "bing_images",
|
|
62
|
+
};
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.filter((r) => r !== null);
|
|
68
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const deviantart: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({ q: query });
|
|
9
|
+
const response = await fetch(
|
|
10
|
+
`https://www.deviantart.com/search?${params}`,
|
|
11
|
+
{
|
|
12
|
+
headers: {
|
|
13
|
+
"User-Agent":
|
|
14
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
if (!response.ok) return [];
|
|
19
|
+
const html = await response.text();
|
|
20
|
+
|
|
21
|
+
if (!html || typeof html !== "string") {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { document } = parseHTML(html);
|
|
26
|
+
|
|
27
|
+
return Array.from(
|
|
28
|
+
document.querySelectorAll("div.V_S0t_ > div > div > a")
|
|
29
|
+
)
|
|
30
|
+
.map((element) => {
|
|
31
|
+
const premiumText = element.parentElement
|
|
32
|
+
?.querySelector("div div div")
|
|
33
|
+
?.textContent;
|
|
34
|
+
if (premiumText && premiumText.includes("Watch the artist to view")) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const url = element.getAttribute("href");
|
|
39
|
+
const title = element.getAttribute("aria-label");
|
|
40
|
+
const thumbnail =
|
|
41
|
+
element.querySelector("div img")?.getAttribute("src") || undefined;
|
|
42
|
+
|
|
43
|
+
let imgSrc = element.querySelector("div img")?.getAttribute("srcset");
|
|
44
|
+
if (imgSrc) {
|
|
45
|
+
imgSrc = imgSrc.split(" ")[0];
|
|
46
|
+
try {
|
|
47
|
+
const imgUrl = new URL(imgSrc);
|
|
48
|
+
const pathParts = imgUrl.pathname.split("/v1");
|
|
49
|
+
if (pathParts.length > 0) {
|
|
50
|
+
imgUrl.pathname = pathParts[0];
|
|
51
|
+
imgSrc = imgUrl.toString();
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// If URL parsing fails, keep original
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!url || !title) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
url,
|
|
64
|
+
title,
|
|
65
|
+
content: "",
|
|
66
|
+
engine: "deviantart",
|
|
67
|
+
thumbnail: imgSrc || thumbnail,
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.filter((r) => r !== null);
|
|
71
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const flickr: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
text: query,
|
|
9
|
+
page: String(page || 1),
|
|
10
|
+
});
|
|
11
|
+
const response = await fetch(
|
|
12
|
+
`https://www.flickr.com/search?${params}`,
|
|
13
|
+
{
|
|
14
|
+
headers: {
|
|
15
|
+
"User-Agent":
|
|
16
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
17
|
+
Accept:
|
|
18
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
19
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
if (!response.ok) return [];
|
|
24
|
+
const html = await response.text();
|
|
25
|
+
|
|
26
|
+
if (!html || typeof html !== "string") {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const modelExportMatch = html.match(/modelExport:\s*({[\s\S]*?}),\s*[\n\r]/);
|
|
32
|
+
if (!modelExportMatch) {
|
|
33
|
+
console.log("Flickr: modelExport not found in HTML");
|
|
34
|
+
if (html.length > 0) {
|
|
35
|
+
console.log("Flickr: HTML sample:", html.substring(0, 1000));
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const modelExport = JSON.parse(modelExportMatch[1]);
|
|
41
|
+
console.log("Flickr: Parsed modelExport successfully");
|
|
42
|
+
|
|
43
|
+
if (!modelExport.legend || !modelExport.legend[0]) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const legend = modelExport.legend;
|
|
48
|
+
|
|
49
|
+
const data = legend
|
|
50
|
+
.map((index: any) => {
|
|
51
|
+
if (index.length !== 8) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const photo =
|
|
57
|
+
modelExport.main[index[0]][parseInt(index[1])][index[2]][
|
|
58
|
+
index[3]
|
|
59
|
+
][index[4]][index[5]][parseInt(index[6])][index[7]];
|
|
60
|
+
|
|
61
|
+
const title = photo.title || "";
|
|
62
|
+
const description = photo.description || "";
|
|
63
|
+
const author = photo.realname || photo.username || "";
|
|
64
|
+
|
|
65
|
+
const imageSizes = [
|
|
66
|
+
"o",
|
|
67
|
+
"k",
|
|
68
|
+
"h",
|
|
69
|
+
"b",
|
|
70
|
+
"c",
|
|
71
|
+
"z",
|
|
72
|
+
"m",
|
|
73
|
+
"n",
|
|
74
|
+
"t",
|
|
75
|
+
"q",
|
|
76
|
+
"s",
|
|
77
|
+
];
|
|
78
|
+
let sizeData = null;
|
|
79
|
+
|
|
80
|
+
for (const size of imageSizes) {
|
|
81
|
+
if (photo.sizes?.data?.[size]?.data) {
|
|
82
|
+
sizeData = photo.sizes.data[size].data;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!sizeData) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const imgSrc = sizeData.url;
|
|
92
|
+
const resolution = `${sizeData.width} x ${sizeData.height}`;
|
|
93
|
+
|
|
94
|
+
let thumbnail = imgSrc;
|
|
95
|
+
if (photo.sizes?.data?.n?.data?.url) {
|
|
96
|
+
thumbnail = photo.sizes.data.n.data.url;
|
|
97
|
+
} else if (photo.sizes?.data?.z?.data?.url) {
|
|
98
|
+
thumbnail = photo.sizes.data.z.data.url;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const url = photo.ownerNsid
|
|
102
|
+
? `https://www.flickr.com/photos/${photo.ownerNsid}/${photo.id}`
|
|
103
|
+
: imgSrc;
|
|
104
|
+
|
|
105
|
+
const content = [
|
|
106
|
+
description,
|
|
107
|
+
author ? `Author: ${author}` : "",
|
|
108
|
+
`Resolution: ${resolution}`,
|
|
109
|
+
]
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join("\n");
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
url,
|
|
115
|
+
title,
|
|
116
|
+
content,
|
|
117
|
+
thumbnail,
|
|
118
|
+
engine: "flickr",
|
|
119
|
+
};
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
.filter((r: any) => r !== null);
|
|
125
|
+
|
|
126
|
+
console.log(`Flickr: Extracted ${data.length} valid results`);
|
|
127
|
+
return data;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error("Error parsing Flickr response:", e);
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
};
|