@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.
- package/bin/cli.js +44 -0
- package/client/index.html +13 -0
- package/client/package-lock.json +1003 -0
- package/client/package.json +22 -0
- package/client/public/favicon.svg +1 -0
- package/client/public/icons.svg +24 -0
- package/client/src/App.jsx +210 -0
- package/client/src/assets/hero.png +0 -0
- package/client/src/assets/typescript.svg +1 -0
- package/client/src/assets/vite.svg +1 -0
- package/client/src/components/Download/DownloadItem.css +246 -0
- package/client/src/components/Download/DownloadItem.jsx +108 -0
- package/client/src/components/Download/FlyingThumbnail.css +33 -0
- package/client/src/components/Download/FlyingThumbnail.jsx +48 -0
- package/client/src/components/Layout/Header.css +91 -0
- package/client/src/components/Layout/Header.jsx +49 -0
- package/client/src/components/Layout/SystemCheckScreen.css +250 -0
- package/client/src/components/Layout/SystemCheckScreen.jsx +110 -0
- package/client/src/components/Player/AudioPlayer.css +280 -0
- package/client/src/components/Player/AudioPlayer.jsx +154 -0
- package/client/src/components/Playlist/PlaylistInput.css +71 -0
- package/client/src/components/Playlist/PlaylistInput.jsx +44 -0
- package/client/src/components/Playlist/PlaylistView.css +150 -0
- package/client/src/components/Playlist/PlaylistView.jsx +104 -0
- package/client/src/components/Search/SearchBar.css +163 -0
- package/client/src/components/Search/SearchBar.jsx +127 -0
- package/client/src/components/Search/SearchResults.css +42 -0
- package/client/src/components/Search/SearchResults.jsx +64 -0
- package/client/src/components/Search/SongCard.css +296 -0
- package/client/src/components/Search/SongCard.jsx +204 -0
- package/client/src/components/Settings/SettingsPanel.css +345 -0
- package/client/src/components/Settings/SettingsPanel.jsx +174 -0
- package/client/src/hooks/useDownload.js +107 -0
- package/client/src/hooks/usePlayer.js +188 -0
- package/client/src/hooks/usePlaylist.js +52 -0
- package/client/src/hooks/useSearch.js +34 -0
- package/client/src/index.css +245 -0
- package/client/src/main.jsx +10 -0
- package/client/src/pages/DownloadsPage.css +37 -0
- package/client/src/pages/DownloadsPage.jsx +80 -0
- package/client/src/pages/PlaylistPage.jsx +46 -0
- package/client/src/pages/SearchPage.jsx +63 -0
- package/client/tsconfig.json +24 -0
- package/client/vite.config.js +12 -0
- package/main.js +79 -0
- package/package.json +48 -0
- package/server/index.js +54 -0
- package/server/package-lock.json +1428 -0
- package/server/package.json +14 -0
- package/server/routes/browse.js +23 -0
- package/server/routes/download.js +339 -0
- package/server/routes/playLocal.js +50 -0
- package/server/routes/playlist.js +28 -0
- package/server/routes/search.js +21 -0
- package/server/routes/stream.js +53 -0
- package/server/services/downloadService.js +143 -0
- package/server/services/ytmusicService.js +62 -0
- package/server/utils/dependencyChecker.js +174 -0
- package/server/utils/paths.js +11 -0
- package/server/utils/sanitize.js +9 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
.settings-tab-container {
|
|
2
|
+
display: flex;
|
|
3
|
+
justify-content: center;
|
|
4
|
+
padding: 2rem 0;
|
|
5
|
+
animation: fadeIn 0.3s ease;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.settings-card {
|
|
9
|
+
width: 100%;
|
|
10
|
+
max-width: 600px;
|
|
11
|
+
background: var(--bg-secondary);
|
|
12
|
+
border: 1px solid var(--border);
|
|
13
|
+
box-shadow: var(--shadow-lg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.settings-header {
|
|
17
|
+
padding: 1.5rem;
|
|
18
|
+
border-bottom: 1px solid var(--border);
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.settings-title-container {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 0.75rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.settings-title-icon {
|
|
31
|
+
color: var(--accent-primary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.settings-header h2 {
|
|
35
|
+
margin: 0;
|
|
36
|
+
font-size: 1.25rem;
|
|
37
|
+
color: var(--text-primary);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.settings-form {
|
|
41
|
+
padding: 2rem 1.5rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.form-group {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: 0.75rem;
|
|
48
|
+
margin-bottom: 2rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.form-group label {
|
|
52
|
+
font-size: 0.95rem;
|
|
53
|
+
font-weight: 500;
|
|
54
|
+
color: var(--text-primary);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.toggle-group {
|
|
58
|
+
margin-top: 1rem;
|
|
59
|
+
margin-bottom: 2rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.toggle-row {
|
|
63
|
+
display: flex;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
align-items: center;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.toggle-label-text {
|
|
69
|
+
font-size: 0.95rem;
|
|
70
|
+
font-weight: 500;
|
|
71
|
+
color: var(--text-primary);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.toggle-switch {
|
|
75
|
+
position: relative;
|
|
76
|
+
display: inline-block;
|
|
77
|
+
width: 46px;
|
|
78
|
+
height: 24px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.toggle-switch input {
|
|
82
|
+
opacity: 0;
|
|
83
|
+
width: 0;
|
|
84
|
+
height: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.toggle-slider {
|
|
88
|
+
position: absolute;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
top: 0;
|
|
91
|
+
left: 0;
|
|
92
|
+
right: 0;
|
|
93
|
+
bottom: 0;
|
|
94
|
+
background-color: var(--bg-tertiary);
|
|
95
|
+
border: 1px solid var(--border);
|
|
96
|
+
transition: .3s;
|
|
97
|
+
border-radius: 24px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.toggle-slider:before {
|
|
101
|
+
position: absolute;
|
|
102
|
+
content: "";
|
|
103
|
+
height: 16px;
|
|
104
|
+
width: 16px;
|
|
105
|
+
left: 3px;
|
|
106
|
+
bottom: 3px;
|
|
107
|
+
background-color: var(--text-secondary);
|
|
108
|
+
transition: .3s;
|
|
109
|
+
border-radius: 50%;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.toggle-switch input:checked + .toggle-slider {
|
|
113
|
+
background: var(--accent-gradient);
|
|
114
|
+
border-color: transparent;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.toggle-switch input:checked + .toggle-slider:before {
|
|
118
|
+
transform: translateX(22px);
|
|
119
|
+
background-color: #fff;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.toggle-switch input:focus + .toggle-slider {
|
|
123
|
+
box-shadow: 0 0 1px var(--accent-primary);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.form-group input {
|
|
127
|
+
flex: 1;
|
|
128
|
+
background: var(--bg-tertiary);
|
|
129
|
+
border: 1px solid var(--border);
|
|
130
|
+
padding: 0.75rem 1rem;
|
|
131
|
+
border-radius: var(--radius-sm);
|
|
132
|
+
color: var(--text-primary);
|
|
133
|
+
font-size: 1rem;
|
|
134
|
+
outline: none;
|
|
135
|
+
transition: border-color var(--transition-fast);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.form-group input:focus {
|
|
139
|
+
border-color: var(--accent-primary);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.input-with-button {
|
|
143
|
+
display: flex;
|
|
144
|
+
gap: 0.5rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.btn-browse {
|
|
148
|
+
background: var(--bg-tertiary);
|
|
149
|
+
color: var(--text-secondary);
|
|
150
|
+
border: 1px solid var(--border);
|
|
151
|
+
padding: 0 1.25rem;
|
|
152
|
+
border-radius: var(--radius-sm);
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
justify-content: center;
|
|
156
|
+
transition: all var(--transition-fast);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.btn-browse:hover:not(:disabled) {
|
|
160
|
+
background: var(--accent-primary);
|
|
161
|
+
border-color: var(--accent-primary);
|
|
162
|
+
color: white;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.btn-browse:disabled {
|
|
166
|
+
opacity: 0.5;
|
|
167
|
+
cursor: not-allowed;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.help-text {
|
|
171
|
+
margin: 0;
|
|
172
|
+
font-size: 0.8rem;
|
|
173
|
+
color: var(--text-muted);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.settings-actions {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
justify-content: flex-end;
|
|
180
|
+
gap: 1.5rem;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.btn-primary {
|
|
184
|
+
padding: 0.75rem 1.75rem;
|
|
185
|
+
border-radius: var(--radius-sm);
|
|
186
|
+
font-weight: 500;
|
|
187
|
+
font-size: 0.95rem;
|
|
188
|
+
background: var(--accent-gradient);
|
|
189
|
+
color: white;
|
|
190
|
+
transition: all var(--transition-fast);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.btn-primary:hover {
|
|
194
|
+
transform: translateY(-1px);
|
|
195
|
+
box-shadow: var(--shadow-glow);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.settings-divider {
|
|
199
|
+
height: 1px;
|
|
200
|
+
background: var(--glass-border);
|
|
201
|
+
margin: 2rem 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.cache-action-row {
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
gap: 1.5rem;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.btn-danger {
|
|
211
|
+
display: flex;
|
|
212
|
+
align-items: center;
|
|
213
|
+
gap: 0.5rem;
|
|
214
|
+
padding: 0.65rem 1.25rem;
|
|
215
|
+
border-radius: var(--radius-sm);
|
|
216
|
+
font-weight: 500;
|
|
217
|
+
font-size: 0.9rem;
|
|
218
|
+
background: rgba(239, 68, 68, 0.1);
|
|
219
|
+
border: 1px solid rgba(239, 68, 68, 0.4);
|
|
220
|
+
color: #ef4444;
|
|
221
|
+
transition: all var(--transition-fast);
|
|
222
|
+
cursor: pointer;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.btn-danger:hover:not(:disabled) {
|
|
226
|
+
background: rgba(239, 68, 68, 0.2);
|
|
227
|
+
border-color: #ef4444;
|
|
228
|
+
color: #ef4444;
|
|
229
|
+
box-shadow: 0 0 12px rgba(239, 68, 68, 0.3);
|
|
230
|
+
transform: translateY(-1px);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.btn-danger:active {
|
|
234
|
+
transform: translateY(0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.btn-danger:disabled {
|
|
238
|
+
opacity: 0.5;
|
|
239
|
+
cursor: not-allowed;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.cleared-success-msg {
|
|
243
|
+
color: #10b981;
|
|
244
|
+
font-size: 0.9rem;
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
gap: 0.25rem;
|
|
248
|
+
font-weight: 500;
|
|
249
|
+
animation: checkPop 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.saved-success-msg {
|
|
253
|
+
position: absolute;
|
|
254
|
+
top: 1.5rem;
|
|
255
|
+
right: 1.5rem;
|
|
256
|
+
color: #10b981;
|
|
257
|
+
font-size: 0.9rem;
|
|
258
|
+
display: flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
gap: 0.25rem;
|
|
261
|
+
font-weight: 500;
|
|
262
|
+
opacity: 0;
|
|
263
|
+
transform: translateY(-5px);
|
|
264
|
+
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
265
|
+
pointer-events: none;
|
|
266
|
+
z-index: 10;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.saved-success-msg.visible {
|
|
270
|
+
opacity: 1;
|
|
271
|
+
transform: translateY(0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@keyframes fadeIn {
|
|
275
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
276
|
+
to { opacity: 1; transform: translateY(0); }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.label-with-info {
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
gap: 0.5rem;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.tooltip-container {
|
|
286
|
+
position: relative;
|
|
287
|
+
display: inline-flex;
|
|
288
|
+
align-items: center;
|
|
289
|
+
justify-content: center;
|
|
290
|
+
cursor: help;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.info-icon {
|
|
294
|
+
color: var(--text-muted);
|
|
295
|
+
opacity: 0.6;
|
|
296
|
+
transition: opacity 0.2s ease, color 0.2s ease;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.tooltip-container:hover .info-icon {
|
|
300
|
+
opacity: 1;
|
|
301
|
+
color: var(--accent-primary);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.tooltip-text {
|
|
305
|
+
visibility: hidden;
|
|
306
|
+
width: 250px;
|
|
307
|
+
background-color: var(--bg-tertiary);
|
|
308
|
+
color: var(--text-primary);
|
|
309
|
+
text-align: left;
|
|
310
|
+
border: 1px solid var(--border);
|
|
311
|
+
border-radius: var(--radius-sm);
|
|
312
|
+
padding: 0.75rem;
|
|
313
|
+
font-size: 0.8rem;
|
|
314
|
+
font-weight: 400;
|
|
315
|
+
line-height: 1.4;
|
|
316
|
+
box-shadow: var(--shadow-lg);
|
|
317
|
+
backdrop-filter: blur(12px);
|
|
318
|
+
-webkit-backdrop-filter: blur(12px);
|
|
319
|
+
|
|
320
|
+
position: absolute;
|
|
321
|
+
z-index: 100;
|
|
322
|
+
bottom: 125%;
|
|
323
|
+
left: 50%;
|
|
324
|
+
transform: translateX(-50%) translateY(5px);
|
|
325
|
+
opacity: 0;
|
|
326
|
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
327
|
+
pointer-events: none;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.tooltip-text::after {
|
|
331
|
+
content: "";
|
|
332
|
+
position: absolute;
|
|
333
|
+
top: 100%;
|
|
334
|
+
left: 50%;
|
|
335
|
+
margin-left: -5px;
|
|
336
|
+
border-width: 5px;
|
|
337
|
+
border-style: solid;
|
|
338
|
+
border-color: var(--border) transparent transparent transparent;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.tooltip-container:hover .tooltip-text {
|
|
342
|
+
visibility: visible;
|
|
343
|
+
opacity: 1;
|
|
344
|
+
transform: translateX(-50%) translateY(0);
|
|
345
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useState, useRef } from 'react';
|
|
2
|
+
import { Settings, FolderOpen, Check, Trash2, Info } from 'lucide-react';
|
|
3
|
+
import './SettingsPanel.css';
|
|
4
|
+
|
|
5
|
+
export default function SettingsPanel({ outputDir, onSave }) {
|
|
6
|
+
const [dir, setDir] = useState(outputDir);
|
|
7
|
+
const [downloadLyrics, setDownloadLyrics] = useState(() => {
|
|
8
|
+
return localStorage.getItem('ytmd_download_lyrics') !== 'false';
|
|
9
|
+
});
|
|
10
|
+
const [isBrowsing, setIsBrowsing] = useState(false);
|
|
11
|
+
const [showSavedMsg, setShowSavedMsg] = useState(false);
|
|
12
|
+
const [isClearing, setIsClearing] = useState(false);
|
|
13
|
+
const [showClearedMsg, setShowClearedMsg] = useState(false);
|
|
14
|
+
const savedMsgTimeoutRef = useRef(null);
|
|
15
|
+
|
|
16
|
+
const triggerSavedIndicator = () => {
|
|
17
|
+
if (savedMsgTimeoutRef.current) {
|
|
18
|
+
clearTimeout(savedMsgTimeoutRef.current);
|
|
19
|
+
}
|
|
20
|
+
setShowSavedMsg(true);
|
|
21
|
+
savedMsgTimeoutRef.current = setTimeout(() => {
|
|
22
|
+
setShowSavedMsg(false);
|
|
23
|
+
}, 1500);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleBrowse = async () => {
|
|
27
|
+
setIsBrowsing(true);
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch('/api/browse');
|
|
30
|
+
if (res.ok) {
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
if (data.path) {
|
|
33
|
+
setDir(data.path);
|
|
34
|
+
onSave(data.path);
|
|
35
|
+
triggerSavedIndicator();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error('Failed to browse for folder', err);
|
|
40
|
+
} finally {
|
|
41
|
+
setIsBrowsing(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleClearCache = async () => {
|
|
46
|
+
setIsClearing(true);
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch('/api/download/clear-cache', { method: 'POST' });
|
|
49
|
+
if (!res.ok) throw new Error('Failed to clear cache');
|
|
50
|
+
setShowClearedMsg(true);
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
setShowClearedMsg(false);
|
|
53
|
+
}, 3000);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
alert(err.message);
|
|
56
|
+
} finally {
|
|
57
|
+
setIsClearing(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleInputChange = (e) => {
|
|
62
|
+
setDir(e.target.value);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleInputBlur = () => {
|
|
66
|
+
if (dir !== outputDir) {
|
|
67
|
+
onSave(dir);
|
|
68
|
+
triggerSavedIndicator();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleKeyDown = (e) => {
|
|
73
|
+
if (e.key === 'Enter') {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
e.target.blur();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="settings-tab-container">
|
|
81
|
+
<div className="settings-card liquid-glass" style={{ position: 'relative' }}>
|
|
82
|
+
<span className={`saved-success-msg ${showSavedMsg ? 'visible' : ''}`}>
|
|
83
|
+
<Check size={16} /> Auto-saved
|
|
84
|
+
</span>
|
|
85
|
+
|
|
86
|
+
<div className="settings-form">
|
|
87
|
+
<div className="form-group">
|
|
88
|
+
<label htmlFor="outputDir" className="label-with-info">
|
|
89
|
+
Download Directory
|
|
90
|
+
<div className="tooltip-container">
|
|
91
|
+
<Info size={14} className="info-icon" />
|
|
92
|
+
<span className="tooltip-text">Select where your downloaded FLAC / lossy files will be saved on your system.</span>
|
|
93
|
+
</div>
|
|
94
|
+
</label>
|
|
95
|
+
<div className="input-with-button">
|
|
96
|
+
<input
|
|
97
|
+
id="outputDir"
|
|
98
|
+
type="text"
|
|
99
|
+
value={dir}
|
|
100
|
+
onChange={handleInputChange}
|
|
101
|
+
onBlur={handleInputBlur}
|
|
102
|
+
onKeyDown={handleKeyDown}
|
|
103
|
+
placeholder="/Users/sayan/Desktop"
|
|
104
|
+
/>
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
className="btn-browse"
|
|
108
|
+
onClick={handleBrowse}
|
|
109
|
+
disabled={isBrowsing}
|
|
110
|
+
title="Browse directories"
|
|
111
|
+
>
|
|
112
|
+
{isBrowsing ? '...' : <FolderOpen size={18} />}
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="form-group toggle-group">
|
|
118
|
+
<div className="toggle-row">
|
|
119
|
+
<span className="toggle-label-text label-with-info">
|
|
120
|
+
Download & Embed Lyrics
|
|
121
|
+
<div className="tooltip-container">
|
|
122
|
+
<Info size={14} className="info-icon" />
|
|
123
|
+
<span className="tooltip-text">Fetches lyrics from YouTube Music and embeds them into metadata tags inside the audio track.</span>
|
|
124
|
+
</div>
|
|
125
|
+
</span>
|
|
126
|
+
<label className="toggle-switch">
|
|
127
|
+
<input
|
|
128
|
+
id="downloadLyrics"
|
|
129
|
+
type="checkbox"
|
|
130
|
+
checked={downloadLyrics}
|
|
131
|
+
onChange={e => {
|
|
132
|
+
const val = e.target.checked;
|
|
133
|
+
setDownloadLyrics(val);
|
|
134
|
+
localStorage.setItem('ytmd_download_lyrics', val ? 'true' : 'false');
|
|
135
|
+
triggerSavedIndicator();
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
<span className="toggle-slider"></span>
|
|
139
|
+
</label>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="settings-divider" />
|
|
144
|
+
|
|
145
|
+
<div className="form-group cache-group">
|
|
146
|
+
<label className="label-with-info">
|
|
147
|
+
Cache & History
|
|
148
|
+
<div className="tooltip-container">
|
|
149
|
+
<Info size={14} className="info-icon" />
|
|
150
|
+
<span className="tooltip-text">Resets the local YouTube decipher cache (helps fix 403 Forbidden errors) and clears completed or failed items from download history.</span>
|
|
151
|
+
</div>
|
|
152
|
+
</label>
|
|
153
|
+
<div className="cache-action-row">
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
className="btn-danger"
|
|
157
|
+
onClick={handleClearCache}
|
|
158
|
+
disabled={isClearing}
|
|
159
|
+
title="Resets yt-dlp decipher caches and clears completed/failed items from memory"
|
|
160
|
+
>
|
|
161
|
+
<Trash2 size={16} /> {isClearing ? 'Clearing Cache...' : 'Clear Cache & History'}
|
|
162
|
+
</button>
|
|
163
|
+
{showClearedMsg && (
|
|
164
|
+
<span className="cleared-success-msg">
|
|
165
|
+
<Check size={16} /> Cache cleared!
|
|
166
|
+
</span>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useDownload() {
|
|
4
|
+
const [downloads, setDownloads] = useState(new Map());
|
|
5
|
+
|
|
6
|
+
// Connect to SSE stream
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const eventSource = new EventSource('/api/download/progress');
|
|
9
|
+
|
|
10
|
+
eventSource.onmessage = (event) => {
|
|
11
|
+
try {
|
|
12
|
+
const payload = JSON.parse(event.data);
|
|
13
|
+
if (payload.type === 'INITIAL') {
|
|
14
|
+
setDownloads(new Map(payload.data));
|
|
15
|
+
} else if (payload.type === 'DELTA') {
|
|
16
|
+
setDownloads(prev => {
|
|
17
|
+
const next = new Map(prev);
|
|
18
|
+
for (const [id, update] of payload.data) {
|
|
19
|
+
const existing = next.get(id) || {};
|
|
20
|
+
next.set(id, { ...existing, ...update });
|
|
21
|
+
}
|
|
22
|
+
return next;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error('Failed to parse SSE data', err);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
eventSource.onerror = (err) => {
|
|
31
|
+
console.error('SSE Error:', err);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
eventSource.close();
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const startDownload = useCallback(async (song, outputDir) => {
|
|
40
|
+
try {
|
|
41
|
+
const downloadLyrics = localStorage.getItem('ytmd_download_lyrics') !== 'false';
|
|
42
|
+
const res = await fetch('/api/download', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
videoId: song.videoId,
|
|
49
|
+
title: song.title,
|
|
50
|
+
artist: song.artist,
|
|
51
|
+
album: song.album,
|
|
52
|
+
thumbnail: song.thumbnail,
|
|
53
|
+
outputDir,
|
|
54
|
+
downloadLyrics
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
throw new Error('Failed to start download');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
return data.downloadId;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('Download trigger error:', err);
|
|
66
|
+
alert(`Failed to start download: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const startBulkDownload = useCallback(async (songs, outputDir) => {
|
|
71
|
+
try {
|
|
72
|
+
const downloadLyrics = localStorage.getItem('ytmd_download_lyrics') !== 'false';
|
|
73
|
+
const res = await fetch('/api/download/bulk', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
songs: songs.map(s => ({
|
|
78
|
+
videoId: s.videoId,
|
|
79
|
+
title: s.title,
|
|
80
|
+
artist: s.artist,
|
|
81
|
+
album: s.album,
|
|
82
|
+
thumbnail: s.thumbnail
|
|
83
|
+
})),
|
|
84
|
+
outputDir,
|
|
85
|
+
downloadLyrics
|
|
86
|
+
})
|
|
87
|
+
});
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const errorData = await res.json();
|
|
90
|
+
throw new Error(errorData.error || 'Failed to start bulk download');
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error('Bulk download error:', err);
|
|
94
|
+
alert(`Failed to start bulk download: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const clearQueue = useCallback(async () => {
|
|
99
|
+
try {
|
|
100
|
+
await fetch('/api/download/clear-queue', { method: 'POST' });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('Failed to clear queue:', err);
|
|
103
|
+
}
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
return { downloads, startDownload, startBulkDownload, clearQueue };
|
|
107
|
+
}
|