OhMyDitzzy commited on
Commit
22bf4eb
·
1 Parent(s): 1e55efc

Add youtube downloader

Browse files
src/components/PluginCard.tsx CHANGED
@@ -3,6 +3,7 @@ import { Card } from "@/components/ui/card";
3
  import { Badge } from "@/components/ui/badge";
4
  import { Button } from "@/components/ui/button";
5
  import { Input } from "@/components/ui/input";
 
6
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
7
  import { PluginMetadata } from "@/client/hooks/usePlugin";
8
  import { Play, ChevronDown, ChevronUp, Copy, Check } from "lucide-react";
@@ -116,6 +117,42 @@ export function PluginCard({ plugin }: PluginCardProps) {
116
  setTimeout(() => setCopiedRequestUrl(false), 2000);
117
  };
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  const hasQueryParams = plugin.parameters?.query && plugin.parameters.query.length > 0;
120
  const hasBodyParams = plugin.parameters?.body && plugin.parameters.body.length > 0;
121
  const hasPathParams = plugin.parameters?.path && plugin.parameters.path.length > 0;
@@ -280,7 +317,14 @@ export function PluginCard({ plugin }: PluginCardProps) {
280
  {plugin.parameters?.path?.map((param) => (
281
  <tr key={param.name} className="border-b border-white/5">
282
  <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
283
- <td className="py-3 pr-4 text-blue-400 font-mono text-xs">{param.type}</td>
 
 
 
 
 
 
 
284
  <td className="py-3 pr-4">
285
  <span className={param.required ? "text-red-400" : "text-gray-500"}>
286
  {param.required ? "Yes" : "No"}
@@ -293,7 +337,14 @@ export function PluginCard({ plugin }: PluginCardProps) {
293
  {plugin.parameters?.query?.map((param) => (
294
  <tr key={param.name} className="border-b border-white/5">
295
  <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
296
- <td className="py-3 pr-4 text-blue-400 font-mono text-xs">{param.type}</td>
 
 
 
 
 
 
 
297
  <td className="py-3 pr-4">
298
  <span className={param.required ? "text-red-400" : "text-gray-500"}>
299
  {param.required ? "Yes" : "No"}
@@ -306,7 +357,14 @@ export function PluginCard({ plugin }: PluginCardProps) {
306
  {plugin.parameters?.body?.map((param) => (
307
  <tr key={param.name} className="border-b border-white/5">
308
  <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
309
- <td className="py-3 pr-4 text-blue-400 font-mono text-xs">{param.type}</td>
 
 
 
 
 
 
 
310
  <td className="py-3 pr-4">
311
  <span className={param.required ? "text-red-400" : "text-gray-500"}>
312
  {param.required ? "Yes" : "No"}
@@ -388,14 +446,13 @@ export function PluginCard({ plugin }: PluginCardProps) {
388
  {param.name}
389
  {param.required && <span className="text-red-400 ml-1">*</span>}
390
  <span className="text-xs text-gray-500 ml-2">({param.type})</span>
 
 
 
 
 
391
  </label>
392
- <Input
393
- type="text"
394
- placeholder={param.example?.toString() || param.description}
395
- value={paramValues[param.name] || ""}
396
- onChange={(e) => handleParamChange(param.name, e.target.value)}
397
- className="bg-black/50 border-white/10 text-white focus:border-purple-500"
398
- />
399
  <p className="text-xs text-gray-500 mt-1">{param.description}</p>
400
  </div>
401
  ))}
@@ -407,14 +464,13 @@ export function PluginCard({ plugin }: PluginCardProps) {
407
  {param.name}
408
  {param.required && <span className="text-red-400 ml-1">*</span>}
409
  <span className="text-xs text-gray-500 ml-2">({param.type})</span>
 
 
 
 
 
410
  </label>
411
- <Input
412
- type="text"
413
- placeholder={param.example?.toString() || param.description}
414
- value={paramValues[param.name] || ""}
415
- onChange={(e) => handleParamChange(param.name, e.target.value)}
416
- className="bg-black/50 border-white/10 text-white focus:border-purple-500"
417
- />
418
  <p className="text-xs text-gray-500 mt-1">{param.description}</p>
419
  </div>
420
  ))}
@@ -498,4 +554,4 @@ export function PluginCard({ plugin }: PluginCardProps) {
498
  )}
499
  </Card>
500
  );
501
- }
 
3
  import { Badge } from "@/components/ui/badge";
4
  import { Button } from "@/components/ui/button";
5
  import { Input } from "@/components/ui/input";
6
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
7
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
8
  import { PluginMetadata } from "@/client/hooks/usePlugin";
9
  import { Play, ChevronDown, ChevronUp, Copy, Check } from "lucide-react";
 
117
  setTimeout(() => setCopiedRequestUrl(false), 2000);
118
  };
119
 
120
+ const renderParameterInput = (param: any) => {
121
+ if (param.enum && Array.isArray(param.enum) && param.enum.length > 0) {
122
+ return (
123
+ <Select
124
+ value={paramValues[param.name] || ""}
125
+ onValueChange={(value) => handleParamChange(param.name, value)}
126
+ >
127
+ <SelectTrigger className="bg-black/50 border-white/10 text-white focus:border-purple-500">
128
+ <SelectValue placeholder={`Select ${param.name}...`} />
129
+ </SelectTrigger>
130
+ <SelectContent className="bg-zinc-900 border-white/10">
131
+ {param.enum.map((option: any) => (
132
+ <SelectItem
133
+ key={option}
134
+ value={option.toString()}
135
+ className="text-white hover:bg-white/10 focus:bg-white/10"
136
+ >
137
+ {option}
138
+ </SelectItem>
139
+ ))}
140
+ </SelectContent>
141
+ </Select>
142
+ );
143
+ } else {
144
+ return (
145
+ <Input
146
+ type="text"
147
+ placeholder={param.example?.toString() || param.description}
148
+ value={paramValues[param.name] || ""}
149
+ onChange={(e) => handleParamChange(param.name, e.target.value)}
150
+ className="bg-black/50 border-white/10 text-white focus:border-purple-500"
151
+ />
152
+ );
153
+ }
154
+ };
155
+
156
  const hasQueryParams = plugin.parameters?.query && plugin.parameters.query.length > 0;
157
  const hasBodyParams = plugin.parameters?.body && plugin.parameters.body.length > 0;
158
  const hasPathParams = plugin.parameters?.path && plugin.parameters.path.length > 0;
 
317
  {plugin.parameters?.path?.map((param) => (
318
  <tr key={param.name} className="border-b border-white/5">
319
  <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
320
+ <td className="py-3 pr-4">
321
+ <span className="text-blue-400 font-mono text-xs">{param.type}</span>
322
+ {param.enum && (
323
+ <span className="ml-2 text-xs text-gray-500">
324
+ (options: {param.enum.join(', ')})
325
+ </span>
326
+ )}
327
+ </td>
328
  <td className="py-3 pr-4">
329
  <span className={param.required ? "text-red-400" : "text-gray-500"}>
330
  {param.required ? "Yes" : "No"}
 
337
  {plugin.parameters?.query?.map((param) => (
338
  <tr key={param.name} className="border-b border-white/5">
339
  <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
340
+ <td className="py-3 pr-4">
341
+ <span className="text-blue-400 font-mono text-xs">{param.type}</span>
342
+ {param.enum && (
343
+ <span className="ml-2 text-xs text-gray-500">
344
+ (options: {param.enum.join(', ')})
345
+ </span>
346
+ )}
347
+ </td>
348
  <td className="py-3 pr-4">
349
  <span className={param.required ? "text-red-400" : "text-gray-500"}>
350
  {param.required ? "Yes" : "No"}
 
357
  {plugin.parameters?.body?.map((param) => (
358
  <tr key={param.name} className="border-b border-white/5">
359
  <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
360
+ <td className="py-3 pr-4">
361
+ <span className="text-blue-400 font-mono text-xs">{param.type}</span>
362
+ {param.enum && (
363
+ <span className="ml-2 text-xs text-gray-500">
364
+ (options: {param.enum.join(', ')})
365
+ </span>
366
+ )}
367
+ </td>
368
  <td className="py-3 pr-4">
369
  <span className={param.required ? "text-red-400" : "text-gray-500"}>
370
  {param.required ? "Yes" : "No"}
 
446
  {param.name}
447
  {param.required && <span className="text-red-400 ml-1">*</span>}
448
  <span className="text-xs text-gray-500 ml-2">({param.type})</span>
449
+ {param.enum && (
450
+ <span className="text-xs text-purple-400 ml-2">
451
+ • Select from options
452
+ </span>
453
+ )}
454
  </label>
455
+ {renderParameterInput(param)}
 
 
 
 
 
 
456
  <p className="text-xs text-gray-500 mt-1">{param.description}</p>
457
  </div>
458
  ))}
 
464
  {param.name}
465
  {param.required && <span className="text-red-400 ml-1">*</span>}
466
  <span className="text-xs text-gray-500 ml-2">({param.type})</span>
467
+ {param.enum && (
468
+ <span className="text-xs text-purple-400 ml-2">
469
+ • Select from options
470
+ </span>
471
+ )}
472
  </label>
473
+ {renderParameterInput(param)}
 
 
 
 
 
 
474
  <p className="text-xs text-gray-500 mt-1">{param.description}</p>
475
  </div>
476
  ))}
 
554
  )}
555
  </Card>
556
  );
557
+ }
src/server/index.ts CHANGED
@@ -111,7 +111,7 @@ app.use((req, res, next) => {
111
  if (path.startsWith("/api")) {
112
  let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
113
  if (capturedJsonResponse) {
114
- logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
115
  }
116
 
117
  log(logLine);
 
111
  if (path.startsWith("/api")) {
112
  let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
113
  if (capturedJsonResponse) {
114
+ logLine += ` :: ${JSON.stringify(capturedJsonResponse, null, 2)}`;
115
  }
116
 
117
  log(logLine);
src/server/plugins/downloader/youtube.js ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from "axios";
2
+ import { sendSuccess, ErrorResponses } from "../../lib/response-helper.js";
3
+
4
+ class Youtube {
5
+ constructor() {
6
+ this.API_URL = "https://thesocialcat.com/api/youtube-download";
7
+ this.HEADERS = {
8
+ "accept": "*/*",
9
+ "accept-language": "id-ID",
10
+ "content-type": "application/json",
11
+ "Referer": "https://thesocialcat.com/tools/youtube-video-downloader",
12
+ "Referrer-Policy": "strict-origin-when-cross-origin"
13
+ };
14
+
15
+ this.CREATED_BY = "Ditzzy";
16
+ this.NOTE = "Thank you for using this scrape, I hope you appreciate me for making this scrape by not deleting wm";
17
+ }
18
+
19
+ wrapResponse(data) {
20
+ return {
21
+ created_by: this.CREATED_BY,
22
+ note: this.NOTE,
23
+ results: data
24
+ };
25
+ }
26
+
27
+ async download(url, format) {
28
+ try {
29
+ const config = {
30
+ url: this.API_URL,
31
+ headers: this.HEADERS,
32
+ method: "POST",
33
+ data: {
34
+ format,
35
+ url
36
+ }
37
+ };
38
+
39
+ const { data } = await axios.request(config);
40
+ return this.wrapResponse(data);
41
+ } catch (e) {
42
+ throw new Error(`Error downloading YouTube content: ${e}`);
43
+ }
44
+ }
45
+ }
46
+
47
+ /** @type {import("../../types/plugin").ApiPluginHandler} */
48
+ const handler = {
49
+ name: "YouTube Downloader",
50
+ description: "Download YouTube media with resolution up to 1080p and support audio",
51
+ version: "1.0.0",
52
+ method: "GET",
53
+ category: ["downloader"],
54
+ alias: ["youtube", "yt"],
55
+ tags: ["social-media", "video", "downloader"],
56
+ parameters: {
57
+ query: [
58
+ {
59
+ name: "url",
60
+ type: "string",
61
+ required: true,
62
+ description: "Your YouTube URL",
63
+ example: "https://youtu.be/zawDTvoXT8k?si=FgZnxXzMXJI8jfkB"
64
+ },
65
+ {
66
+ name: "format",
67
+ type: "string",
68
+ required: true,
69
+ description: "Format to download",
70
+ example: "720p",
71
+ enum: ["144p", "240p", "360p", "480p", "720p", "1080p", "audio"],
72
+ default: "720p"
73
+ }
74
+ ],
75
+ body: [],
76
+ headers: []
77
+ },
78
+ responses: {
79
+ 200: {
80
+ status: 200,
81
+ description: "Successfully retrieved YouTube video data",
82
+ example: {
83
+ status: 200,
84
+ author: "Ditzzy",
85
+ note: "Thank you for using this API!",
86
+ results: {}
87
+ }
88
+ },
89
+ 400: {
90
+ status: 400,
91
+ description: "Invalid YouTube URL provided",
92
+ example: {
93
+ status: 400,
94
+ message: "Invalid URL - must be a valid YouTube URL"
95
+ }
96
+ },
97
+ 404: {
98
+ status: 404,
99
+ description: "Missing required parameter",
100
+ example: {
101
+ status: 404,
102
+ message: "Missing required parameter: ..."
103
+ }
104
+ },
105
+ 500: {
106
+ status: 500,
107
+ description: "Server error or YouTube API unavailable",
108
+ example: {
109
+ status: 500,
110
+ message: "An error occurred, please try again later."
111
+ }
112
+ }
113
+ },
114
+ exec: async (req, res) => {
115
+ const { url } = req.query
116
+ const { format } = req.query
117
+
118
+ if (!url) return ErrorResponses.missingParameter(res, "url");
119
+
120
+ if (!format) return ErrorResponses.missingParameter(res, "format");
121
+
122
+ const regex = /^(https?:\/\/)?((www\.|m\.)?youtube(-nocookie)?\.com|youtu\.be)(\/(embed\/|v\/|watch\?v=|watch\?.+&v=|shorts\/)?)([\w-]{11})(\S+)?$/;
123
+
124
+ if (!regex.test(url)) return ErrorResponses.invalidUrl(res, "Invalid URL - must be a valid YouTube URL");
125
+
126
+ const yt = new Youtube();
127
+ try {
128
+ const download = await yt.download(url, format);
129
+
130
+ return sendSuccess(res, download.results);
131
+ } catch (e) {
132
+ console.error("TikTok download error:", e);
133
+ return ErrorResponses.serverError(res);
134
+ }
135
+ }
136
+ }
137
+
138
+ export default handler;