search-web-api 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +307 -0
  2. package/demo/index.ts +38 -0
  3. package/demo/openapi.ts +402 -0
  4. package/demo/routes/autocomplete.ts +124 -0
  5. package/demo/routes/search.ts +61 -0
  6. package/docs/AUTOCOMPLETE.md +296 -0
  7. package/docs/CATEGORY_SEARCH.md +265 -0
  8. package/package.json +32 -0
  9. package/src/autocomplete/autocomplete-ai-next-word-predictor.ts +47 -0
  10. package/src/autocomplete/autocomplete-search-engine-backends.ts +231 -0
  11. package/src/category-registry.ts +6 -0
  12. package/src/config/search-engine-constants.ts +216 -0
  13. package/src/constants.ts +6 -0
  14. package/src/engine-descriptions.ts +7 -0
  15. package/src/engine-status.ts +7 -0
  16. package/src/engine.ts +6 -0
  17. package/src/registry/search-engine-category-registry.ts +200 -0
  18. package/src/registry/search-engine-descriptions.ts +121 -0
  19. package/src/registry/search-engine-status-tracker.ts +240 -0
  20. package/src/result-container.ts +8 -0
  21. package/src/search/search-engines-registry-list.ts +205 -0
  22. package/src/search/search-query-executor.ts +168 -0
  23. package/src/search/search-result-container.ts +421 -0
  24. package/src/search-web-types.ts +7 -0
  25. package/src/search.ts +10 -0
  26. package/src/sources/academic/arxiv.ts +72 -0
  27. package/src/sources/academic/core.ts +65 -0
  28. package/src/sources/academic/crossref.ts +75 -0
  29. package/src/sources/academic/doaj.ts +63 -0
  30. package/src/sources/academic/google_scholar.ts +53 -0
  31. package/src/sources/academic/openalex.ts +75 -0
  32. package/src/sources/academic/pubmed.ts +96 -0
  33. package/src/sources/academic/semantic_scholar.ts +77 -0
  34. package/src/sources/academic/wikidata.ts +44 -0
  35. package/src/sources/general/baidu.ts +59 -0
  36. package/src/sources/general/bing.ts +30 -0
  37. package/src/sources/general/brave.ts +54 -0
  38. package/src/sources/general/duckduckgo.ts +49 -0
  39. package/src/sources/general/google.ts +68 -0
  40. package/src/sources/general/mojeek.ts +56 -0
  41. package/src/sources/general/qwant.ts +37 -0
  42. package/src/sources/general/startpage.ts +44 -0
  43. package/src/sources/general/yahoo.ts +41 -0
  44. package/src/sources/general/yandex.ts +56 -0
  45. package/src/sources/images/bing_images.ts +68 -0
  46. package/src/sources/images/deviantart.ts +71 -0
  47. package/src/sources/images/flickr.ts +132 -0
  48. package/src/sources/images/google_images.ts +101 -0
  49. package/src/sources/images/imgur.ts +58 -0
  50. package/src/sources/images/openclipart.ts +52 -0
  51. package/src/sources/images/pixabay.ts +56 -0
  52. package/src/sources/images/unsplash.ts +38 -0
  53. package/src/sources/images/wallhaven.ts +50 -0
  54. package/src/sources/it/crates.ts +41 -0
  55. package/src/sources/it/dockerhub.ts +47 -0
  56. package/src/sources/it/github.ts +37 -0
  57. package/src/sources/it/gitlab.ts +55 -0
  58. package/src/sources/it/npm.ts +36 -0
  59. package/src/sources/it/packagist.ts +43 -0
  60. package/src/sources/it/pypi.ts +30 -0
  61. package/src/sources/it/rubygems.ts +43 -0
  62. package/src/sources/it/stackoverflow.ts +39 -0
  63. package/src/sources/maps/apple_maps.ts +105 -0
  64. package/src/sources/maps/openstreetmap.ts +34 -0
  65. package/src/sources/maps/photon.ts +77 -0
  66. package/src/sources/news/bing_news.ts +95 -0
  67. package/src/sources/news/google_news.ts +80 -0
  68. package/src/sources/news/hackernews.ts +94 -0
  69. package/src/sources/news/yahoo_news.ts +77 -0
  70. package/src/sources/shopping/ebay.ts +96 -0
  71. package/src/sources/social/mastodon.ts +46 -0
  72. package/src/sources/social/medium.ts +52 -0
  73. package/src/sources/social/reddit.ts +48 -0
  74. package/src/sources/social/soundcloud.ts +64 -0
  75. package/src/sources/social/twitter.ts +56 -0
  76. package/src/sources/specialized/annas_archive.ts +97 -0
  77. package/src/sources/specialized/archive.ts +48 -0
  78. package/src/sources/specialized/genius.ts +43 -0
  79. package/src/sources/specialized/goodreads.ts +62 -0
  80. package/src/sources/specialized/imdb.ts +55 -0
  81. package/src/sources/specialized/openlibrary.ts +59 -0
  82. package/src/sources/specialized/wikipedia.ts +37 -0
  83. package/src/sources/specialized/wttr.ts +98 -0
  84. package/src/sources/torrents/1337x.ts +43 -0
  85. package/src/sources/torrents/eztv.ts +47 -0
  86. package/src/sources/torrents/kickass.ts +63 -0
  87. package/src/sources/torrents/nyaa.ts +53 -0
  88. package/src/sources/torrents/solidtorrents.ts +68 -0
  89. package/src/sources/torrents/thepiratebay.ts +57 -0
  90. package/src/sources/torrents/yts.ts +54 -0
  91. package/src/sources/videos/bing_videos.ts +91 -0
  92. package/src/sources/videos/dailymotion.ts +101 -0
  93. package/src/sources/videos/invidious.ts +86 -0
  94. package/src/sources/videos/peertube.ts +76 -0
  95. package/src/sources/videos/vimeo.ts +67 -0
  96. package/src/sources/videos/youtube.ts +78 -0
  97. package/src/suggest-next-words/autocomplete-ai.ts +38 -0
  98. package/src/suggest-next-words/autocomplete-search-engines.ts +384 -0
  99. package/src/suggest-next-words/misspelled-typos-8k.json +1 -0
  100. package/src/types/search-engine-interface.ts +27 -0
  101. package/src/types/search-result-types.ts +405 -0
  102. package/test/api.test.ts +128 -0
  103. package/test/autocomplete-ai.test.ts +20 -0
  104. package/test/autocomplete-engines.test.ts +131 -0
  105. package/test/engine-health-suite.test.ts +350 -0
  106. package/test/search.test.ts +69 -0
  107. package/test/sources-unit.test.ts +1152 -0
  108. package/test/sources.test.ts +182 -0
  109. package/test/test-utils.ts +81 -0
  110. package/tsconfig.json +16 -0
  111. package/vitest.config.ts +20 -0
@@ -0,0 +1,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
+ };