@involvex/youtube-music-cli 0.0.11 → 0.0.12
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/CHANGELOG.md +2 -0
- package/dist/source/components/playlist/PlaylistList.js +17 -2
- package/dist/source/components/search/SearchResults.js +12 -3
- package/dist/source/services/download/download.service.d.ts +3 -0
- package/dist/source/services/download/download.service.js +129 -27
- package/dist/source/services/youtube-music/api.js +33 -100
- package/dist/youtube-music-cli.exe +0 -0
- package/package.json +3 -13
package/CHANGELOG.md
CHANGED
|
@@ -22,6 +22,7 @@ export default function PlaylistList() {
|
|
|
22
22
|
const [renamingPlaylistId, setRenamingPlaylistId] = useState(null);
|
|
23
23
|
const [renameValue, setRenameValue] = useState('');
|
|
24
24
|
const [downloadStatus, setDownloadStatus] = useState(null);
|
|
25
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
25
26
|
useKeyboardBlocker(renamingPlaylistId !== null);
|
|
26
27
|
const handleCreate = useCallback(() => {
|
|
27
28
|
const name = `Playlist ${playlists.length + 1}`;
|
|
@@ -82,6 +83,10 @@ export default function PlaylistList() {
|
|
|
82
83
|
const handleDownload = useCallback(async () => {
|
|
83
84
|
if (renamingPlaylistId)
|
|
84
85
|
return;
|
|
86
|
+
if (isDownloading) {
|
|
87
|
+
setDownloadStatus('Download already in progress. Please wait.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
85
90
|
const playlist = playlists[selectedIndex];
|
|
86
91
|
if (!playlist)
|
|
87
92
|
return;
|
|
@@ -95,15 +100,25 @@ export default function PlaylistList() {
|
|
|
95
100
|
setDownloadStatus(`No tracks to download in "${playlist.name}".`);
|
|
96
101
|
return;
|
|
97
102
|
}
|
|
98
|
-
setDownloadStatus(`Downloading ${target.tracks.length} track(s) from "${playlist.name}"
|
|
103
|
+
setDownloadStatus(`Downloading ${target.tracks.length} track(s) from "${playlist.name}"... this can take a few minutes.`);
|
|
99
104
|
try {
|
|
105
|
+
setIsDownloading(true);
|
|
100
106
|
const summary = await downloadService.downloadTracks(target.tracks);
|
|
101
107
|
setDownloadStatus(`Downloaded ${summary.downloaded}, skipped ${summary.skipped}, failed ${summary.failed}.`);
|
|
102
108
|
}
|
|
103
109
|
catch (error) {
|
|
104
110
|
setDownloadStatus(error instanceof Error ? error.message : 'Failed to download playlist.');
|
|
105
111
|
}
|
|
106
|
-
|
|
112
|
+
finally {
|
|
113
|
+
setIsDownloading(false);
|
|
114
|
+
}
|
|
115
|
+
}, [
|
|
116
|
+
downloadService,
|
|
117
|
+
isDownloading,
|
|
118
|
+
playlists,
|
|
119
|
+
renamingPlaylistId,
|
|
120
|
+
selectedIndex,
|
|
121
|
+
]);
|
|
107
122
|
useKeyBinding(KEYBINDINGS.UP, navigateUp);
|
|
108
123
|
useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
|
|
109
124
|
useKeyBinding(KEYBINDINGS.SELECT, startPlaylist);
|
|
@@ -9,7 +9,7 @@ import { usePlayer } from "../../hooks/usePlayer.js";
|
|
|
9
9
|
import { usePlaylist } from "../../hooks/usePlaylist.js";
|
|
10
10
|
import { KEYBINDINGS, VIEW } from "../../utils/constants.js";
|
|
11
11
|
import { truncate } from "../../utils/format.js";
|
|
12
|
-
import { useCallback, useRef, useEffect } from 'react';
|
|
12
|
+
import { useCallback, useRef, useEffect, useState } from 'react';
|
|
13
13
|
import { logger } from "../../services/logger/logger.service.js";
|
|
14
14
|
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
15
15
|
import { getMusicService } from "../../services/youtube-music/api.js";
|
|
@@ -24,6 +24,7 @@ function SearchResults({ results, selectedIndex, isActive = true, onMixCreated,
|
|
|
24
24
|
const musicService = getMusicService();
|
|
25
25
|
const downloadService = getDownloadService();
|
|
26
26
|
const { createPlaylist } = usePlaylist();
|
|
27
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
27
28
|
const mixCreatedRef = useRef(onMixCreated);
|
|
28
29
|
mixCreatedRef.current = onMixCreated;
|
|
29
30
|
const downloadStatusRef = useRef(onDownloadStatus);
|
|
@@ -225,6 +226,10 @@ function SearchResults({ results, selectedIndex, isActive = true, onMixCreated,
|
|
|
225
226
|
const downloadSelected = useCallback(async () => {
|
|
226
227
|
if (!isActive)
|
|
227
228
|
return;
|
|
229
|
+
if (isDownloading) {
|
|
230
|
+
downloadStatusRef.current?.('Download already in progress. Please wait.');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
228
233
|
const selected = results[selectedIndex];
|
|
229
234
|
if (!selected)
|
|
230
235
|
return;
|
|
@@ -234,19 +239,23 @@ function SearchResults({ results, selectedIndex, isActive = true, onMixCreated,
|
|
|
234
239
|
return;
|
|
235
240
|
}
|
|
236
241
|
try {
|
|
242
|
+
setIsDownloading(true);
|
|
237
243
|
const target = await downloadService.resolveSearchTarget(selected);
|
|
238
244
|
if (target.tracks.length === 0) {
|
|
239
245
|
downloadStatusRef.current?.(`No tracks found for "${target.name}".`);
|
|
240
246
|
return;
|
|
241
247
|
}
|
|
242
|
-
downloadStatusRef.current?.(`Downloading ${target.tracks.length} track(s) from "${target.name}"
|
|
248
|
+
downloadStatusRef.current?.(`Downloading ${target.tracks.length} track(s) from "${target.name}"... this can take a few minutes.`);
|
|
243
249
|
const summary = await downloadService.downloadTracks(target.tracks);
|
|
244
250
|
downloadStatusRef.current?.(`Downloaded ${summary.downloaded}, skipped ${summary.skipped}, failed ${summary.failed}.`);
|
|
245
251
|
}
|
|
246
252
|
catch (error) {
|
|
247
253
|
downloadStatusRef.current?.(error instanceof Error ? error.message : 'Download failed.');
|
|
248
254
|
}
|
|
249
|
-
|
|
255
|
+
finally {
|
|
256
|
+
setIsDownloading(false);
|
|
257
|
+
}
|
|
258
|
+
}, [downloadService, isActive, isDownloading, results, selectedIndex]);
|
|
250
259
|
useKeyBinding(KEYBINDINGS.UP, navigateUp);
|
|
251
260
|
useKeyBinding(KEYBINDINGS.DOWN, navigateDown);
|
|
252
261
|
useKeyBinding(KEYBINDINGS.SELECT, handleSelect);
|
|
@@ -13,6 +13,7 @@ type DownloadTarget = {
|
|
|
13
13
|
declare class DownloadService {
|
|
14
14
|
private ffmpegChecked;
|
|
15
15
|
private ffmpegAvailable;
|
|
16
|
+
private activeDownload;
|
|
16
17
|
private readonly config;
|
|
17
18
|
private readonly musicService;
|
|
18
19
|
getConfig(): {
|
|
@@ -30,6 +31,8 @@ declare class DownloadService {
|
|
|
30
31
|
private ensureFfmpeg;
|
|
31
32
|
private convertAudio;
|
|
32
33
|
private runFfmpeg;
|
|
34
|
+
private recordViaYtDlp;
|
|
35
|
+
private recordViaMpv;
|
|
33
36
|
}
|
|
34
37
|
export declare function getDownloadService(): DownloadService;
|
|
35
38
|
export {};
|
|
@@ -7,6 +7,7 @@ import { getMusicService } from "../youtube-music/api.js";
|
|
|
7
7
|
class DownloadService {
|
|
8
8
|
ffmpegChecked = false;
|
|
9
9
|
ffmpegAvailable = false;
|
|
10
|
+
activeDownload = false;
|
|
10
11
|
config = getConfigService();
|
|
11
12
|
musicService = getMusicService();
|
|
12
13
|
getConfig() {
|
|
@@ -55,49 +56,91 @@ class DownloadService {
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
async downloadTracks(tracks) {
|
|
59
|
+
if (this.activeDownload) {
|
|
60
|
+
throw new Error('A download is already in progress. Please wait for it to finish.');
|
|
61
|
+
}
|
|
58
62
|
const { directory, format } = this.getConfig();
|
|
59
63
|
if (!directory) {
|
|
60
64
|
throw new Error('No download directory configured.');
|
|
61
65
|
}
|
|
62
66
|
mkdirSync(directory, { recursive: true });
|
|
63
67
|
await this.ensureFfmpeg();
|
|
68
|
+
this.activeDownload = true;
|
|
64
69
|
const result = {
|
|
65
70
|
downloaded: 0,
|
|
66
71
|
skipped: 0,
|
|
67
72
|
failed: 0,
|
|
68
73
|
errors: [],
|
|
69
74
|
};
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
try {
|
|
76
|
+
for (const track of tracks) {
|
|
77
|
+
const destination = this.getDestinationPath(track, directory, format);
|
|
78
|
+
const tempSource = `${destination}.source`;
|
|
79
|
+
try {
|
|
80
|
+
logger.info('DownloadService', 'Starting track download', {
|
|
81
|
+
videoId: track.videoId,
|
|
82
|
+
title: track.title,
|
|
83
|
+
});
|
|
84
|
+
if (existsSync(destination)) {
|
|
85
|
+
result.skipped++;
|
|
86
|
+
logger.debug('DownloadService', 'Skipping existing file', {
|
|
87
|
+
destination,
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const streamUrl = await this.musicService.getStreamUrl(track.videoId);
|
|
93
|
+
const audioBuffer = await this.fetchAudio(streamUrl);
|
|
94
|
+
writeFileSync(tempSource, audioBuffer);
|
|
95
|
+
}
|
|
96
|
+
catch (streamError) {
|
|
97
|
+
logger.warn('DownloadService', 'Stream URL extraction failed, falling back to yt-dlp', {
|
|
98
|
+
videoId: track.videoId,
|
|
99
|
+
error: streamError instanceof Error
|
|
100
|
+
? streamError.message
|
|
101
|
+
: String(streamError),
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
await this.recordViaYtDlp(track.videoId, tempSource);
|
|
105
|
+
}
|
|
106
|
+
catch (ytdlpError) {
|
|
107
|
+
logger.warn('DownloadService', 'yt-dlp fallback failed, falling back to mpv recording', {
|
|
108
|
+
videoId: track.videoId,
|
|
109
|
+
error: ytdlpError instanceof Error
|
|
110
|
+
? ytdlpError.message
|
|
111
|
+
: String(ytdlpError),
|
|
112
|
+
});
|
|
113
|
+
await this.recordViaMpv(track.videoId, tempSource);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await this.convertAudio(tempSource, destination, format);
|
|
117
|
+
result.downloaded++;
|
|
118
|
+
logger.info('DownloadService', 'Track download complete', {
|
|
119
|
+
videoId: track.videoId,
|
|
120
|
+
destination,
|
|
121
|
+
});
|
|
77
122
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
finally {
|
|
95
|
-
if (existsSync(tempSource)) {
|
|
96
|
-
unlinkSync(tempSource);
|
|
123
|
+
catch (error) {
|
|
124
|
+
result.failed++;
|
|
125
|
+
const message = error instanceof Error ? error.message : 'Unknown download failure';
|
|
126
|
+
result.errors.push(message);
|
|
127
|
+
logger.error('DownloadService', 'Track download failed', {
|
|
128
|
+
videoId: track.videoId,
|
|
129
|
+
title: track.title,
|
|
130
|
+
error: message,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
if (existsSync(tempSource)) {
|
|
135
|
+
unlinkSync(tempSource);
|
|
136
|
+
}
|
|
97
137
|
}
|
|
98
138
|
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
this.activeDownload = false;
|
|
99
143
|
}
|
|
100
|
-
return result;
|
|
101
144
|
}
|
|
102
145
|
uniqueTracks(tracks) {
|
|
103
146
|
const seen = new Set();
|
|
@@ -187,6 +230,65 @@ class DownloadService {
|
|
|
187
230
|
});
|
|
188
231
|
});
|
|
189
232
|
}
|
|
233
|
+
async recordViaYtDlp(videoId, outputPath) {
|
|
234
|
+
const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
235
|
+
await new Promise((resolve, reject) => {
|
|
236
|
+
const process = spawn('yt-dlp', [
|
|
237
|
+
'--no-playlist',
|
|
238
|
+
'--quiet',
|
|
239
|
+
'--no-warnings',
|
|
240
|
+
'--js-runtimes',
|
|
241
|
+
'node',
|
|
242
|
+
'-f',
|
|
243
|
+
'bestaudio',
|
|
244
|
+
'--output',
|
|
245
|
+
outputPath,
|
|
246
|
+
watchUrl,
|
|
247
|
+
], { windowsHide: true });
|
|
248
|
+
let stderr = '';
|
|
249
|
+
let stdout = '';
|
|
250
|
+
process.stderr.on('data', chunk => {
|
|
251
|
+
stderr += String(chunk);
|
|
252
|
+
});
|
|
253
|
+
process.stdout.on('data', chunk => {
|
|
254
|
+
stdout += String(chunk);
|
|
255
|
+
});
|
|
256
|
+
process.on('error', reject);
|
|
257
|
+
process.on('exit', code => {
|
|
258
|
+
if (code === 0 && existsSync(outputPath)) {
|
|
259
|
+
resolve();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
reject(new Error((stderr || stdout).trim() ||
|
|
263
|
+
`yt-dlp exited with code ${code} and no output file`));
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async recordViaMpv(videoId, outputPath) {
|
|
268
|
+
const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
269
|
+
await new Promise((resolve, reject) => {
|
|
270
|
+
const process = spawn('mpv', [
|
|
271
|
+
watchUrl,
|
|
272
|
+
'--no-video',
|
|
273
|
+
'--ao=null',
|
|
274
|
+
'--ytdl=yes',
|
|
275
|
+
'--really-quiet',
|
|
276
|
+
`--stream-record=${outputPath}`,
|
|
277
|
+
], { windowsHide: true });
|
|
278
|
+
let stderr = '';
|
|
279
|
+
process.stderr.on('data', chunk => {
|
|
280
|
+
stderr += String(chunk);
|
|
281
|
+
});
|
|
282
|
+
process.on('error', reject);
|
|
283
|
+
process.on('exit', code => {
|
|
284
|
+
if (code === 0 && existsSync(outputPath)) {
|
|
285
|
+
resolve();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
reject(new Error(stderr.trim() || `mpv exited with code ${code} and no output file`));
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
190
292
|
}
|
|
191
293
|
let downloadServiceInstance = null;
|
|
192
294
|
export function getDownloadService() {
|
|
@@ -295,119 +295,52 @@ class MusicService {
|
|
|
295
295
|
}
|
|
296
296
|
async getStreamUrl(videoId) {
|
|
297
297
|
logger.info('MusicService', 'Starting stream extraction', { videoId });
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
});
|
|
303
|
-
const ytdl = await import('@distube/ytdl-core');
|
|
304
|
-
const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
305
|
-
const info = await ytdl.default.getInfo(videoUrl);
|
|
306
|
-
logger.debug('MusicService', 'ytdl-core getInfo succeeded', {
|
|
307
|
-
formatCount: info.formats.length,
|
|
308
|
-
});
|
|
309
|
-
const audioFormats = ytdl.default.filterFormats(info.formats, 'audioonly');
|
|
310
|
-
logger.debug('MusicService', 'ytdl-core audio formats filtered', {
|
|
311
|
-
audioFormatCount: audioFormats.length,
|
|
312
|
-
});
|
|
313
|
-
if (audioFormats.length > 0) {
|
|
314
|
-
// Get highest quality audio
|
|
315
|
-
const bestAudio = audioFormats.sort((a, b) => {
|
|
316
|
-
const aBitrate = Number.parseInt(String(a.audioBitrate || 0));
|
|
317
|
-
const bBitrate = Number.parseInt(String(b.audioBitrate || 0));
|
|
318
|
-
return bBitrate - aBitrate;
|
|
319
|
-
})[0];
|
|
320
|
-
if (bestAudio?.url) {
|
|
321
|
-
logger.info('MusicService', 'Using ytdl-core stream', {
|
|
322
|
-
bitrate: bestAudio.audioBitrate,
|
|
323
|
-
urlLength: bestAudio.url.length,
|
|
324
|
-
mimeType: bestAudio.mimeType,
|
|
325
|
-
});
|
|
326
|
-
return bestAudio.url;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
logger.warn('MusicService', 'ytdl-core: No audio formats with URL found');
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
logger.error('MusicService', 'ytdl-core extraction failed', {
|
|
333
|
-
error: error instanceof Error ? error.message : String(error),
|
|
334
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
335
|
-
});
|
|
298
|
+
const isBunRuntime = typeof globalThis.Bun !== 'undefined';
|
|
299
|
+
// Try Method 1: @distube/ytdl-core (skip under Bun due undici incompatibility)
|
|
300
|
+
if (isBunRuntime) {
|
|
301
|
+
logger.warn('MusicService', 'Skipping ytdl-core extraction on Bun runtime', { videoId });
|
|
336
302
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
const yt = await getClient();
|
|
343
|
-
const video = (await yt.getInfo(videoId));
|
|
344
|
-
logger.debug('MusicService', 'youtubei.js getInfo succeeded');
|
|
345
|
-
// Get the download URL for the video
|
|
346
|
-
const streamData = video.chooseFormat?.({
|
|
347
|
-
type: 'audio',
|
|
348
|
-
quality: 'best',
|
|
349
|
-
});
|
|
350
|
-
if (streamData?.url) {
|
|
351
|
-
logger.info('MusicService', 'Using youtubei.js stream (chooseFormat)', {
|
|
352
|
-
urlLength: streamData.url.length,
|
|
303
|
+
else {
|
|
304
|
+
try {
|
|
305
|
+
logger.debug('MusicService', 'Attempting ytdl-core extraction', {
|
|
306
|
+
videoId,
|
|
353
307
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (streamingData?.adaptive_formats) {
|
|
360
|
-
logger.debug('MusicService', 'Found adaptive_formats', {
|
|
361
|
-
count: streamingData.adaptive_formats.length,
|
|
308
|
+
const ytdl = await import('@distube/ytdl-core');
|
|
309
|
+
const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
310
|
+
const info = await ytdl.default.getInfo(videoUrl);
|
|
311
|
+
logger.debug('MusicService', 'ytdl-core getInfo succeeded', {
|
|
312
|
+
formatCount: info.formats.length,
|
|
362
313
|
});
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
count: audioFormats.length,
|
|
314
|
+
const audioFormats = ytdl.default.filterFormats(info.formats, 'audioonly');
|
|
315
|
+
logger.debug('MusicService', 'ytdl-core audio formats filtered', {
|
|
316
|
+
audioFormatCount: audioFormats.length,
|
|
367
317
|
});
|
|
368
318
|
if (audioFormats.length > 0) {
|
|
369
|
-
//
|
|
370
|
-
const
|
|
371
|
-
const aBitrate = Number.parseInt(String(a.
|
|
372
|
-
const bBitrate = Number.parseInt(String(b.
|
|
319
|
+
// Get highest quality audio
|
|
320
|
+
const bestAudio = audioFormats.sort((a, b) => {
|
|
321
|
+
const aBitrate = Number.parseInt(String(a.audioBitrate || 0));
|
|
322
|
+
const bBitrate = Number.parseInt(String(b.audioBitrate || 0));
|
|
373
323
|
return bBitrate - aBitrate;
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
urlLength: bestAudio.url.length,
|
|
383
|
-
});
|
|
384
|
-
return bestAudio.url;
|
|
385
|
-
}
|
|
386
|
-
// Check for signatureCipher (needs decoding)
|
|
387
|
-
if (bestAudio.signature_cipher || bestAudio.signatureCipher) {
|
|
388
|
-
logger.warn('MusicService', 'Format has signature cipher (not supported)', {
|
|
389
|
-
hasCipher: true,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
324
|
+
})[0];
|
|
325
|
+
if (bestAudio?.url) {
|
|
326
|
+
logger.info('MusicService', 'Using ytdl-core stream', {
|
|
327
|
+
bitrate: bestAudio.audioBitrate,
|
|
328
|
+
urlLength: bestAudio.url.length,
|
|
329
|
+
mimeType: bestAudio.mimeType,
|
|
330
|
+
});
|
|
331
|
+
return bestAudio.url;
|
|
392
332
|
}
|
|
393
333
|
}
|
|
334
|
+
logger.warn('MusicService', 'ytdl-core: No audio formats with URL found');
|
|
394
335
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
catch (error) {
|
|
398
|
-
if (error instanceof Error && error.message.includes('ParsingError')) {
|
|
399
|
-
logger.warn('MusicService', 'youtubei.js parsing error (expected)', {
|
|
400
|
-
error: error.message,
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
logger.error('MusicService', 'youtubei.js extraction failed', {
|
|
336
|
+
catch (error) {
|
|
337
|
+
logger.error('MusicService', 'ytdl-core extraction failed', {
|
|
405
338
|
error: error instanceof Error ? error.message : String(error),
|
|
406
339
|
stack: error instanceof Error ? error.stack : undefined,
|
|
407
340
|
});
|
|
408
341
|
}
|
|
409
342
|
}
|
|
410
|
-
// Try Method
|
|
343
|
+
// Try Method 2: youtube-ext (lightweight, no parser path)
|
|
411
344
|
try {
|
|
412
345
|
logger.debug('MusicService', 'Attempting youtube-ext extraction', {
|
|
413
346
|
videoId,
|
|
@@ -445,7 +378,7 @@ class MusicService {
|
|
|
445
378
|
stack: error instanceof Error ? error.stack : undefined,
|
|
446
379
|
});
|
|
447
380
|
}
|
|
448
|
-
// Try Method
|
|
381
|
+
// Try Method 3: Invidious API (last resort)
|
|
449
382
|
try {
|
|
450
383
|
logger.debug('MusicService', 'Attempting Invidious extraction', {
|
|
451
384
|
videoId,
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@involvex/youtube-music-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "- A Commandline music player for youtube-music",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -47,20 +47,12 @@
|
|
|
47
47
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern dist",
|
|
48
48
|
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix --ignore-pattern dist",
|
|
49
49
|
"start": "bun run dist/source/cli.js",
|
|
50
|
-
"test": "
|
|
50
|
+
"test": "bun run build && ava",
|
|
51
51
|
"typecheck": "tsc --noEmit",
|
|
52
52
|
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
|
53
53
|
"clean": "rimraf dist",
|
|
54
54
|
"release": "powershell -File scripts/release.ps1"
|
|
55
55
|
},
|
|
56
|
-
"xo": {
|
|
57
|
-
"react": true,
|
|
58
|
-
"prettier": true,
|
|
59
|
-
"rules": {
|
|
60
|
-
"react/prop-types": "off"
|
|
61
|
-
},
|
|
62
|
-
"semicolon": true
|
|
63
|
-
},
|
|
64
56
|
"prettier": "@vdemedes/prettier-config",
|
|
65
57
|
"ava": {
|
|
66
58
|
"files": [
|
|
@@ -93,7 +85,6 @@
|
|
|
93
85
|
"chalk": "^5.6.2",
|
|
94
86
|
"conventional-changelog-cli": "^5.0.0",
|
|
95
87
|
"eslint": "^10.0.0",
|
|
96
|
-
"eslint-config-xo-react": "^0.29.0",
|
|
97
88
|
"eslint-plugin-react": "^7.37.5",
|
|
98
89
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
99
90
|
"globals": "^17.3.0",
|
|
@@ -106,8 +97,7 @@
|
|
|
106
97
|
"rimraf": "^6.1.3",
|
|
107
98
|
"ts-node": "^10.9.2",
|
|
108
99
|
"typescript": "^5.9.3",
|
|
109
|
-
"typescript-eslint": "^8.56.0"
|
|
110
|
-
"xo": "^1.2.3"
|
|
100
|
+
"typescript-eslint": "^8.56.0"
|
|
111
101
|
},
|
|
112
102
|
"engines": {
|
|
113
103
|
"node": ">=16"
|