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,98 @@
|
|
|
1
|
+
import { EngineFunction, EngineResult } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
// Weather condition mapping
|
|
4
|
+
const WWO_TO_CONDITION: Record<string, string> = {
|
|
5
|
+
"113": "clear sky",
|
|
6
|
+
"116": "partly cloudy",
|
|
7
|
+
"119": "cloudy",
|
|
8
|
+
"122": "fair",
|
|
9
|
+
"143": "fair",
|
|
10
|
+
"176": "light rain showers",
|
|
11
|
+
"179": "light snow showers",
|
|
12
|
+
"248": "fog",
|
|
13
|
+
"260": "fog",
|
|
14
|
+
"263": "light rain showers",
|
|
15
|
+
"296": "light rain",
|
|
16
|
+
"302": "rain",
|
|
17
|
+
"308": "heavy rain",
|
|
18
|
+
"323": "light snow showers",
|
|
19
|
+
"332": "heavy snow",
|
|
20
|
+
"386": "rain showers and thunder",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const wttr: EngineFunction = async (
|
|
24
|
+
query: string,
|
|
25
|
+
page: number | undefined
|
|
26
|
+
) => {
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
format: "j1",
|
|
29
|
+
lang: "en",
|
|
30
|
+
});
|
|
31
|
+
const response = await fetch(`https://wttr.in/${query}?${params}`, {
|
|
32
|
+
headers: {
|
|
33
|
+
"User-Agent":
|
|
34
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) return [];
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
const results: EngineResult[] = [];
|
|
40
|
+
|
|
41
|
+
if (!data || !data.current_condition) {
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const current = data.current_condition[0];
|
|
46
|
+
const weatherCode = current.weatherCode || "";
|
|
47
|
+
const condition = WWO_TO_CONDITION[weatherCode] || "unknown";
|
|
48
|
+
|
|
49
|
+
const content = [
|
|
50
|
+
`Temperature: ${current.temp_C || current.tempC}°C (Feels like: ${current.FeelsLikeC}°C)`,
|
|
51
|
+
`Condition: ${condition}`,
|
|
52
|
+
`Humidity: ${current.humidity}%`,
|
|
53
|
+
`Wind: ${current.windspeedKmph} km/h from ${current.winddirDegree}°`,
|
|
54
|
+
`Pressure: ${current.pressure} hPa`,
|
|
55
|
+
`Cloud cover: ${current.cloudcover}%`,
|
|
56
|
+
]
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join("\n");
|
|
59
|
+
|
|
60
|
+
// Add current weather
|
|
61
|
+
results.push({
|
|
62
|
+
url: `https://wttr.in/${data.nearest_area?.[0]?.areaName?.[0]?.value || "weather"}`,
|
|
63
|
+
title: `Weather in ${data.nearest_area?.[0]?.areaName?.[0]?.value || "Unknown Location"}`,
|
|
64
|
+
content,
|
|
65
|
+
engine: "wttr",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Add forecast
|
|
69
|
+
if (data.weather && Array.isArray(data.weather)) {
|
|
70
|
+
for (const day of data.weather.slice(0, 3)) {
|
|
71
|
+
// Only next 3 days
|
|
72
|
+
const date = day.date;
|
|
73
|
+
const hourly = day.hourly || [];
|
|
74
|
+
|
|
75
|
+
if (hourly.length > 0) {
|
|
76
|
+
const forecast = hourly[Math.floor(hourly.length / 2)]; // Middle of day
|
|
77
|
+
const forecastCondition =
|
|
78
|
+
WWO_TO_CONDITION[forecast.weatherCode] || "unknown";
|
|
79
|
+
|
|
80
|
+
const forecastContent = [
|
|
81
|
+
`Temperature: ${forecast.tempC}°C`,
|
|
82
|
+
`Condition: ${forecastCondition}`,
|
|
83
|
+
`Humidity: ${forecast.humidity}%`,
|
|
84
|
+
`Wind: ${forecast.windspeedKmph} km/h`,
|
|
85
|
+
].join(" | ");
|
|
86
|
+
|
|
87
|
+
results.push({
|
|
88
|
+
url: `https://wttr.in/${data.nearest_area?.[0]?.areaName?.[0]?.value || "weather"}`,
|
|
89
|
+
title: `Forecast for ${date}`,
|
|
90
|
+
content: forecastContent,
|
|
91
|
+
engine: "wttr",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return results;
|
|
98
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const torrent_1337x: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const response = await fetch(
|
|
9
|
+
`https://1337x.to/search/${encodeURIComponent(query)}/${page || 1}/`,
|
|
10
|
+
{
|
|
11
|
+
headers: {
|
|
12
|
+
"User-Agent":
|
|
13
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
if (!response.ok) return [];
|
|
18
|
+
const html = await response.text();
|
|
19
|
+
const { document } = parseHTML(html);
|
|
20
|
+
const results: any[] = [];
|
|
21
|
+
|
|
22
|
+
document.querySelectorAll("table.table-list tbody tr").forEach((el) => {
|
|
23
|
+
const element = el;
|
|
24
|
+
const links = element.querySelectorAll("td.name a");
|
|
25
|
+
const link = links[1] || links[0]; // Second link is usually the torrent detail page
|
|
26
|
+
const url = `https://1337x.to${link?.getAttribute("href")}`;
|
|
27
|
+
const title = link?.textContent?.trim() || "";
|
|
28
|
+
const seeds = element.querySelector("td.seeds")?.textContent?.trim() || "";
|
|
29
|
+
const leeches = element.querySelector("td.leeches")?.textContent?.trim() || "";
|
|
30
|
+
const size = element.querySelector("td.size")?.textContent?.trim() || "";
|
|
31
|
+
|
|
32
|
+
if (url && title) {
|
|
33
|
+
results.push({
|
|
34
|
+
url,
|
|
35
|
+
title,
|
|
36
|
+
content: `Size: ${size}, Seeds: ${seeds}, Leeches: ${leeches}`,
|
|
37
|
+
engine: "1337x",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return results;
|
|
43
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const eztv: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const response = await fetch(
|
|
9
|
+
`https://eztv.re/search/${encodeURIComponent(query)}`,
|
|
10
|
+
{
|
|
11
|
+
headers: {
|
|
12
|
+
"User-Agent":
|
|
13
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
if (!response.ok) return [];
|
|
18
|
+
const html = await response.text();
|
|
19
|
+
const { document } = parseHTML(html);
|
|
20
|
+
const results: any[] = [];
|
|
21
|
+
|
|
22
|
+
document.querySelectorAll("table.forum_header_border tr.forum_header_border").forEach((el) => {
|
|
23
|
+
const element = el;
|
|
24
|
+
const titleColumn = element.querySelectorAll("td")[1];
|
|
25
|
+
const titleLink = titleColumn?.querySelector("a.epinfo");
|
|
26
|
+
const magnetLink = element.querySelector("a.magnet");
|
|
27
|
+
const title = titleLink?.textContent?.trim() || "";
|
|
28
|
+
const url = magnetLink?.getAttribute("href") || "";
|
|
29
|
+
|
|
30
|
+
// Get torrent metadata
|
|
31
|
+
const tds = element.querySelectorAll("td");
|
|
32
|
+
const size = tds[3]?.textContent?.trim() || "";
|
|
33
|
+
const date = tds[4]?.textContent?.trim() || "";
|
|
34
|
+
const seeds = tds[5]?.textContent?.trim() || "";
|
|
35
|
+
|
|
36
|
+
if (url && title) {
|
|
37
|
+
results.push({
|
|
38
|
+
url,
|
|
39
|
+
title,
|
|
40
|
+
content: `Size: ${size}, Seeds: ${seeds}, Released: ${date}`,
|
|
41
|
+
engine: "eztv",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return results;
|
|
47
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const kickass: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const response = await fetch(
|
|
9
|
+
`https://kickasstorrents.to/usearch/${encodeURIComponent(query)}/${page || 1}/`,
|
|
10
|
+
{
|
|
11
|
+
headers: {
|
|
12
|
+
"User-Agent":
|
|
13
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
if (!response.ok) return [];
|
|
18
|
+
const data = await response.text();
|
|
19
|
+
const results: any[] = [];
|
|
20
|
+
|
|
21
|
+
if (!data || typeof data !== "string") {
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { document } = parseHTML(data);
|
|
26
|
+
|
|
27
|
+
document.querySelectorAll("table.data tr").forEach((element, i) => {
|
|
28
|
+
// Skip header row
|
|
29
|
+
if (i === 0) return;
|
|
30
|
+
|
|
31
|
+
const rowElem = element;
|
|
32
|
+
const $link = rowElem.querySelectorAll("a.cellMainLink");
|
|
33
|
+
|
|
34
|
+
if (!$link.length) return;
|
|
35
|
+
|
|
36
|
+
const href = $link[0]?.getAttribute("href");
|
|
37
|
+
const title = $link[0]?.textContent?.trim() || "";
|
|
38
|
+
const description = rowElem.querySelector("span.font11px.lightgrey.block")?.textContent?.trim() || "";
|
|
39
|
+
const seed = parseInt(rowElem.querySelector("td.green")?.textContent?.trim() || "") || 0;
|
|
40
|
+
const leech = parseInt(rowElem.querySelector("td.red")?.textContent?.trim() || "") || 0;
|
|
41
|
+
const filesize = rowElem.querySelector("td.nobr")?.textContent?.trim() || "";
|
|
42
|
+
|
|
43
|
+
const content = [
|
|
44
|
+
description,
|
|
45
|
+
filesize ? `Size: ${filesize}` : "",
|
|
46
|
+
`Seeds: ${seed}`,
|
|
47
|
+
`Leeches: ${leech}`,
|
|
48
|
+
]
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(" | ");
|
|
51
|
+
|
|
52
|
+
results.push({
|
|
53
|
+
url: `https://kickasstorrents.to${href}`,
|
|
54
|
+
title,
|
|
55
|
+
content,
|
|
56
|
+
filesize,
|
|
57
|
+
seed,
|
|
58
|
+
engine: "kickass",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return results;
|
|
63
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const nyaa: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
f: "0",
|
|
10
|
+
c: "0_0",
|
|
11
|
+
q: query,
|
|
12
|
+
p: String(page || 1),
|
|
13
|
+
});
|
|
14
|
+
const response = await fetch(`https://nyaa.si/?${params}`, {
|
|
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
|
+
if (!response.ok) return [];
|
|
21
|
+
const html = await response.text();
|
|
22
|
+
const { document } = parseHTML(html);
|
|
23
|
+
const results: any[] = [];
|
|
24
|
+
|
|
25
|
+
document.querySelectorAll("table.torrent-list tbody tr").forEach((el) => {
|
|
26
|
+
const element = el;
|
|
27
|
+
const tds = element.querySelectorAll("td");
|
|
28
|
+
const category = tds[0]?.textContent?.trim() || "";
|
|
29
|
+
const titleColumn = tds[1];
|
|
30
|
+
// find 'a' elements in titleColumn, filter out .comments
|
|
31
|
+
const titleLink = titleColumn?.querySelector("a:not(.comments)"); // Assuming simple check or just taking first non-comment
|
|
32
|
+
const magnetLink = element.querySelector('a[href^="magnet:"]');
|
|
33
|
+
const title = titleLink?.textContent?.trim() || "";
|
|
34
|
+
const url = magnetLink?.getAttribute("href") || "";
|
|
35
|
+
|
|
36
|
+
// Get torrent metadata
|
|
37
|
+
const size = tds[3]?.textContent?.trim() || "";
|
|
38
|
+
const seeders = tds[5]?.textContent?.trim() || "";
|
|
39
|
+
const leechers = tds[6]?.textContent?.trim() || "";
|
|
40
|
+
const downloads = tds[7]?.textContent?.trim() || "";
|
|
41
|
+
|
|
42
|
+
if (url && title) {
|
|
43
|
+
results.push({
|
|
44
|
+
url,
|
|
45
|
+
title,
|
|
46
|
+
content: `Category: ${category}, Size: ${size}, Seeds: ${seeders}, Leeches: ${leechers}, Downloads: ${downloads}`,
|
|
47
|
+
engine: "nyaa",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return results;
|
|
53
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const solidtorrents: 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://solidtorrents.to/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 data = await response.text();
|
|
23
|
+
const results: any[] = [];
|
|
24
|
+
|
|
25
|
+
if (!data || typeof data !== "string") {
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { document } = parseHTML(data);
|
|
30
|
+
|
|
31
|
+
document.querySelectorAll("li.search-result").forEach((element) => {
|
|
32
|
+
const elElem = element;
|
|
33
|
+
|
|
34
|
+
const torrentfile = elElem.querySelector("a.dl-torrent")?.getAttribute("href");
|
|
35
|
+
const magnet = elElem.querySelector("a.dl-magnet")?.getAttribute("href");
|
|
36
|
+
|
|
37
|
+
if (!torrentfile || !magnet) {
|
|
38
|
+
return; // skip results without torrent links
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const title = elElem.querySelector("h5.title")?.textContent?.trim() || "";
|
|
42
|
+
const url = elElem.querySelector("h5.title a")?.getAttribute("href");
|
|
43
|
+
const category = elElem.querySelector("a.category")?.textContent?.trim() || "";
|
|
44
|
+
|
|
45
|
+
const stats = Array.from(elElem.querySelectorAll(".stats div")).map(
|
|
46
|
+
(el) => el.textContent?.trim() || ""
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const content = [
|
|
50
|
+
category ? `Category: ${category}` : "",
|
|
51
|
+
stats[1] ? `Size: ${stats[1]}` : "",
|
|
52
|
+
stats[3] ? `Seeds: ${stats[3]}` : "",
|
|
53
|
+
stats[2] ? `Leeches: ${stats[2]}` : "",
|
|
54
|
+
stats[4] ? `Date: ${stats[4]}` : "",
|
|
55
|
+
]
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join(" | ");
|
|
58
|
+
|
|
59
|
+
results.push({
|
|
60
|
+
url: `https://solidtorrents.to${url}`,
|
|
61
|
+
title,
|
|
62
|
+
content,
|
|
63
|
+
engine: "solidtorrents",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return results;
|
|
68
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
export const thepiratebay: EngineFunction = async (
|
|
5
|
+
query: string,
|
|
6
|
+
page: number | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const params = new URLSearchParams({
|
|
9
|
+
q: query,
|
|
10
|
+
page: String((page || 1) - 1),
|
|
11
|
+
});
|
|
12
|
+
const response = await fetch(
|
|
13
|
+
`https://thepiratebay.org/search.php?${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 { document } = parseHTML(html);
|
|
24
|
+
const results: any[] = [];
|
|
25
|
+
|
|
26
|
+
document.querySelectorAll("#searchResult tbody tr").forEach((el) => {
|
|
27
|
+
const element = el;
|
|
28
|
+
const titleLink = element.querySelector("td.vertTh a.detLink");
|
|
29
|
+
const magnetLink = element.querySelector('a[href^="magnet:"]');
|
|
30
|
+
const title = titleLink?.textContent?.trim() || "";
|
|
31
|
+
const url = magnetLink?.getAttribute("href") || "";
|
|
32
|
+
const descElement = element.querySelector("font.detDesc");
|
|
33
|
+
const descText = descElement?.textContent?.trim() || "";
|
|
34
|
+
|
|
35
|
+
// Extract size, uploader, and date from description
|
|
36
|
+
const sizeMatch = descText.match(/Size\s+([^,]+)/i);
|
|
37
|
+
const uploaderMatch = descText.match(/ULed by\s+([^,]+)/i);
|
|
38
|
+
const size = sizeMatch ? sizeMatch[1] : "Unknown";
|
|
39
|
+
const uploader = uploaderMatch ? uploaderMatch[1] : "Unknown";
|
|
40
|
+
|
|
41
|
+
// Get seeders and leechers
|
|
42
|
+
const tds = element.querySelectorAll("td");
|
|
43
|
+
const seeders = tds[2]?.textContent?.trim() || "";
|
|
44
|
+
const leechers = tds[3]?.textContent?.trim() || "";
|
|
45
|
+
|
|
46
|
+
if (url && title) {
|
|
47
|
+
results.push({
|
|
48
|
+
url,
|
|
49
|
+
title,
|
|
50
|
+
content: `Size: ${size}, Seeds: ${seeders}, Leeches: ${leechers}, Uploader: ${uploader}`,
|
|
51
|
+
engine: "thepiratebay",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return results;
|
|
57
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const yts: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
query_term: query,
|
|
9
|
+
page: String(page || 1),
|
|
10
|
+
limit: "20",
|
|
11
|
+
});
|
|
12
|
+
const response = await fetch(
|
|
13
|
+
`https://yts.mx/api/v2/list_movies.json?${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 json = await response.json();
|
|
23
|
+
const results: any[] = [];
|
|
24
|
+
|
|
25
|
+
if (json && json.data && json.data.movies) {
|
|
26
|
+
json.data.movies.forEach((movie: any) => {
|
|
27
|
+
const title = movie.title_long || movie.title;
|
|
28
|
+
const rating = movie.rating || "N/A";
|
|
29
|
+
const year = movie.year || "";
|
|
30
|
+
const genres = movie.genres ? movie.genres.join(", ") : "";
|
|
31
|
+
|
|
32
|
+
// Create a result for each torrent quality
|
|
33
|
+
if (movie.torrents && movie.torrents.length > 0) {
|
|
34
|
+
movie.torrents.forEach((torrent: any) => {
|
|
35
|
+
const quality = torrent.quality || "";
|
|
36
|
+
const size = torrent.size || "";
|
|
37
|
+
const seeds = torrent.seeds || 0;
|
|
38
|
+
const peers = torrent.peers || 0;
|
|
39
|
+
const magnetLink = `magnet:?xt=urn:btih:${torrent.hash}&dn=${encodeURIComponent(title)}&tr=udp://open.demonii.com:1337/announce&tr=udp://tracker.openbittorrent.com:80&tr=udp://tracker.coppersurfer.tk:6969&tr=udp://glotorrents.pw:6969/announce&tr=udp://tracker.opentrackr.org:1337/announce&tr=udp://torrent.gresille.org:80/announce&tr=udp://p4p.arenabg.com:1337&tr=udp://tracker.leechers-paradise.org:6969`;
|
|
40
|
+
|
|
41
|
+
results.push({
|
|
42
|
+
url: magnetLink,
|
|
43
|
+
title: `${title} [${quality}]`,
|
|
44
|
+
content: `Rating: ${rating}/10, Year: ${year}, Size: ${size}, Seeds: ${seeds}, Peers: ${peers}, Genres: ${genres}`,
|
|
45
|
+
thumbnail: movie.medium_cover_image,
|
|
46
|
+
engine: "yts",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return results;
|
|
54
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bing Videos Search Engine
|
|
6
|
+
*
|
|
7
|
+
* Searches Bing for video content, including:
|
|
8
|
+
* - Video URLs
|
|
9
|
+
* - Thumbnails
|
|
10
|
+
* - Duration and metadata
|
|
11
|
+
*/
|
|
12
|
+
export const bing_videos: EngineFunction = async (
|
|
13
|
+
query: string,
|
|
14
|
+
page: number | undefined
|
|
15
|
+
) => {
|
|
16
|
+
const params = new URLSearchParams({
|
|
17
|
+
q: query,
|
|
18
|
+
async: "content",
|
|
19
|
+
first: String(((page || 1) - 1) * 35 + 1),
|
|
20
|
+
count: "35",
|
|
21
|
+
});
|
|
22
|
+
const response = await fetch(
|
|
23
|
+
`https://www.bing.com/videos/asyncv2?${params}`,
|
|
24
|
+
{
|
|
25
|
+
headers: {
|
|
26
|
+
"User-Agent":
|
|
27
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
28
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
29
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
if (!response.ok) return [];
|
|
34
|
+
const htmlString = await response.text();
|
|
35
|
+
const { document } = parseHTML(htmlString);
|
|
36
|
+
const results = [] as any[];
|
|
37
|
+
|
|
38
|
+
// Find video result containers
|
|
39
|
+
const videoElements = document.querySelectorAll(
|
|
40
|
+
'div.dg_u div[id*="mc_vtvc_video"]'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
for (const element of Array.from(videoElements)) {
|
|
44
|
+
try {
|
|
45
|
+
// Extract metadata from vrhdata attribute
|
|
46
|
+
const vrhDataElement = element.querySelector("div.vrhdata");
|
|
47
|
+
const vrhmAttr = vrhDataElement?.getAttribute("vrhm");
|
|
48
|
+
|
|
49
|
+
if (!vrhmAttr) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const metadata = JSON.parse(vrhmAttr);
|
|
54
|
+
|
|
55
|
+
// Extract additional info
|
|
56
|
+
const metaSpans = element.querySelectorAll(
|
|
57
|
+
"div.mc_vtvc_meta_block span"
|
|
58
|
+
);
|
|
59
|
+
const info = Array.from(metaSpans)
|
|
60
|
+
.map((span) => span.textContent?.trim())
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join(" - ");
|
|
63
|
+
|
|
64
|
+
const duration = metadata.du || "";
|
|
65
|
+
const content = `${duration} - ${info}`.trim();
|
|
66
|
+
|
|
67
|
+
// Extract thumbnail
|
|
68
|
+
const thumbnailElement = element.querySelector(
|
|
69
|
+
"div.mc_vtvc_th img, img"
|
|
70
|
+
);
|
|
71
|
+
const thumbnail = thumbnailElement?.getAttribute("src");
|
|
72
|
+
|
|
73
|
+
const result: any = {
|
|
74
|
+
url: metadata.murl || "",
|
|
75
|
+
title: metadata.vt || "",
|
|
76
|
+
content,
|
|
77
|
+
thumbnail,
|
|
78
|
+
engine: "bing_videos",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
result.template = "videos";
|
|
82
|
+
|
|
83
|
+
results.push(result);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Skip malformed results
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return results;
|
|
91
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { EngineFunction } from "../../types/search-engine-interface.js";
|
|
2
|
+
|
|
3
|
+
export const dailymotion: EngineFunction = async (
|
|
4
|
+
query: string,
|
|
5
|
+
page: number | undefined
|
|
6
|
+
) => {
|
|
7
|
+
const params = new URLSearchParams({
|
|
8
|
+
search: query,
|
|
9
|
+
family_filter: "false",
|
|
10
|
+
thumbnail_ratio: "original",
|
|
11
|
+
languages: "en",
|
|
12
|
+
page: String(page || 1),
|
|
13
|
+
password_protected: "false",
|
|
14
|
+
private: "false",
|
|
15
|
+
sort: "relevance",
|
|
16
|
+
limit: "10",
|
|
17
|
+
fields:
|
|
18
|
+
"allow_embed,description,title,created_time,duration,url,thumbnail_360_url,id",
|
|
19
|
+
});
|
|
20
|
+
const response = await fetch(
|
|
21
|
+
`https://api.dailymotion.com/videos?${params}`,
|
|
22
|
+
{
|
|
23
|
+
headers: {
|
|
24
|
+
"User-Agent":
|
|
25
|
+
"Mozilla/5.0 (X1; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
if (!response.ok) return [];
|
|
30
|
+
const jsonData = await response.json();
|
|
31
|
+
const results = [] as any[];
|
|
32
|
+
|
|
33
|
+
if (jsonData.error) {
|
|
34
|
+
console.error("Dailymotion API error:", jsonData.error.message);
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const videoList = jsonData.list || [];
|
|
39
|
+
|
|
40
|
+
for (const video of videoList) {
|
|
41
|
+
const title = video.title || "";
|
|
42
|
+
const url = video.url || "";
|
|
43
|
+
let content = video.description || "";
|
|
44
|
+
|
|
45
|
+
// Strip HTML tags from description
|
|
46
|
+
content = content.replace(/<[^>]*>/g, "").trim();
|
|
47
|
+
|
|
48
|
+
// Truncate if too long
|
|
49
|
+
if (content.length > 300) {
|
|
50
|
+
content = content.substring(0, 300) + "...";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Format duration
|
|
54
|
+
let duration = "";
|
|
55
|
+
if (video.duration) {
|
|
56
|
+
const hours = Math.floor(video.duration / 3600);
|
|
57
|
+
const minutes = Math.floor((video.duration % 3600) / 60);
|
|
58
|
+
const seconds = video.duration % 60;
|
|
59
|
+
|
|
60
|
+
if (hours > 0) {
|
|
61
|
+
duration = `${String(hours).padStart(2, "0")}:${String(
|
|
62
|
+
minutes
|
|
63
|
+
).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
64
|
+
} else {
|
|
65
|
+
duration = `${String(minutes).padStart(2, "0")}:${String(
|
|
66
|
+
seconds
|
|
67
|
+
).padStart(2, "0")}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Format published date
|
|
72
|
+
const publishedDate = video.created_time
|
|
73
|
+
? new Date(video.created_time * 1000).toISOString().split("T")[0]
|
|
74
|
+
: "";
|
|
75
|
+
|
|
76
|
+
const metadata = [
|
|
77
|
+
publishedDate ? `Published: ${publishedDate}` : "",
|
|
78
|
+
duration ? `Duration: ${duration}` : "",
|
|
79
|
+
]
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join(" | ");
|
|
82
|
+
|
|
83
|
+
const fullContent = metadata ? `${metadata}\n${content}` : content;
|
|
84
|
+
|
|
85
|
+
// Get thumbnail
|
|
86
|
+
let thumbnail = video.thumbnail_360_url || "";
|
|
87
|
+
if (thumbnail) {
|
|
88
|
+
thumbnail = thumbnail.replace("http://", "https://");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
results.push({
|
|
92
|
+
url,
|
|
93
|
+
title,
|
|
94
|
+
content: fullContent,
|
|
95
|
+
thumbnail,
|
|
96
|
+
engine: "dailymotion",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return results;
|
|
101
|
+
};
|