@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,296 @@
1
+ .song-card {
2
+ display: flex;
3
+ align-items: center;
4
+ background: var(--bg-secondary);
5
+ border: 1px solid var(--border);
6
+ border-radius: var(--radius-md);
7
+ padding: 0.75rem;
8
+ gap: 1rem;
9
+ transition: all var(--transition-normal);
10
+ }
11
+
12
+ .song-card:hover {
13
+ background: var(--bg-tertiary);
14
+ border-color: var(--accent-primary);
15
+ box-shadow: var(--shadow-glow);
16
+ transform: scale(1.01);
17
+ }
18
+
19
+ .song-card.playing {
20
+ border-color: var(--accent-primary);
21
+ box-shadow: var(--shadow-glow);
22
+ background: var(--bg-tertiary);
23
+ }
24
+
25
+ .song-card-body {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 1rem;
29
+ flex: 1;
30
+ min-width: 0;
31
+ cursor: pointer;
32
+ }
33
+
34
+ .song-thumbnail {
35
+ width: 60px;
36
+ height: 60px;
37
+ border-radius: var(--radius-sm);
38
+ overflow: hidden;
39
+ flex-shrink: 0;
40
+ background: var(--bg-tertiary);
41
+ position: relative;
42
+ }
43
+
44
+ .song-thumbnail img {
45
+ width: 100%;
46
+ height: 100%;
47
+ object-fit: cover;
48
+ }
49
+
50
+ .thumbnail-hover-hint {
51
+ position: absolute;
52
+ inset: 0;
53
+ background: rgba(0, 0, 0, 0.4);
54
+ backdrop-filter: blur(2px);
55
+ -webkit-backdrop-filter: blur(2px);
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ color: white;
60
+ opacity: 0;
61
+ transition: opacity var(--transition-fast);
62
+ z-index: 5;
63
+ }
64
+
65
+ .song-thumbnail:hover .thumbnail-hover-hint {
66
+ opacity: 1;
67
+ }
68
+
69
+ .equalizer-overlay {
70
+ position: absolute;
71
+ inset: 0;
72
+ background: rgba(0, 0, 0, 0.5);
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ gap: 3px;
77
+ border-radius: var(--radius-sm);
78
+ }
79
+
80
+ .equalizer-overlay .bar {
81
+ width: 4px;
82
+ background: var(--accent-primary);
83
+ border-radius: 2px;
84
+ animation: equalizerBounce 1s ease-in-out infinite alternate;
85
+ }
86
+
87
+ .equalizer-overlay .bar1 { animation-delay: 0.1s; }
88
+ .equalizer-overlay .bar2 { animation-delay: 0.4s; }
89
+ .equalizer-overlay .bar3 { animation-delay: 0.2s; }
90
+
91
+ .placeholder-thumb {
92
+ width: 100%;
93
+ height: 100%;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ font-size: 1.5rem;
98
+ }
99
+
100
+ .song-info {
101
+ flex: 1;
102
+ min-width: 0; /* allows text truncation */
103
+ }
104
+
105
+ .song-title {
106
+ font-size: 15px;
107
+ font-weight: 600;
108
+ color: var(--text-primary);
109
+ margin: 0 0 0.25rem 0;
110
+ white-space: nowrap;
111
+ overflow: hidden;
112
+ text-overflow: ellipsis;
113
+ }
114
+
115
+ .song-meta {
116
+ font-size: 13px;
117
+ color: var(--text-secondary);
118
+ margin: 0;
119
+ white-space: nowrap;
120
+ overflow: hidden;
121
+ text-overflow: ellipsis;
122
+ }
123
+
124
+ .download-btn {
125
+ width: 40px;
126
+ height: 40px;
127
+ border-radius: 50%;
128
+ border: 1px solid var(--accent-primary);
129
+ color: var(--accent-primary);
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ transition: all var(--transition-fast);
134
+ flex-shrink: 0;
135
+ margin-right: 0.5rem;
136
+ }
137
+
138
+ .download-btn:hover {
139
+ background: var(--accent-primary);
140
+ color: white;
141
+ transform: scale(1.15) rotate(5deg);
142
+ }
143
+
144
+ .download-btn:active {
145
+ transform: scale(0.9);
146
+ }
147
+
148
+ .progress-ring {
149
+ width: 40px;
150
+ height: 40px;
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ border-radius: var(--radius-sm);
155
+ background: var(--bg-tertiary);
156
+ box-shadow: var(--shadow-glow);
157
+ flex-shrink: 0;
158
+ margin-right: 0.5rem;
159
+ }
160
+
161
+ .progress-ring__circle {
162
+ transition: stroke-dashoffset 0.3s ease;
163
+ transform: rotate(-90deg);
164
+ transform-origin: 50% 50%;
165
+ }
166
+
167
+ .progress-ring__circle.pulsing {
168
+ animation: pulse 1.5s infinite ease-in-out;
169
+ }
170
+
171
+ .check-icon, .error-icon {
172
+ animation: checkPop 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
173
+ }
174
+
175
+ @keyframes checkPop {
176
+ 0% { transform: scale(0); }
177
+ 50% { transform: scale(1.3); }
178
+ 100% { transform: scale(1); }
179
+ }
180
+
181
+ /* --- Hover Modal Styles --- */
182
+
183
+ .song-hover-modal-overlay {
184
+ perspective: 1000px;
185
+ }
186
+
187
+ .song-hover-modal {
188
+ background: rgba(22, 22, 26, 0.85); /* Slightly darker for contrast */
189
+ backdrop-filter: blur(24px);
190
+ -webkit-backdrop-filter: blur(24px);
191
+ border: 1px solid rgba(255, 255, 255, 0.1);
192
+ border-radius: var(--radius-lg);
193
+ padding: 1rem;
194
+ box-shadow: 0 24px 48px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--accent-primary);
195
+ display: flex;
196
+ flex-direction: column;
197
+ gap: 1rem;
198
+ animation: modalPopUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
199
+ transform-origin: center center;
200
+ }
201
+
202
+ @keyframes modalPopUp {
203
+ 0% {
204
+ opacity: 0;
205
+ transform: translate(-50%, -50%) scale(0.9) translateY(15px) rotateX(10deg);
206
+ }
207
+ 100% {
208
+ opacity: 1;
209
+ transform: translate(-50%, -50%) scale(1) translateY(0) rotateX(0);
210
+ }
211
+ }
212
+
213
+ .hover-modal-hero {
214
+ position: relative;
215
+ width: 100%;
216
+ aspect-ratio: 1 / 1;
217
+ border-radius: var(--radius-md);
218
+ overflow: hidden;
219
+ background: var(--bg-tertiary);
220
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05);
221
+ }
222
+
223
+ .hover-modal-img {
224
+ width: 100%;
225
+ height: 100%;
226
+ object-fit: cover;
227
+ animation: imageFadeIn 0.3s ease;
228
+ }
229
+
230
+ @keyframes imageFadeIn {
231
+ from { opacity: 0; transform: scale(1.05); }
232
+ to { opacity: 1; transform: scale(1); }
233
+ }
234
+
235
+ .hover-placeholder {
236
+ width: 100%;
237
+ height: 100%;
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: center;
241
+ color: var(--text-secondary);
242
+ }
243
+
244
+ .hover-modal-content {
245
+ display: flex;
246
+ flex-direction: column;
247
+ gap: 0.75rem;
248
+ padding: 0 0.25rem;
249
+ }
250
+
251
+ .hover-modal-title {
252
+ font-size: 1.25rem;
253
+ font-weight: 700;
254
+ color: var(--text-primary);
255
+ margin: 0;
256
+ line-height: 1.3;
257
+ }
258
+
259
+ .hover-modal-details {
260
+ display: flex;
261
+ flex-direction: column;
262
+ gap: 0.5rem;
263
+ }
264
+
265
+ .hover-detail-item {
266
+ display: flex;
267
+ align-items: center;
268
+ gap: 0.6rem;
269
+ font-size: 0.9rem;
270
+ color: var(--text-secondary);
271
+ font-weight: 500;
272
+ }
273
+
274
+ .hover-detail-item svg {
275
+ color: var(--accent-primary);
276
+ opacity: 0.8;
277
+ }
278
+
279
+ .hover-playing-badge {
280
+ position: absolute;
281
+ top: 0.75rem;
282
+ right: 0.75rem;
283
+ background: rgba(16, 185, 129, 0.2);
284
+ backdrop-filter: blur(8px);
285
+ -webkit-backdrop-filter: blur(8px);
286
+ border: 1px solid rgba(16, 185, 129, 0.4);
287
+ color: #10b981;
288
+ padding: 0.35rem 0.6rem;
289
+ border-radius: var(--radius-sm);
290
+ font-size: 0.75rem;
291
+ font-weight: 700;
292
+ display: flex;
293
+ align-items: center;
294
+ gap: 0.5rem;
295
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
296
+ }
@@ -0,0 +1,204 @@
1
+ import { useRef, useState, useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { Music, Disc3, Clock, Maximize2 } from 'lucide-react';
4
+ import './SongCard.css';
5
+
6
+ function formatDuration(seconds) {
7
+ if (!seconds) return '0:00';
8
+ const m = Math.floor(seconds / 60);
9
+ const s = seconds % 60;
10
+ return `${m}:${s.toString().padStart(2, '0')}`;
11
+ }
12
+
13
+ export default function SongCard({ song, onDownload, onFlyAnimation, downloadStatus, downloadPercent, onPlay, isCurrentlyPlaying }) {
14
+ const buttonRef = useRef(null);
15
+ const cardRef = useRef(null);
16
+ const [showModal, setShowModal] = useState(false);
17
+ const [modalRect, setModalRect] = useState(null);
18
+
19
+ const handleDownloadClick = () => {
20
+ if (buttonRef.current && onFlyAnimation) {
21
+ const rect = buttonRef.current.getBoundingClientRect();
22
+ onFlyAnimation({
23
+ thumbnailSrc: song.thumbnail,
24
+ startRect: rect
25
+ });
26
+ }
27
+ onDownload(song);
28
+ };
29
+
30
+ const handleThumbnailClick = (e) => {
31
+ e.stopPropagation();
32
+ if (cardRef.current) {
33
+ setModalRect(cardRef.current.getBoundingClientRect());
34
+ setShowModal(true);
35
+ }
36
+ };
37
+
38
+ return (
39
+ <>
40
+ <div
41
+ ref={cardRef}
42
+ className={`song-card liquid-glass ${isCurrentlyPlaying ? 'playing' : ''}`}
43
+ >
44
+ <div
45
+ className="song-card-body"
46
+ onClick={(e) => {
47
+ if (onPlay) {
48
+ const thumbEl = e.currentTarget.querySelector('.song-thumbnail');
49
+ const rect = thumbEl ? thumbEl.getBoundingClientRect() : e.currentTarget.getBoundingClientRect();
50
+ onPlay(song, rect);
51
+ }
52
+ }}
53
+ title="Play song"
54
+ >
55
+ <div
56
+ className="song-thumbnail"
57
+ onClick={handleThumbnailClick}
58
+ title="Click to view details"
59
+ >
60
+ {song.thumbnail ? (
61
+ <>
62
+ <img src={song.thumbnail} alt={song.title} />
63
+ {isCurrentlyPlaying && (
64
+ <div className="equalizer-overlay">
65
+ <div className="bar bar1"></div>
66
+ <div className="bar bar2"></div>
67
+ <div className="bar bar3"></div>
68
+ </div>
69
+ )}
70
+ </>
71
+ ) : (
72
+ <div className="placeholder-thumb">
73
+ <Music size={24} className="placeholder-icon" />
74
+ </div>
75
+ )}
76
+ <div className="thumbnail-hover-hint">
77
+ <Maximize2 size={20} />
78
+ </div>
79
+ </div>
80
+
81
+ <div className="song-info">
82
+ <h3 className="song-title">{song.title}</h3>
83
+ <p className="song-meta">
84
+ {song.artist} {song.album && `• ${song.album}`} • {formatDuration(song.duration)}
85
+ </p>
86
+ </div>
87
+ </div>
88
+
89
+ {(!downloadStatus) && (
90
+ <button
91
+ ref={buttonRef}
92
+ className="download-btn"
93
+ onClick={handleDownloadClick}
94
+ title="Download"
95
+ >
96
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
97
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
98
+ <polyline points="7 10 12 15 17 10"></polyline>
99
+ <line x1="12" y1="15" x2="12" y2="3"></line>
100
+ </svg>
101
+ </button>
102
+ )}
103
+
104
+ {(downloadStatus === 'queued' || downloadStatus === 'downloading') && (() => {
105
+ const pctFloat = parseFloat(downloadPercent) || 0;
106
+ const radius = 16;
107
+ const circumference = 2 * Math.PI * radius;
108
+ const strokeDashoffset = circumference - (pctFloat / 100) * circumference;
109
+
110
+ return (
111
+ <div className="progress-ring">
112
+ <svg width="40" height="40" viewBox="0 0 40 40">
113
+ <circle cx="20" cy="20" r={radius} fill="none" stroke="var(--border)" strokeWidth="3" />
114
+ <circle
115
+ className={`progress-ring__circle ${downloadStatus === 'queued' ? 'pulsing' : ''}`}
116
+ cx="20" cy="20" r={radius}
117
+ fill="none"
118
+ stroke="var(--accent-primary)"
119
+ strokeWidth="3"
120
+ strokeDasharray={circumference}
121
+ strokeDashoffset={strokeDashoffset}
122
+ strokeLinecap="round"
123
+ />
124
+ </svg>
125
+ </div>
126
+ );
127
+ })()}
128
+
129
+ {downloadStatus === 'completed' && (
130
+ <div className="progress-ring">
131
+ <svg className="check-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
132
+ <polyline points="20 6 9 17 4 12"></polyline>
133
+ </svg>
134
+ </div>
135
+ )}
136
+
137
+ {downloadStatus === 'error' && (
138
+ <div className="progress-ring">
139
+ <svg className="error-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
140
+ <line x1="18" y1="6" x2="6" y2="18"></line>
141
+ <line x1="6" y1="6" x2="18" y2="18"></line>
142
+ </svg>
143
+ </div>
144
+ )}
145
+ </div>
146
+
147
+ {showModal && modalRect && createPortal(
148
+ <div
149
+ className="song-hover-modal-overlay"
150
+ onClick={() => setShowModal(false)}
151
+ style={{
152
+ position: 'fixed',
153
+ top: 0,
154
+ left: 0,
155
+ width: '100vw',
156
+ height: '100vh',
157
+ pointerEvents: 'auto',
158
+ zIndex: 99999,
159
+ }}
160
+ >
161
+ <div
162
+ className="song-hover-modal liquid-glass"
163
+ onClick={(e) => e.stopPropagation()}
164
+ style={{
165
+ position: 'absolute',
166
+ top: `${modalRect.top + modalRect.height / 2}px`,
167
+ left: `${modalRect.left + modalRect.width / 2}px`,
168
+ width: `${Math.max(350, modalRect.width * 1.15)}px`,
169
+ transform: 'translate(-50%, -50%) scale(1)'
170
+ }}
171
+ >
172
+ <div className="hover-modal-hero">
173
+ {song.thumbnail ? (
174
+ <img src={song.thumbnail.replace('w120-h120', 'w480-h480')} alt={song.title} className="hover-modal-img" />
175
+ ) : (
176
+ <div className="hover-placeholder"><Music size={64} /></div>
177
+ )}
178
+ {isCurrentlyPlaying && (
179
+ <div className="hover-playing-badge">
180
+ <div className="equalizer-overlay active">
181
+ <div className="bar bar1"></div>
182
+ <div className="bar bar2"></div>
183
+ <div className="bar bar3"></div>
184
+ </div>
185
+ <span>PLAYING</span>
186
+ </div>
187
+ )}
188
+ </div>
189
+
190
+ <div className="hover-modal-content">
191
+ <h2 className="hover-modal-title">{song.title}</h2>
192
+ <div className="hover-modal-details">
193
+ <span className="hover-detail-item"><Music size={14} /> {song.artist}</span>
194
+ {song.album && <span className="hover-detail-item"><Disc3 size={14} /> {song.album}</span>}
195
+ <span className="hover-detail-item"><Clock size={14} /> {formatDuration(song.duration)}</span>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>,
200
+ document.body
201
+ )}
202
+ </>
203
+ );
204
+ }