@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,62 @@
|
|
|
1
|
+
import YTMusic from 'ytmusic-api';
|
|
2
|
+
|
|
3
|
+
const ytmusic = new YTMusic();
|
|
4
|
+
const initPromise = ytmusic.initialize();
|
|
5
|
+
|
|
6
|
+
export async function searchSongs(query) {
|
|
7
|
+
await initPromise;
|
|
8
|
+
try {
|
|
9
|
+
const results = await ytmusic.searchSongs(query);
|
|
10
|
+
return results.slice(0, 20).map(result => ({
|
|
11
|
+
videoId: result.videoId,
|
|
12
|
+
title: result.name,
|
|
13
|
+
artist: result.artist?.name || 'Unknown Artist',
|
|
14
|
+
album: result.album?.name || null,
|
|
15
|
+
duration: result.duration,
|
|
16
|
+
thumbnail: result.thumbnails?.[result.thumbnails.length - 1]?.url || null
|
|
17
|
+
}));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('ytmusicService search error:', error);
|
|
20
|
+
throw new Error('Failed to search songs');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getPlaylist(playlistId) {
|
|
25
|
+
await initPromise;
|
|
26
|
+
try {
|
|
27
|
+
const playlist = await ytmusic.getPlaylist(playlistId);
|
|
28
|
+
if (!playlist) throw new Error('Playlist not found');
|
|
29
|
+
|
|
30
|
+
// In ytmusic-api, getPlaylist gets the metadata but NOT the tracks/videos.
|
|
31
|
+
// We must call getPlaylistVideos to retrieve the tracklist.
|
|
32
|
+
const videos = await ytmusic.getPlaylistVideos(playlistId);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
title: playlist.name || 'Unknown Playlist',
|
|
36
|
+
trackCount: videos.length || playlist.trackCount || 0,
|
|
37
|
+
tracks: videos.map(result => ({
|
|
38
|
+
videoId: result.videoId,
|
|
39
|
+
title: result.name,
|
|
40
|
+
artist: result.artist?.name || 'Unknown Artist',
|
|
41
|
+
album: null, // Playlists usually don't return album names in basic response
|
|
42
|
+
duration: result.duration,
|
|
43
|
+
thumbnail: result.thumbnails?.[result.thumbnails.length - 1]?.url || null
|
|
44
|
+
}))
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('ytmusicService getPlaylist error:', error);
|
|
48
|
+
throw new Error('Failed to fetch playlist');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getLyricsText(videoId) {
|
|
53
|
+
await initPromise;
|
|
54
|
+
try {
|
|
55
|
+
const lines = await ytmusic.getLyrics(videoId);
|
|
56
|
+
if (!lines || lines.length === 0) return null;
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn(`[ytmusicService] Lyrics not available for video ${videoId}:`, error.message);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import https from 'https';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import ffmpeg from 'ffmpeg-static';
|
|
6
|
+
import { getDataDir } from './paths.js';
|
|
7
|
+
|
|
8
|
+
const BIN_DIR = path.join(getDataDir(), 'bin');
|
|
9
|
+
let YTDLP_PATH = 'yt-dlp';
|
|
10
|
+
let FFMPEG_PATH = ffmpeg || 'ffmpeg';
|
|
11
|
+
|
|
12
|
+
const status = {
|
|
13
|
+
status: 'idle', // 'idle' | 'checking' | 'downloading' | 'ready' | 'failed'
|
|
14
|
+
ytDlp: 'idle', // 'idle' | 'checking' | 'downloading' | 'ready' | 'failed'
|
|
15
|
+
ffmpeg: 'idle', // 'idle' | 'checking' | 'ready' | 'failed'
|
|
16
|
+
error: null
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function downloadFile(url, destPath) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const file = fs.createWriteStream(destPath);
|
|
22
|
+
function get(reqUrl) {
|
|
23
|
+
https.get(reqUrl, (response) => {
|
|
24
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
25
|
+
get(response.headers.location);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (response.statusCode !== 200) {
|
|
29
|
+
reject(new Error(`Failed to download from ${reqUrl}: Status Code ${response.statusCode}`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
response.pipe(file);
|
|
33
|
+
file.on('finish', () => {
|
|
34
|
+
file.close(resolve);
|
|
35
|
+
});
|
|
36
|
+
}).on('error', (err) => {
|
|
37
|
+
fs.unlink(destPath, () => reject(err));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
get(url);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function checkAndSetupDependencies() {
|
|
45
|
+
status.status = 'checking';
|
|
46
|
+
status.ytDlp = 'checking';
|
|
47
|
+
status.ffmpeg = 'checking';
|
|
48
|
+
console.log('[System Check] Commencing system dependency check...');
|
|
49
|
+
|
|
50
|
+
// 1. FFmpeg Verification
|
|
51
|
+
try {
|
|
52
|
+
if (ffmpeg) {
|
|
53
|
+
console.log(`[System Check] FFmpeg resolved via ffmpeg-static at: ${ffmpeg}`);
|
|
54
|
+
FFMPEG_PATH = ffmpeg;
|
|
55
|
+
status.ffmpeg = 'ready';
|
|
56
|
+
} else {
|
|
57
|
+
execSync('ffmpeg -version', { stdio: 'ignore' });
|
|
58
|
+
console.log('[System Check] FFmpeg resolved via global installation.');
|
|
59
|
+
FFMPEG_PATH = 'ffmpeg';
|
|
60
|
+
status.ffmpeg = 'ready';
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('[System Check] FFmpeg check failed globally.', err);
|
|
64
|
+
status.ffmpeg = 'failed';
|
|
65
|
+
status.error = 'FFmpeg is not installed on this system.';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. yt-dlp Verification
|
|
69
|
+
try {
|
|
70
|
+
let resolvedGlobal = false;
|
|
71
|
+
if (process.platform === 'darwin') {
|
|
72
|
+
const commonPaths = ['/opt/homebrew/bin/yt-dlp', '/usr/local/bin/yt-dlp', '/usr/bin/yt-dlp'];
|
|
73
|
+
for (const p of commonPaths) {
|
|
74
|
+
if (fs.existsSync(p)) {
|
|
75
|
+
try {
|
|
76
|
+
execSync(`"${p}" --version`, { stdio: 'ignore' });
|
|
77
|
+
console.log(`[System Check] yt-dlp resolved at common macOS path: ${p}`);
|
|
78
|
+
YTDLP_PATH = p;
|
|
79
|
+
status.ytDlp = 'ready';
|
|
80
|
+
resolvedGlobal = true;
|
|
81
|
+
break;
|
|
82
|
+
} catch (e) {}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!resolvedGlobal) {
|
|
88
|
+
execSync('yt-dlp --version', { stdio: 'ignore' });
|
|
89
|
+
console.log('[System Check] yt-dlp resolved via global installation.');
|
|
90
|
+
YTDLP_PATH = 'yt-dlp';
|
|
91
|
+
status.ytDlp = 'ready';
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.log('[System Check] yt-dlp not found globally. Checking local bin/ folder...');
|
|
95
|
+
|
|
96
|
+
// Ensure bin folder exists
|
|
97
|
+
if (!fs.existsSync(BIN_DIR)) {
|
|
98
|
+
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const binaryName = process.platform === 'win32' ? 'yt-dlp.exe' : 'yt-dlp';
|
|
102
|
+
const localPath = path.join(BIN_DIR, binaryName);
|
|
103
|
+
|
|
104
|
+
if (fs.existsSync(localPath)) {
|
|
105
|
+
try {
|
|
106
|
+
if (process.platform !== 'win32') {
|
|
107
|
+
fs.chmodSync(localPath, '755');
|
|
108
|
+
if (process.platform === 'darwin') {
|
|
109
|
+
try { execSync(`xattr -d com.apple.quarantine "${localPath}"`, { stdio: 'ignore' }); } catch (e) {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
execSync(`"${localPath}" --version`, { stdio: 'ignore' });
|
|
113
|
+
console.log(`[System Check] Local yt-dlp found at: ${localPath}`);
|
|
114
|
+
YTDLP_PATH = localPath;
|
|
115
|
+
status.ytDlp = 'ready';
|
|
116
|
+
} catch (binErr) {
|
|
117
|
+
console.warn('[System Check] Local yt-dlp check failed, starting re-download.', binErr);
|
|
118
|
+
try { fs.unlinkSync(localPath); } catch (e) {}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Download if not resolved
|
|
123
|
+
if (status.ytDlp !== 'ready') {
|
|
124
|
+
try {
|
|
125
|
+
status.status = 'downloading';
|
|
126
|
+
status.ytDlp = 'downloading';
|
|
127
|
+
console.log('[System Check] Downloading yt-dlp from GitHub...');
|
|
128
|
+
|
|
129
|
+
let downloadUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
|
|
130
|
+
if (process.platform === 'win32') {
|
|
131
|
+
downloadUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe';
|
|
132
|
+
} else if (process.platform === 'darwin') {
|
|
133
|
+
// Use the shebang-based Python script instead of yt-dlp_macos to avoid PyInstaller 30s decompression delay on macOS
|
|
134
|
+
downloadUrl = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await downloadFile(downloadUrl, localPath);
|
|
138
|
+
|
|
139
|
+
if (process.platform !== 'win32') {
|
|
140
|
+
fs.chmodSync(localPath, '755');
|
|
141
|
+
if (process.platform === 'darwin') {
|
|
142
|
+
try { execSync(`xattr -d com.apple.quarantine "${localPath}"`, { stdio: 'ignore' }); } catch (e) {}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Final verification of downloaded binary
|
|
147
|
+
execSync(`"${localPath}" --version`, { stdio: 'ignore' });
|
|
148
|
+
console.log(`[System Check] yt-dlp successfully downloaded and verified at: ${localPath}`);
|
|
149
|
+
YTDLP_PATH = localPath;
|
|
150
|
+
status.ytDlp = 'ready';
|
|
151
|
+
} catch (dlErr) {
|
|
152
|
+
console.error('[System Check] Failed to download or verify local yt-dlp:', dlErr);
|
|
153
|
+
status.ytDlp = 'failed';
|
|
154
|
+
status.error = `yt-dlp is missing and auto-download failed: ${dlErr.message}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Final Overall Status
|
|
160
|
+
if (status.ffmpeg === 'ready' && status.ytDlp === 'ready') {
|
|
161
|
+
status.status = 'ready';
|
|
162
|
+
status.error = null;
|
|
163
|
+
console.log('[System Check] Setup successful! All dependencies resolved.');
|
|
164
|
+
} else {
|
|
165
|
+
status.status = 'failed';
|
|
166
|
+
console.error('[System Check] Setup failed. Dependencies are incomplete.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function getDependencyStatus() {
|
|
171
|
+
return status;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { YTDLP_PATH, FFMPEG_PATH };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function sanitizeFilename(name) {
|
|
2
|
+
if (!name) return 'Unknown';
|
|
3
|
+
// Replace illegal filename characters with underscores
|
|
4
|
+
const sanitized = name.replace(/[\\/:*?"<>|]/g, '_');
|
|
5
|
+
// Trim whitespace
|
|
6
|
+
const trimmed = sanitized.trim();
|
|
7
|
+
// Limit to 200 chars
|
|
8
|
+
return trimmed.substring(0, 200);
|
|
9
|
+
}
|