@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,63 @@
|
|
|
1
|
+
import { useSearch } from '../hooks/useSearch';
|
|
2
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
3
|
+
import SearchBar from '../components/Search/SearchBar';
|
|
4
|
+
import SearchResults from '../components/Search/SearchResults';
|
|
5
|
+
|
|
6
|
+
export default function SearchPage({ outputDir, onDownload, onFlyAnimation, downloads, onPlay, currentSong, searchQueryPreset, onClearPreset }) {
|
|
7
|
+
const { results, isLoading, error, search } = useSearch();
|
|
8
|
+
const [isDefaultTrending, setIsDefaultTrending] = useState(true);
|
|
9
|
+
|
|
10
|
+
// Load world top trending English songs by default on mount
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
search('Trending English Songs');
|
|
13
|
+
}, [search]);
|
|
14
|
+
|
|
15
|
+
// Sync state if searchQueryPreset is injected
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (searchQueryPreset) {
|
|
18
|
+
setIsDefaultTrending(false);
|
|
19
|
+
}
|
|
20
|
+
}, [searchQueryPreset]);
|
|
21
|
+
|
|
22
|
+
const handleDownload = (song) => {
|
|
23
|
+
onDownload(song, outputDir);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleSearch = useCallback((query) => {
|
|
27
|
+
if (!query.trim()) {
|
|
28
|
+
setIsDefaultTrending(true);
|
|
29
|
+
search('Trending English Songs');
|
|
30
|
+
} else {
|
|
31
|
+
setIsDefaultTrending(false);
|
|
32
|
+
search(query);
|
|
33
|
+
}
|
|
34
|
+
}, [search]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="search-page">
|
|
38
|
+
<SearchBar
|
|
39
|
+
onSearch={handleSearch}
|
|
40
|
+
searchQueryPreset={searchQueryPreset}
|
|
41
|
+
onClearPreset={onClearPreset}
|
|
42
|
+
isLoading={isLoading}
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
{error && (
|
|
46
|
+
<div className="error-message">
|
|
47
|
+
<p>⚠️ {error}</p>
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
<SearchResults
|
|
52
|
+
results={results}
|
|
53
|
+
isLoading={isLoading}
|
|
54
|
+
onDownload={handleDownload}
|
|
55
|
+
onFlyAnimation={onFlyAnimation}
|
|
56
|
+
downloads={downloads}
|
|
57
|
+
onPlay={onPlay}
|
|
58
|
+
currentSong={currentSong}
|
|
59
|
+
isDefaultTrending={isDefaultTrending}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2023",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"lib": ["ES2023", "DOM"],
|
|
6
|
+
"types": ["vite/client"],
|
|
7
|
+
"allowArbitraryExtensions": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"erasableSyntaxOnly": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"]
|
|
24
|
+
}
|
package/main.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { app, BrowserWindow } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { fork } = require('child_process');
|
|
4
|
+
|
|
5
|
+
let serverProcess = null;
|
|
6
|
+
let mainWindow = null;
|
|
7
|
+
|
|
8
|
+
function startBackend() {
|
|
9
|
+
const serverPath = path.join(__dirname, 'server/index.js');
|
|
10
|
+
|
|
11
|
+
// Fork the Express backend process
|
|
12
|
+
serverProcess = fork(serverPath, [], {
|
|
13
|
+
env: {
|
|
14
|
+
...process.env,
|
|
15
|
+
NODE_ENV: 'production',
|
|
16
|
+
PORT: '3001'
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
serverProcess.on('exit', (code, signal) => {
|
|
21
|
+
console.log(`Backend process exited with code ${code} and signal ${signal}`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
serverProcess.on('error', (err) => {
|
|
25
|
+
console.error('Backend process error:', err);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createWindow() {
|
|
30
|
+
mainWindow = new BrowserWindow({
|
|
31
|
+
width: 1200,
|
|
32
|
+
height: 800,
|
|
33
|
+
title: 'YTM',
|
|
34
|
+
webPreferences: {
|
|
35
|
+
nodeIntegration: false,
|
|
36
|
+
contextIsolation: true
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const isDev = !app.isPackaged;
|
|
41
|
+
|
|
42
|
+
const loadURL = () => {
|
|
43
|
+
const url = isDev ? 'http://127.0.0.1:5173' : 'http://127.0.0.1:3001';
|
|
44
|
+
mainWindow.loadURL(url).catch((err) => {
|
|
45
|
+
console.log(`Frontend port not ready yet, retrying in 200ms...`);
|
|
46
|
+
setTimeout(loadURL, 200);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
loadURL();
|
|
51
|
+
|
|
52
|
+
mainWindow.on('closed', () => {
|
|
53
|
+
mainWindow = null;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
app.whenReady().then(() => {
|
|
58
|
+
startBackend();
|
|
59
|
+
createWindow();
|
|
60
|
+
|
|
61
|
+
app.on('activate', () => {
|
|
62
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
63
|
+
createWindow();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
app.on('window-all-closed', () => {
|
|
69
|
+
if (process.platform !== 'darwin') {
|
|
70
|
+
app.quit();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
app.on('will-quit', () => {
|
|
75
|
+
if (serverProcess) {
|
|
76
|
+
console.log('[Electron] Killing backend child process...');
|
|
77
|
+
serverProcess.kill('SIGTERM');
|
|
78
|
+
}
|
|
79
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@supasayan/ytm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Root package to run both client and server",
|
|
5
|
+
"main": "main.js",
|
|
6
|
+
"productName": "YTM",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ytm": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "npx concurrently \"npm run start --prefix server\" \"npm run dev --prefix client\"",
|
|
12
|
+
"dev": "npx concurrently \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
|
|
13
|
+
"install-all": "npm install --prefix server && npm install --prefix client",
|
|
14
|
+
"app:dev": "npx concurrently \"npm run dev --prefix server\" \"npm run dev --prefix client\" \"npx wait-on http://localhost:5173 && npx wait-on http://localhost:3001 && npx electron .\"",
|
|
15
|
+
"app:build": "npm run build --prefix client && npx electron-builder --mac"
|
|
16
|
+
},
|
|
17
|
+
"build": {
|
|
18
|
+
"appId": "com.sayan.ytm",
|
|
19
|
+
"productName": "YTM",
|
|
20
|
+
"directories": {
|
|
21
|
+
"output": "dist-app"
|
|
22
|
+
},
|
|
23
|
+
"mac": {
|
|
24
|
+
"target": [
|
|
25
|
+
"dmg"
|
|
26
|
+
],
|
|
27
|
+
"category": "public.app-category.music"
|
|
28
|
+
},
|
|
29
|
+
"dmg": {
|
|
30
|
+
"title": "Install YTM"
|
|
31
|
+
},
|
|
32
|
+
"asar": true,
|
|
33
|
+
"asarUnpack": [
|
|
34
|
+
"**/node_modules/ffmpeg-static/**/*"
|
|
35
|
+
],
|
|
36
|
+
"files": [
|
|
37
|
+
"main.js",
|
|
38
|
+
"server/**/*",
|
|
39
|
+
"client/dist/**/*",
|
|
40
|
+
"package.json"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"electron": "^43.1.0",
|
|
45
|
+
"electron-builder": "^26.15.3",
|
|
46
|
+
"wait-on": "^9.0.10"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/server/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
import searchRoutes from './routes/search.js';
|
|
8
|
+
import playlistRoutes from './routes/playlist.js';
|
|
9
|
+
import downloadRoutes from './routes/download.js';
|
|
10
|
+
import browseRoutes from './routes/browse.js';
|
|
11
|
+
import streamRoutes from './routes/stream.js';
|
|
12
|
+
import playLocalRoutes from './routes/playLocal.js';
|
|
13
|
+
import { checkAndSetupDependencies, getDependencyStatus } from './utils/dependencyChecker.js';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const app = express();
|
|
19
|
+
const PORT = process.env.PORT || 3001;
|
|
20
|
+
|
|
21
|
+
// Middleware
|
|
22
|
+
app.use(cors());
|
|
23
|
+
app.use(express.json());
|
|
24
|
+
|
|
25
|
+
// Serve static assets from the client build folder in production
|
|
26
|
+
const clientDistPath = path.join(__dirname, '../client/dist');
|
|
27
|
+
app.use(express.static(clientDistPath));
|
|
28
|
+
|
|
29
|
+
// Routes
|
|
30
|
+
app.use('/api/search', searchRoutes);
|
|
31
|
+
app.use('/api/playlist', playlistRoutes);
|
|
32
|
+
app.use('/api/download', downloadRoutes);
|
|
33
|
+
app.use('/api/browse', browseRoutes);
|
|
34
|
+
app.use('/api/stream', streamRoutes);
|
|
35
|
+
app.use('/api/play-local', playLocalRoutes);
|
|
36
|
+
|
|
37
|
+
// System Check Endpoint
|
|
38
|
+
app.get('/api/system-check', (req, res) => {
|
|
39
|
+
res.json({
|
|
40
|
+
...getDependencyStatus(),
|
|
41
|
+
defaultOutputDir: path.join(os.homedir(), 'Downloads')
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Fallback for Single Page App routing (redirect all non-api routes to index.html)
|
|
46
|
+
app.get('*', (req, res) => {
|
|
47
|
+
res.sendFile(path.join(clientDistPath, 'index.html'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
app.listen(PORT, async () => {
|
|
51
|
+
console.log(`YTMDownloader Backend running on http://127.0.0.1:${PORT}`);
|
|
52
|
+
// Run async setup check
|
|
53
|
+
await checkAndSetupDependencies();
|
|
54
|
+
});
|