@supasayan/ytm 1.0.0

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 (60) hide show
  1. package/bin/cli.js +44 -0
  2. package/client/index.html +13 -0
  3. package/client/package-lock.json +1003 -0
  4. package/client/package.json +22 -0
  5. package/client/public/favicon.svg +1 -0
  6. package/client/public/icons.svg +24 -0
  7. package/client/src/App.jsx +210 -0
  8. package/client/src/assets/hero.png +0 -0
  9. package/client/src/assets/typescript.svg +1 -0
  10. package/client/src/assets/vite.svg +1 -0
  11. package/client/src/components/Download/DownloadItem.css +246 -0
  12. package/client/src/components/Download/DownloadItem.jsx +108 -0
  13. package/client/src/components/Download/FlyingThumbnail.css +33 -0
  14. package/client/src/components/Download/FlyingThumbnail.jsx +48 -0
  15. package/client/src/components/Layout/Header.css +91 -0
  16. package/client/src/components/Layout/Header.jsx +49 -0
  17. package/client/src/components/Layout/SystemCheckScreen.css +250 -0
  18. package/client/src/components/Layout/SystemCheckScreen.jsx +110 -0
  19. package/client/src/components/Player/AudioPlayer.css +280 -0
  20. package/client/src/components/Player/AudioPlayer.jsx +154 -0
  21. package/client/src/components/Playlist/PlaylistInput.css +71 -0
  22. package/client/src/components/Playlist/PlaylistInput.jsx +44 -0
  23. package/client/src/components/Playlist/PlaylistView.css +150 -0
  24. package/client/src/components/Playlist/PlaylistView.jsx +104 -0
  25. package/client/src/components/Search/SearchBar.css +163 -0
  26. package/client/src/components/Search/SearchBar.jsx +127 -0
  27. package/client/src/components/Search/SearchResults.css +42 -0
  28. package/client/src/components/Search/SearchResults.jsx +64 -0
  29. package/client/src/components/Search/SongCard.css +296 -0
  30. package/client/src/components/Search/SongCard.jsx +204 -0
  31. package/client/src/components/Settings/SettingsPanel.css +345 -0
  32. package/client/src/components/Settings/SettingsPanel.jsx +174 -0
  33. package/client/src/hooks/useDownload.js +107 -0
  34. package/client/src/hooks/usePlayer.js +188 -0
  35. package/client/src/hooks/usePlaylist.js +52 -0
  36. package/client/src/hooks/useSearch.js +34 -0
  37. package/client/src/index.css +245 -0
  38. package/client/src/main.jsx +10 -0
  39. package/client/src/pages/DownloadsPage.css +37 -0
  40. package/client/src/pages/DownloadsPage.jsx +80 -0
  41. package/client/src/pages/PlaylistPage.jsx +46 -0
  42. package/client/src/pages/SearchPage.jsx +63 -0
  43. package/client/tsconfig.json +24 -0
  44. package/client/vite.config.js +12 -0
  45. package/main.js +79 -0
  46. package/package.json +48 -0
  47. package/server/index.js +54 -0
  48. package/server/package-lock.json +1428 -0
  49. package/server/package.json +14 -0
  50. package/server/routes/browse.js +23 -0
  51. package/server/routes/download.js +339 -0
  52. package/server/routes/playLocal.js +50 -0
  53. package/server/routes/playlist.js +28 -0
  54. package/server/routes/search.js +21 -0
  55. package/server/routes/stream.js +53 -0
  56. package/server/services/downloadService.js +143 -0
  57. package/server/services/ytmusicService.js +62 -0
  58. package/server/utils/dependencyChecker.js +174 -0
  59. package/server/utils/paths.js +11 -0
  60. package/server/utils/sanitize.js +9 -0
@@ -0,0 +1,108 @@
1
+ import { Music, CheckCircle2, AlertCircle, Loader2, Clock } from 'lucide-react';
2
+ import './DownloadItem.css';
3
+
4
+ export default function DownloadItem({ download, onPlay, isCurrentlyPlaying, onSearchAlternative }) {
5
+ const { title, artist, album, thumbnail, status, percent, speed, eta, error } = download;
6
+
7
+ const isClickable = status === 'completed';
8
+
9
+ return (
10
+ <div
11
+ className={`download-item liquid-glass ${status} ${isClickable ? 'clickable' : ''} ${isCurrentlyPlaying ? 'playing' : ''}`}
12
+ onClick={(e) => {
13
+ if (isClickable && onPlay) {
14
+ const rect = e.currentTarget.getBoundingClientRect();
15
+ // Adjust starting rect to represent a smaller thumbnail-like size
16
+ const startRect = {
17
+ top: rect.top + 8,
18
+ left: rect.left + 8,
19
+ width: 56,
20
+ height: 56
21
+ };
22
+ onPlay(download, startRect);
23
+ }
24
+ }}
25
+ >
26
+ <div className="dl-thumbnail">
27
+ {thumbnail ? (
28
+ <>
29
+ <img src={thumbnail} alt={title} />
30
+ {isCurrentlyPlaying && (
31
+ <div className="equalizer-overlay">
32
+ <div className="bar bar1"></div>
33
+ <div className="bar bar2"></div>
34
+ <div className="bar bar3"></div>
35
+ </div>
36
+ )}
37
+ </>
38
+ ) : (
39
+ <div className="placeholder-thumb">
40
+ <Music size={20} className="placeholder-icon" />
41
+ </div>
42
+ )}
43
+ </div>
44
+
45
+ <div className="dl-info">
46
+ <div className="dl-title-row">
47
+ <span className="dl-title" title={title}>
48
+ <span className="dl-title-text">{title}</span>
49
+ {isCurrentlyPlaying && (
50
+ <span className="equalizer-inline">
51
+ <span className="bar bar1"></span>
52
+ <span className="bar bar2"></span>
53
+ <span className="bar bar3"></span>
54
+ </span>
55
+ )}
56
+ </span>
57
+ </div>
58
+
59
+ <span className="dl-meta">
60
+ {artist || 'Unknown Artist'} {album && `• ${album}`}
61
+ </span>
62
+
63
+ {(status === 'downloading' || status === 'queued') && (
64
+ <div className="dl-progress-container">
65
+ <div className="dl-progress-wrapper">
66
+ <div
67
+ className="dl-progress-bar"
68
+ style={{ width: percent || '0%' }}
69
+ />
70
+ </div>
71
+ {status === 'downloading' && (
72
+ <span className="dl-stats">
73
+ {percent} • {speed} • ETA: {eta}
74
+ </span>
75
+ )}
76
+ {status === 'queued' && (
77
+ <span className="dl-stats">Queued...</span>
78
+ )}
79
+ </div>
80
+ )}
81
+
82
+ {status === 'error' && (
83
+ <div className="dl-error-container">
84
+ <span className="dl-error" title={error}>{error || 'Failed to download'}</span>
85
+ <button
86
+ className="dl-search-alternative-btn"
87
+ onClick={(e) => {
88
+ e.stopPropagation();
89
+ if (onSearchAlternative) {
90
+ onSearchAlternative(`${title} ${artist}`);
91
+ }
92
+ }}
93
+ >
94
+ Search Alternative
95
+ </button>
96
+ </div>
97
+ )}
98
+ </div>
99
+
100
+ <div className="dl-status-icon-wrapper">
101
+ {status === 'completed' && <CheckCircle2 size={20} className="status-icon completed" />}
102
+ {status === 'error' && <AlertCircle size={20} className="status-icon error" />}
103
+ {status === 'downloading' && <Loader2 size={20} className="status-icon spinner" />}
104
+ {status === 'queued' && <Clock size={20} className="status-icon queued" />}
105
+ </div>
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,33 @@
1
+ .flying-thumbnail {
2
+ position: fixed;
3
+ z-index: 9999;
4
+ border-radius: var(--radius-sm);
5
+ overflow: hidden;
6
+ background: var(--bg-tertiary);
7
+ pointer-events: none;
8
+ box-shadow: var(--shadow-glow);
9
+ }
10
+
11
+ .flying-thumbnail img {
12
+ width: 100%;
13
+ height: 100%;
14
+ object-fit: cover;
15
+ }
16
+
17
+ .flying-thumbnail.animate {
18
+ animation: flyToTarget 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
19
+ }
20
+
21
+ @keyframes flyToTarget {
22
+ 0% {
23
+ transform: translate(0, 0) scale(1);
24
+ opacity: 1;
25
+ }
26
+ 70% {
27
+ opacity: 0.8;
28
+ }
29
+ 100% {
30
+ transform: translate(var(--end-x), var(--end-y)) scale(0.4);
31
+ opacity: 0;
32
+ }
33
+ }
@@ -0,0 +1,48 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import './FlyingThumbnail.css';
4
+
5
+ export default function FlyingThumbnail({ id, thumbnailSrc, startRect, endRect, onComplete }) {
6
+ const [isAnimating, setIsAnimating] = useState(false);
7
+
8
+ useEffect(() => {
9
+ // Small delay to ensure DOM is ready and CSS transitions can catch the change
10
+ const raf = requestAnimationFrame(() => {
11
+ setIsAnimating(true);
12
+ });
13
+
14
+ // Animation duration is 600ms
15
+ const timer = setTimeout(() => {
16
+ onComplete(id);
17
+ }, 600);
18
+
19
+ return () => {
20
+ cancelAnimationFrame(raf);
21
+ clearTimeout(timer);
22
+ };
23
+ }, [id, onComplete]);
24
+
25
+ // Initial position
26
+ const style = {
27
+ top: startRect.top,
28
+ left: startRect.left,
29
+ width: startRect.width || 40,
30
+ height: startRect.height || 40,
31
+ '--end-x': `${endRect.left - startRect.left}px`,
32
+ '--end-y': `${endRect.top - startRect.top}px`,
33
+ };
34
+
35
+ return createPortal(
36
+ <div
37
+ className={`flying-thumbnail ${isAnimating ? 'animate' : ''}`}
38
+ style={style}
39
+ >
40
+ {thumbnailSrc ? (
41
+ <img src={thumbnailSrc} alt="fly" />
42
+ ) : (
43
+ <div className="placeholder-thumb">🎵</div>
44
+ )}
45
+ </div>,
46
+ document.body
47
+ );
48
+ }
@@ -0,0 +1,91 @@
1
+ .app-header {
2
+ position: sticky;
3
+ top: 0;
4
+ z-index: 100;
5
+ background-color: var(--bg-primary);
6
+ border-bottom: 1px solid var(--glass-border);
7
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
8
+ }
9
+
10
+ .header-content {
11
+ max-width: 1200px;
12
+ margin: 0 auto;
13
+ padding: 0 2rem;
14
+ height: 70px;
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: space-between;
18
+ }
19
+
20
+ .logo h2 {
21
+ font-size: 1.5rem;
22
+ font-weight: 700;
23
+ color: var(--text-primary);
24
+ margin: 0;
25
+ }
26
+
27
+ .logo span {
28
+ background: var(--accent-gradient);
29
+ -webkit-background-clip: text;
30
+ -webkit-text-fill-color: transparent;
31
+ }
32
+
33
+ .tab-nav {
34
+ display: flex;
35
+ gap: 1.5rem;
36
+ height: 100%;
37
+ }
38
+
39
+ .tab-btn {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 0.5rem;
43
+ height: 100%;
44
+ padding: 0 0.5rem;
45
+ font-size: 0.95rem;
46
+ font-weight: 500;
47
+ color: var(--text-secondary);
48
+ position: relative;
49
+ transition: color var(--transition-fast);
50
+ }
51
+
52
+ .tab-btn:hover {
53
+ color: var(--text-primary);
54
+ }
55
+
56
+ .tab-btn.active {
57
+ color: var(--accent-primary);
58
+ }
59
+
60
+ .tab-btn.active::after {
61
+ content: '';
62
+ position: absolute;
63
+ bottom: 0;
64
+ left: 0;
65
+ width: 100%;
66
+ height: 3px;
67
+ background: var(--accent-primary);
68
+ box-shadow: 0 -2px 10px var(--accent-primary);
69
+ border-radius: 3px 3px 0 0;
70
+ }
71
+
72
+ .tab-icon {
73
+ flex-shrink: 0;
74
+ }
75
+
76
+ .tab-badge {
77
+ position: absolute;
78
+ top: 15px;
79
+ right: -12px;
80
+ background: var(--accent-primary);
81
+ color: white;
82
+ min-width: 18px;
83
+ height: 18px;
84
+ border-radius: 50%;
85
+ font-size: 11px;
86
+ font-weight: 700;
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ padding: 0 4px;
91
+ }
@@ -0,0 +1,49 @@
1
+ import { Search, Music, Download, Settings } from 'lucide-react';
2
+ import './Header.css';
3
+
4
+ export default function Header({ activeTab, onTabChange, activeDownloadCount = 0, downloadsTabRef }) {
5
+ return (
6
+ <header className="app-header">
7
+ <div className="header-content">
8
+ <div className="logo">
9
+ <h2>YTM<span>Downloader</span></h2>
10
+ </div>
11
+
12
+ <nav className="tab-nav">
13
+ <button
14
+ className={`tab-btn ${activeTab === 'search' ? 'active' : ''}`}
15
+ onClick={() => onTabChange('search')}
16
+ >
17
+ <Search size={18} className="tab-icon" />
18
+ <span>Search</span>
19
+ </button>
20
+ <button
21
+ className={`tab-btn ${activeTab === 'playlist' ? 'active' : ''}`}
22
+ onClick={() => onTabChange('playlist')}
23
+ >
24
+ <Music size={18} className="tab-icon" />
25
+ <span>Playlist</span>
26
+ </button>
27
+ <button
28
+ ref={downloadsTabRef}
29
+ className={`tab-btn ${activeTab === 'downloads' ? 'active' : ''}`}
30
+ onClick={() => onTabChange('downloads')}
31
+ >
32
+ <Download size={18} className="tab-icon" />
33
+ <span>Downloads</span>
34
+ {activeDownloadCount > 0 && (
35
+ <span className="tab-badge">{activeDownloadCount}</span>
36
+ )}
37
+ </button>
38
+ <button
39
+ className={`tab-btn ${activeTab === 'settings' ? 'active' : ''}`}
40
+ onClick={() => onTabChange('settings')}
41
+ >
42
+ <Settings size={18} className="tab-icon" />
43
+ <span>Settings</span>
44
+ </button>
45
+ </nav>
46
+ </div>
47
+ </header>
48
+ );
49
+ }
@@ -0,0 +1,250 @@
1
+ .system-check-container {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ bottom: 0;
7
+ background: radial-gradient(circle at top right, rgba(99, 102, 241, 0.15) 0%, rgba(15, 23, 42, 0.98) 80%);
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 9999;
12
+ padding: 2rem;
13
+ backdrop-filter: blur(10px);
14
+ -webkit-backdrop-filter: blur(10px);
15
+ }
16
+
17
+ .system-check-card {
18
+ width: 100%;
19
+ max-width: 540px;
20
+ padding: 3rem 2.5rem;
21
+ border-radius: var(--radius-lg);
22
+ text-align: center;
23
+ box-shadow: var(--glass-shadow);
24
+ border: 1px solid var(--glass-border);
25
+ animation: scaleUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
26
+ }
27
+
28
+ .status-checking, .status-failed {
29
+ display: flex;
30
+ flex-direction: column;
31
+ align-items: center;
32
+ gap: 1.5rem;
33
+ }
34
+
35
+ .checking-spinner {
36
+ color: var(--accent-primary);
37
+ animation: spin 1.2s linear infinite;
38
+ filter: drop-shadow(0 0 10px rgba(99, 102, 241, 0.4));
39
+ margin-bottom: 0.5rem;
40
+ }
41
+
42
+ .error-icon {
43
+ color: #f59e0b;
44
+ filter: drop-shadow(0 0 15px rgba(245, 158, 11, 0.3));
45
+ margin-bottom: 0.5rem;
46
+ animation: pulseWarning 2s infinite ease-in-out;
47
+ }
48
+
49
+ .system-check-card h2 {
50
+ font-size: 1.5rem;
51
+ font-weight: 700;
52
+ letter-spacing: -0.02em;
53
+ color: var(--text-primary);
54
+ margin: 0;
55
+ }
56
+
57
+ .status-text {
58
+ font-size: 1.05rem;
59
+ color: var(--text-secondary);
60
+ line-height: 1.6;
61
+ margin: 0;
62
+ }
63
+
64
+ .sub-status-text {
65
+ font-size: 0.85rem;
66
+ color: var(--text-muted);
67
+ margin: 0;
68
+ }
69
+
70
+ .error-summary {
71
+ font-size: 1rem;
72
+ color: var(--text-secondary);
73
+ margin: 0;
74
+ }
75
+
76
+ .detailed-error-box {
77
+ width: 100%;
78
+ padding: 0.75rem 1rem;
79
+ background: rgba(239, 68, 68, 0.08);
80
+ border: 1px solid rgba(239, 68, 68, 0.2);
81
+ border-radius: var(--radius-sm);
82
+ font-family: monospace;
83
+ font-size: 0.75rem;
84
+ color: #ef4444;
85
+ text-align: left;
86
+ max-height: 80px;
87
+ overflow-y: auto;
88
+ }
89
+
90
+ .setup-tabs {
91
+ display: flex;
92
+ width: 100%;
93
+ background: rgba(255, 255, 255, 0.03);
94
+ border: 1px solid var(--glass-border);
95
+ border-radius: var(--radius-sm);
96
+ overflow: hidden;
97
+ margin-top: 0.5rem;
98
+ }
99
+
100
+ .system-check-card .tab-btn {
101
+ flex: 1;
102
+ padding: 0.65rem;
103
+ font-size: 0.9rem;
104
+ font-weight: 600;
105
+ color: var(--text-secondary);
106
+ background: transparent;
107
+ transition: all 0.2s ease;
108
+ border-radius: 0;
109
+ }
110
+
111
+ .system-check-card .tab-btn:hover {
112
+ color: var(--text-primary);
113
+ background: rgba(255, 255, 255, 0.02);
114
+ }
115
+
116
+ .system-check-card .tab-btn.active {
117
+ color: white;
118
+ background: var(--accent-gradient);
119
+ }
120
+
121
+ .setup-guide-content {
122
+ width: 100%;
123
+ text-align: left;
124
+ min-height: 140px;
125
+ }
126
+
127
+ .guide-tab-panel {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 0.75rem;
131
+ animation: fadeInTab 0.25s ease-out;
132
+ }
133
+
134
+ .guide-tab-panel p {
135
+ font-size: 0.9rem;
136
+ color: var(--text-secondary);
137
+ margin: 0;
138
+ line-height: 1.5;
139
+ }
140
+
141
+ .code-block-container {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 0.5rem;
145
+ padding: 0.75rem 1rem;
146
+ background: #090d16;
147
+ border: 1px solid var(--border);
148
+ border-radius: var(--radius-sm);
149
+ color: #38bdf8;
150
+ font-family: monospace;
151
+ font-size: 0.85rem;
152
+ position: relative;
153
+ }
154
+
155
+ .code-icon {
156
+ color: var(--text-muted);
157
+ }
158
+
159
+ .code-block-container code {
160
+ flex: 1;
161
+ word-break: break-all;
162
+ }
163
+
164
+ .guide-footer-text {
165
+ font-size: 0.75rem !important;
166
+ color: var(--text-muted) !important;
167
+ }
168
+
169
+ .guide-footer-text a {
170
+ color: var(--accent-primary);
171
+ text-decoration: underline;
172
+ }
173
+
174
+ .ffmpeg-download-link {
175
+ display: inline-flex;
176
+ align-items: center;
177
+ justify-content: center;
178
+ width: 100%;
179
+ padding: 0.75rem;
180
+ background: rgba(99, 102, 241, 0.1);
181
+ border: 1px solid rgba(99, 102, 241, 0.3);
182
+ border-radius: var(--radius-sm);
183
+ color: var(--text-primary);
184
+ font-weight: 600;
185
+ font-size: 0.9rem;
186
+ text-align: center;
187
+ transition: all 0.2s ease;
188
+ margin-top: 0.25rem;
189
+ }
190
+
191
+ .ffmpeg-download-link:hover {
192
+ background: var(--accent-gradient);
193
+ border-color: transparent;
194
+ color: white;
195
+ box-shadow: var(--shadow-glow);
196
+ }
197
+
198
+ .action-row {
199
+ margin-top: 1rem;
200
+ width: 100%;
201
+ }
202
+
203
+ .btn-retry {
204
+ display: inline-flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ gap: 0.5rem;
208
+ width: 100%;
209
+ padding: 0.75rem;
210
+ background: var(--glass-bg);
211
+ border: 1px solid var(--glass-border);
212
+ border-radius: var(--radius-sm);
213
+ color: var(--text-primary);
214
+ font-weight: 600;
215
+ font-size: 0.95rem;
216
+ transition: all 0.2s ease;
217
+ }
218
+
219
+ .btn-retry:hover {
220
+ background: rgba(255, 255, 255, 0.08);
221
+ border-color: var(--text-secondary);
222
+ }
223
+
224
+ .btn-retry svg {
225
+ transition: transform 0.3s ease;
226
+ }
227
+
228
+ .btn-retry:hover svg {
229
+ transform: rotate(180deg);
230
+ }
231
+
232
+ @keyframes spin {
233
+ 100% { transform: rotate(360deg); }
234
+ }
235
+
236
+ @keyframes scaleUp {
237
+ from { transform: scale(0.95); opacity: 0; }
238
+ to { transform: scale(1); opacity: 1; }
239
+ }
240
+
241
+ @keyframes fadeInTab {
242
+ from { opacity: 0; transform: translateY(5px); }
243
+ to { opacity: 1; transform: translateY(0); }
244
+ }
245
+
246
+ @keyframes pulseWarning {
247
+ 0% { transform: scale(1); }
248
+ 50% { transform: scale(1.05); }
249
+ 100% { transform: scale(1); }
250
+ }
@@ -0,0 +1,110 @@
1
+ import { useState } from 'react';
2
+ import { Loader2, Terminal, AlertTriangle, RefreshCw, HelpCircle } from 'lucide-react';
3
+ import './SystemCheckScreen.css';
4
+
5
+ export default function SystemCheckScreen({ status, error, onRetry }) {
6
+ const [activeTab, setActiveTab] = useState(() => {
7
+ const platform = window.navigator.platform.toLowerCase();
8
+ if (platform.includes('mac')) return 'mac';
9
+ if (platform.includes('win')) return 'windows';
10
+ return 'linux';
11
+ });
12
+
13
+ const isDownloading = status === 'downloading' || status === 'checking';
14
+
15
+ return (
16
+ <div className="system-check-container">
17
+ <div className="system-check-card liquid-glass">
18
+ {isDownloading ? (
19
+ <div className="status-checking">
20
+ <Loader2 className="spinner checking-spinner" size={60} />
21
+ <h2>Setting Up Dependencies</h2>
22
+ <p className="status-text">
23
+ We are automatically configuring <strong>FFmpeg</strong> and downloading <strong>yt-dlp</strong> from GitHub...
24
+ </p>
25
+ <p className="sub-status-text">This will only happen once on first startup. Please wait.</p>
26
+ </div>
27
+ ) : (
28
+ <div className="status-failed">
29
+ <AlertTriangle className="error-icon" size={60} />
30
+ <h2>System Dependencies Missing</h2>
31
+ <p className="error-summary">
32
+ The automated setup failed to configure the required tools on your machine.
33
+ </p>
34
+ {error && <div className="detailed-error-box">Error details: {error}</div>}
35
+
36
+ <div className="setup-tabs">
37
+ <button
38
+ className={`tab-btn ${activeTab === 'mac' ? 'active' : ''}`}
39
+ onClick={() => setActiveTab('mac')}
40
+ >
41
+ macOS
42
+ </button>
43
+ <button
44
+ className={`tab-btn ${activeTab === 'windows' ? 'active' : ''}`}
45
+ onClick={() => setActiveTab('windows')}
46
+ >
47
+ Windows
48
+ </button>
49
+ <button
50
+ className={`tab-btn ${activeTab === 'linux' ? 'active' : ''}`}
51
+ onClick={() => setActiveTab('linux')}
52
+ >
53
+ Linux
54
+ </button>
55
+ </div>
56
+
57
+ <div className="setup-guide-content">
58
+ {activeTab === 'mac' && (
59
+ <div className="guide-tab-panel">
60
+ <p>Open your terminal and paste this command to install the required packages via Homebrew:</p>
61
+ <div className="code-block-container">
62
+ <Terminal size={14} className="code-icon" />
63
+ <code>brew install yt-dlp ffmpeg</code>
64
+ </div>
65
+ <p className="guide-footer-text">Don't have Homebrew? Install it from <a href="https://brew.sh" target="_blank" rel="noreferrer">brew.sh</a> first.</p>
66
+ </div>
67
+ )}
68
+
69
+ {activeTab === 'windows' && (
70
+ <div className="guide-tab-panel">
71
+ <p>1. Open Command Prompt/PowerShell and install <strong>yt-dlp</strong>:</p>
72
+ <div className="code-block-container">
73
+ <Terminal size={14} className="code-icon" />
74
+ <code>winget install yt-dlp</code>
75
+ </div>
76
+ <p>2. Download <strong>FFmpeg</strong> from the official website and add it to your System PATH:</p>
77
+ <a
78
+ href="https://github.com/BtbN/FFmpeg-Builds/releases"
79
+ target="_blank"
80
+ rel="noreferrer"
81
+ className="ffmpeg-download-link"
82
+ >
83
+ Get FFmpeg Builds for Windows
84
+ </a>
85
+ </div>
86
+ )}
87
+
88
+ {activeTab === 'linux' && (
89
+ <div className="guide-tab-panel">
90
+ <p>Run the package manager command to install both tools:</p>
91
+ <div className="code-block-container">
92
+ <Terminal size={14} className="code-icon" />
93
+ <code>sudo apt update && sudo apt install -y ffmpeg yt-dlp</code>
94
+ </div>
95
+ <p className="guide-footer-text">For Fedora/RedHat: <code>sudo dnf install ffmpeg yt-dlp</code></p>
96
+ </div>
97
+ )}
98
+ </div>
99
+
100
+ <div className="action-row">
101
+ <button className="btn-retry" onClick={onRetry}>
102
+ <RefreshCw size={16} /> Check Again
103
+ </button>
104
+ </div>
105
+ </div>
106
+ )}
107
+ </div>
108
+ </div>
109
+ );
110
+ }