goofyy-don 0.1.3
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/dist/app.d.ts +6 -0
- package/dist/app.js +144 -0
- package/dist/baseUrl.d.ts +1 -0
- package/dist/baseUrl.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +19 -0
- package/dist/components/BlinkingCursor.d.ts +2 -0
- package/dist/components/BlinkingCursor.js +10 -0
- package/dist/components/InstallInstructions.d.ts +7 -0
- package/dist/components/InstallInstructions.js +41 -0
- package/dist/components/Menu.d.ts +11 -0
- package/dist/components/Menu.js +8 -0
- package/dist/components/PlayPauseButton.d.ts +7 -0
- package/dist/components/PlayPauseButton.js +14 -0
- package/dist/components/ProgressBar.d.ts +3 -0
- package/dist/components/ProgressBar.js +18 -0
- package/dist/screens/About.d.ts +3 -0
- package/dist/screens/About.js +8 -0
- package/dist/screens/Discord.d.ts +3 -0
- package/dist/screens/Discord.js +7 -0
- package/dist/screens/MusicPlayer.d.ts +8 -0
- package/dist/screens/MusicPlayer.js +28 -0
- package/dist/screens/Playlists.d.ts +3 -0
- package/dist/screens/Playlists.js +8 -0
- package/dist/screens/StarGithub.d.ts +3 -0
- package/dist/screens/StarGithub.js +7 -0
- package/dist/screens/TrendingSongs.d.ts +3 -0
- package/dist/screens/TrendingSongs.js +8 -0
- package/dist/services/musicPlayer.d.ts +24 -0
- package/dist/services/musicPlayer.js +217 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
- package/readme.md +69 -0
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
2
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
3
|
+
import { MusicPlayerService } from './services/musicPlayer.js';
|
4
|
+
import { Menu } from './components/Menu.js';
|
5
|
+
import { MusicPlayer } from './screens/MusicPlayer.js';
|
6
|
+
import Playlists from './screens/Playlists.js';
|
7
|
+
import TrendingSongs from './screens/TrendingSongs.js';
|
8
|
+
import About from './screens/About.js';
|
9
|
+
import Discord from './screens/Discord.js';
|
10
|
+
import StarGithub from './screens/StarGithub.js';
|
11
|
+
export default function App({ initialQuery }) {
|
12
|
+
const [state, setState] = useState({
|
13
|
+
isPlaying: false,
|
14
|
+
isPaused: false,
|
15
|
+
currentSong: null,
|
16
|
+
error: null,
|
17
|
+
isSearching: false,
|
18
|
+
progress: {
|
19
|
+
elapsed: 0,
|
20
|
+
total: 0
|
21
|
+
}
|
22
|
+
});
|
23
|
+
const [input, setInput] = useState(initialQuery || '');
|
24
|
+
const { exit } = useApp();
|
25
|
+
const musicPlayerRef = useRef(new MusicPlayerService());
|
26
|
+
const musicPlayer = musicPlayerRef.current;
|
27
|
+
const items = [
|
28
|
+
{ label: 'Music Player', screen: 'music-player' },
|
29
|
+
{ label: 'Playlists', screen: 'playlists' },
|
30
|
+
{ label: 'Trending Songs', screen: 'trending-songs' },
|
31
|
+
{ label: 'About Goofyy', screen: 'about-goofyy' },
|
32
|
+
{ label: 'Discord', screen: 'discord' },
|
33
|
+
{ label: 'Star on Github', screen: 'github' },
|
34
|
+
];
|
35
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
36
|
+
useEffect(() => {
|
37
|
+
if (initialQuery) {
|
38
|
+
handleSearch(initialQuery);
|
39
|
+
}
|
40
|
+
}, []);
|
41
|
+
const handleSearch = async (query) => {
|
42
|
+
if (!query.trim())
|
43
|
+
return;
|
44
|
+
setState((prev) => ({ ...prev, isSearching: true, error: null }));
|
45
|
+
try {
|
46
|
+
// Start both requests in parallel
|
47
|
+
const metadataPromise = musicPlayer.fetchMetadata(query);
|
48
|
+
const streamPromise = Promise.resolve(musicPlayer.getStream(query));
|
49
|
+
// Wait for metadata first to update UI
|
50
|
+
const songInfo = await metadataPromise;
|
51
|
+
const totalDuration = parseDuration(songInfo.duration);
|
52
|
+
setState((prev) => ({
|
53
|
+
...prev,
|
54
|
+
currentSong: songInfo,
|
55
|
+
isSearching: false,
|
56
|
+
progress: {
|
57
|
+
elapsed: 0,
|
58
|
+
total: totalDuration
|
59
|
+
}
|
60
|
+
}));
|
61
|
+
musicPlayer.setProgressCallback((elapsed) => {
|
62
|
+
setState((prev) => ({
|
63
|
+
...prev,
|
64
|
+
progress: {
|
65
|
+
...prev.progress,
|
66
|
+
elapsed
|
67
|
+
}
|
68
|
+
}));
|
69
|
+
});
|
70
|
+
// Wait for stream to be ready, then play
|
71
|
+
const stream = await streamPromise;
|
72
|
+
setState((prev) => ({ ...prev, isPlaying: true }));
|
73
|
+
await musicPlayer.playStream(songInfo, stream);
|
74
|
+
}
|
75
|
+
catch (error) {
|
76
|
+
setState((prev) => ({
|
77
|
+
...prev,
|
78
|
+
error: error instanceof Error ? error.message : 'An error occurred',
|
79
|
+
isSearching: false
|
80
|
+
}));
|
81
|
+
}
|
82
|
+
};
|
83
|
+
const parseDuration = (duration) => {
|
84
|
+
const parts = duration.split(':');
|
85
|
+
if (parts.length === 2) {
|
86
|
+
return parseInt(parts[0] || '0') * 60 + parseInt(parts[1] || '0');
|
87
|
+
}
|
88
|
+
else if (parts.length === 3) {
|
89
|
+
return parseInt(parts[0] || '0') * 3600 + parseInt(parts[1] || '0') * 60 + parseInt(parts[2] || '0');
|
90
|
+
}
|
91
|
+
return 0;
|
92
|
+
};
|
93
|
+
useInput((input2, key) => {
|
94
|
+
// Handle ESC key first - should always work
|
95
|
+
if (key.escape) {
|
96
|
+
musicPlayer.cleanup();
|
97
|
+
exit();
|
98
|
+
return;
|
99
|
+
}
|
100
|
+
// Handle other keys only if not searching
|
101
|
+
if (state.isSearching) {
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
// Spacebar toggles pause/play
|
105
|
+
if (input2 === ' ' && state.isPlaying) {
|
106
|
+
if (!state.isPaused) {
|
107
|
+
musicPlayer.pause();
|
108
|
+
setState(prev => ({ ...prev, isPaused: true }));
|
109
|
+
}
|
110
|
+
else {
|
111
|
+
musicPlayer.resume();
|
112
|
+
setState(prev => ({ ...prev, isPaused: false }));
|
113
|
+
}
|
114
|
+
return;
|
115
|
+
}
|
116
|
+
if (key.return && !state.isPlaying) {
|
117
|
+
handleSearch(input);
|
118
|
+
}
|
119
|
+
else if (input2.length > 0) {
|
120
|
+
setInput(r => r + input2);
|
121
|
+
}
|
122
|
+
else if (key.backspace || key.delete) {
|
123
|
+
setInput(r => r.slice(0, -1));
|
124
|
+
}
|
125
|
+
if (key.leftArrow && selectedIndex > 0) {
|
126
|
+
setSelectedIndex(r => r - 1);
|
127
|
+
}
|
128
|
+
else if (key.rightArrow && selectedIndex < items.length - 1) {
|
129
|
+
setSelectedIndex(r => r + 1);
|
130
|
+
}
|
131
|
+
});
|
132
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
133
|
+
React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
|
134
|
+
React.createElement(Text, null, "\uD83C\uDFB5 Goofyy Music Player"),
|
135
|
+
React.createElement(Text, { color: "gray" }, "Navigate the menu using the left (\u2190) and right (\u2192) arrow keys")),
|
136
|
+
React.createElement(Menu, { items: items, selectedIndex: selectedIndex }),
|
137
|
+
selectedIndex >= 0 && selectedIndex < items.length && items[selectedIndex]?.screen === 'music-player' && (React.createElement(MusicPlayer, { state: state, input: input })),
|
138
|
+
state.isPlaying && (React.createElement(Text, { color: state.isPaused ? 'yellow' : 'green' }, state.isPaused ? 'Paused (press space to resume)' : 'Playing (press space to pause)')),
|
139
|
+
selectedIndex >= 0 && selectedIndex < items.length && items[selectedIndex]?.screen === 'playlists' && (React.createElement(Playlists, null)),
|
140
|
+
selectedIndex >= 0 && selectedIndex < items.length && items[selectedIndex]?.screen === 'trending-songs' && (React.createElement(TrendingSongs, null)),
|
141
|
+
selectedIndex >= 0 && selectedIndex < items.length && items[selectedIndex]?.screen === 'about-goofyy' && (React.createElement(About, null)),
|
142
|
+
selectedIndex >= 0 && selectedIndex < items.length && items[selectedIndex]?.screen === 'discord' && (React.createElement(Discord, null)),
|
143
|
+
selectedIndex >= 0 && selectedIndex < items.length && items[selectedIndex]?.screen === 'github' && (React.createElement(StarGithub, null))));
|
144
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const baseUrl = "https://goofyy.himanshu-saini.com";
|
package/dist/baseUrl.js
ADDED
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
import React from 'react';
|
3
|
+
import { render } from 'ink';
|
4
|
+
import meow from 'meow';
|
5
|
+
import App from './app.js';
|
6
|
+
const cli = meow(`
|
7
|
+
Usage
|
8
|
+
$ goofyy [song name]
|
9
|
+
|
10
|
+
Examples
|
11
|
+
$ goofyy "shape of you"
|
12
|
+
$ goofyy "ed sheeran perfect"
|
13
|
+
$ goofyy "bohemian rhapsody queen"
|
14
|
+
|
15
|
+
Press Ctrl + C to exit
|
16
|
+
`, {
|
17
|
+
importMeta: import.meta,
|
18
|
+
});
|
19
|
+
render(React.createElement(App, { initialQuery: cli.input[0] }));
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
2
|
+
import { Text } from 'ink';
|
3
|
+
export const BlinkingCursor = () => {
|
4
|
+
const [visible, setVisible] = useState(true);
|
5
|
+
useEffect(() => {
|
6
|
+
const timer = setInterval(() => setVisible(v => !v), 500);
|
7
|
+
return () => clearInterval(timer);
|
8
|
+
}, []);
|
9
|
+
return React.createElement(Text, { color: "green" }, visible ? '█' : ' ');
|
10
|
+
};
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Box, Text } from 'ink';
|
3
|
+
import os from 'os';
|
4
|
+
export const InstallInstructions = ({ missing, query }) => {
|
5
|
+
const platform = os.platform();
|
6
|
+
const command = query ? `goofyy "${query}"` : 'goofyy';
|
7
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
8
|
+
React.createElement(Text, { color: "red" }, "\u274C Missing dependencies detected!"),
|
9
|
+
React.createElement(Text, null, " "),
|
10
|
+
platform === 'darwin' && (React.createElement(React.Fragment, null,
|
11
|
+
React.createElement(Text, null, "\uD83C\uDF7A Please install the missing dependencies using Homebrew:"),
|
12
|
+
React.createElement(Text, null, " "),
|
13
|
+
React.createElement(Text, null,
|
14
|
+
" brew install ",
|
15
|
+
missing.join(' ')),
|
16
|
+
React.createElement(Text, null, " "))),
|
17
|
+
platform === 'linux' && (React.createElement(React.Fragment, null,
|
18
|
+
React.createElement(Text, null, "\uD83D\uDC27 Please install the missing dependencies:"),
|
19
|
+
React.createElement(Text, null, " "),
|
20
|
+
React.createElement(Text, null, " # Ubuntu/Debian:"),
|
21
|
+
React.createElement(Text, null, " sudo apt update"),
|
22
|
+
missing.includes('yt-dlp') && (React.createElement(Text, null, " sudo apt install python3-pip && pip3 install yt-dlp")),
|
23
|
+
missing.includes('ffmpeg') && (React.createElement(Text, null, " sudo apt install ffmpeg")),
|
24
|
+
React.createElement(Text, null, " "),
|
25
|
+
React.createElement(Text, null, " # Or using Homebrew on Linux:"),
|
26
|
+
React.createElement(Text, null,
|
27
|
+
" brew install ",
|
28
|
+
missing.join(' ')),
|
29
|
+
React.createElement(Text, null, " "))),
|
30
|
+
platform !== 'darwin' && platform !== 'linux' && (React.createElement(React.Fragment, null,
|
31
|
+
React.createElement(Text, null, "\uD83D\uDCBB Please install the missing dependencies:"),
|
32
|
+
React.createElement(Text, null, " "),
|
33
|
+
React.createElement(Text, null, " yt-dlp: https://github.com/yt-dlp/yt-dlp#installation"),
|
34
|
+
React.createElement(Text, null, " ffmpeg: https://ffmpeg.org/download.html"),
|
35
|
+
React.createElement(Text, null, " "))),
|
36
|
+
React.createElement(Text, null, "Then run the command again:"),
|
37
|
+
React.createElement(Text, null,
|
38
|
+
" ",
|
39
|
+
command),
|
40
|
+
React.createElement(Text, null, " ")));
|
41
|
+
};
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
interface MenuItem {
|
3
|
+
label: string;
|
4
|
+
screen: string;
|
5
|
+
}
|
6
|
+
interface MenuProps {
|
7
|
+
items: MenuItem[];
|
8
|
+
selectedIndex: number;
|
9
|
+
}
|
10
|
+
export declare function Menu({ items, selectedIndex }: MenuProps): React.JSX.Element;
|
11
|
+
export {};
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import { Box, Text } from 'ink';
|
2
|
+
import React from 'react';
|
3
|
+
export function Menu({ items, selectedIndex }) {
|
4
|
+
return (React.createElement(Box, { flexDirection: "row", marginBottom: 1 },
|
5
|
+
React.createElement(Box, null, items.map((item, index) => (React.createElement(Text, { key: item.label, color: index === selectedIndex ? "blue" : "white" },
|
6
|
+
index === selectedIndex ? ' > ' : ' ',
|
7
|
+
item.label))))));
|
8
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Box, Text } from 'ink';
|
3
|
+
export const PlayPauseButton = ({ isPlaying, isPaused }) => {
|
4
|
+
if (!isPlaying) {
|
5
|
+
return (React.createElement(Box, { marginRight: 1 },
|
6
|
+
React.createElement(Text, { color: "gray" }, "\u25B6")));
|
7
|
+
}
|
8
|
+
if (isPaused) {
|
9
|
+
return (React.createElement(Box, { marginRight: 1 },
|
10
|
+
React.createElement(Text, { color: "yellow" }, "\u25B6")));
|
11
|
+
}
|
12
|
+
return (React.createElement(Box, { marginRight: 1 },
|
13
|
+
React.createElement(Text, { color: "green" }, "\u23F8")));
|
14
|
+
};
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Box, Text } from 'ink';
|
3
|
+
const formatTime = (seconds) => {
|
4
|
+
const minutes = Math.floor(seconds / 60);
|
5
|
+
const remainingSeconds = Math.floor(seconds % 60);
|
6
|
+
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
7
|
+
};
|
8
|
+
export const ProgressBar = ({ elapsed, total, width }) => {
|
9
|
+
const progress = Math.min((elapsed / total) * width, width);
|
10
|
+
const progressBar = '█'.repeat(progress) + '░'.repeat(width - progress);
|
11
|
+
return (React.createElement(Box, null,
|
12
|
+
React.createElement(Text, null, formatTime(elapsed)),
|
13
|
+
React.createElement(Text, null,
|
14
|
+
" ",
|
15
|
+
progressBar,
|
16
|
+
" "),
|
17
|
+
React.createElement(Text, null, formatTime(total))));
|
18
|
+
};
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Box, Text } from 'ink';
|
3
|
+
function About() {
|
4
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
5
|
+
React.createElement(Text, { bold: true }, "About Goofyy"),
|
6
|
+
React.createElement(Text, null, "Goofyy is a sleek command-line music player that streams your favorite songs directly in the terminal. With a clean UI, instant search, and fast streaming powered by yt-dlp and ffmpeg, Goofyy makes listening to music simple and fun\u2014right from your terminal. Enjoy real-time progress, easy keyboard controls, and a minimal, distraction-free experience.")));
|
7
|
+
}
|
8
|
+
export default About;
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Box, Text } from 'ink';
|
3
|
+
import { ProgressBar } from '../components/ProgressBar.js';
|
4
|
+
import { BlinkingCursor } from '../components/BlinkingCursor.js';
|
5
|
+
export const MusicPlayer = ({ state, input }) => {
|
6
|
+
if (state.error) {
|
7
|
+
return (React.createElement(Box, null,
|
8
|
+
React.createElement(Text, { color: "red" }, state.error)));
|
9
|
+
}
|
10
|
+
return (React.createElement(React.Fragment, null,
|
11
|
+
!state.currentSong && !state.isSearching && (React.createElement(Box, { marginBottom: 1 },
|
12
|
+
React.createElement(Text, null, "Enter song name to search: "),
|
13
|
+
React.createElement(Text, { color: "green" }, input),
|
14
|
+
React.createElement(BlinkingCursor, null))),
|
15
|
+
state.isSearching && (React.createElement(Box, null,
|
16
|
+
React.createElement(Text, null,
|
17
|
+
"\uD83D\uDD0D Searching for: ",
|
18
|
+
input))),
|
19
|
+
state.currentSong && (React.createElement(Box, { flexDirection: "column" },
|
20
|
+
React.createElement(Box, { marginBottom: 1 },
|
21
|
+
React.createElement(Text, null,
|
22
|
+
"\uD83C\uDFB5 Now playing: ",
|
23
|
+
state.currentSong.title)),
|
24
|
+
React.createElement(Box, { marginBottom: 1 },
|
25
|
+
React.createElement(ProgressBar, { elapsed: state.progress.elapsed, total: state.progress.total, width: 40 })),
|
26
|
+
React.createElement(Text, null, "Join us on discord: https://discord.gg/HNJgYuSUQ3"),
|
27
|
+
React.createElement(Text, null, "Press Ctrl+C to exit")))));
|
28
|
+
};
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Box, Text } from 'ink';
|
3
|
+
function Playlists() {
|
4
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
5
|
+
React.createElement(Text, null, "Playlists Feature Coming Soon!"),
|
6
|
+
React.createElement(Text, null, "Stay tuned for the ability to create and manage your playlists.")));
|
7
|
+
}
|
8
|
+
export default Playlists;
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Box, Text } from 'ink';
|
3
|
+
function TrendingSongs() {
|
4
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
5
|
+
React.createElement(Text, null, "Trending Songs Feature Coming Soon!"),
|
6
|
+
React.createElement(Text, null, "Stay tuned for the ability to see what's trending.")));
|
7
|
+
}
|
8
|
+
export default TrendingSongs;
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { SongInfo } from '../types.js';
|
2
|
+
export declare class MusicPlayerService {
|
3
|
+
private speaker;
|
4
|
+
private onProgressUpdate;
|
5
|
+
private progressInterval;
|
6
|
+
private startTime;
|
7
|
+
private duration;
|
8
|
+
private paused;
|
9
|
+
private currentStream;
|
10
|
+
private pausedElapsed;
|
11
|
+
checkDependencies(): Promise<string[]>;
|
12
|
+
getInstallInstructions(missing: string[]): string;
|
13
|
+
fetchMetadata(query: string): Promise<SongInfo>;
|
14
|
+
getStream(query: string): import("got").Request;
|
15
|
+
setProgressCallback(callback: (elapsed: number) => void): void;
|
16
|
+
private parseDuration;
|
17
|
+
private formatDuration;
|
18
|
+
playStream(songInfo: SongInfo, stream: NodeJS.ReadableStream): Promise<void>;
|
19
|
+
pause(): void;
|
20
|
+
resume(): void;
|
21
|
+
isPaused(): boolean;
|
22
|
+
playSong(songInfo: SongInfo): Promise<void>;
|
23
|
+
cleanup(): void;
|
24
|
+
}
|
@@ -0,0 +1,217 @@
|
|
1
|
+
import got from 'got';
|
2
|
+
import * as portAudio from 'naudiodon2';
|
3
|
+
import { baseUrl } from '../baseUrl.js';
|
4
|
+
export class MusicPlayerService {
|
5
|
+
constructor() {
|
6
|
+
Object.defineProperty(this, "speaker", {
|
7
|
+
enumerable: true,
|
8
|
+
configurable: true,
|
9
|
+
writable: true,
|
10
|
+
value: null
|
11
|
+
});
|
12
|
+
Object.defineProperty(this, "onProgressUpdate", {
|
13
|
+
enumerable: true,
|
14
|
+
configurable: true,
|
15
|
+
writable: true,
|
16
|
+
value: null
|
17
|
+
});
|
18
|
+
Object.defineProperty(this, "progressInterval", {
|
19
|
+
enumerable: true,
|
20
|
+
configurable: true,
|
21
|
+
writable: true,
|
22
|
+
value: null
|
23
|
+
});
|
24
|
+
Object.defineProperty(this, "startTime", {
|
25
|
+
enumerable: true,
|
26
|
+
configurable: true,
|
27
|
+
writable: true,
|
28
|
+
value: 0
|
29
|
+
});
|
30
|
+
// @ts-ignore
|
31
|
+
Object.defineProperty(this, "duration", {
|
32
|
+
enumerable: true,
|
33
|
+
configurable: true,
|
34
|
+
writable: true,
|
35
|
+
value: 0
|
36
|
+
});
|
37
|
+
Object.defineProperty(this, "paused", {
|
38
|
+
enumerable: true,
|
39
|
+
configurable: true,
|
40
|
+
writable: true,
|
41
|
+
value: false
|
42
|
+
});
|
43
|
+
Object.defineProperty(this, "currentStream", {
|
44
|
+
enumerable: true,
|
45
|
+
configurable: true,
|
46
|
+
writable: true,
|
47
|
+
value: null
|
48
|
+
});
|
49
|
+
Object.defineProperty(this, "pausedElapsed", {
|
50
|
+
enumerable: true,
|
51
|
+
configurable: true,
|
52
|
+
writable: true,
|
53
|
+
value: 0
|
54
|
+
});
|
55
|
+
}
|
56
|
+
async checkDependencies() {
|
57
|
+
return [];
|
58
|
+
}
|
59
|
+
// @ts-ignore
|
60
|
+
getInstallInstructions(missing) {
|
61
|
+
return '';
|
62
|
+
}
|
63
|
+
// Fetch metadata only
|
64
|
+
async fetchMetadata(query) {
|
65
|
+
const metadataUrl = `${baseUrl}/metadata?q=${encodeURIComponent(query)}`;
|
66
|
+
const streamUrl = `${baseUrl}/stream?q=${encodeURIComponent(query)}`;
|
67
|
+
const response = await got(metadataUrl, { responseType: 'json' });
|
68
|
+
const data = response.body;
|
69
|
+
return {
|
70
|
+
title: data.title || query,
|
71
|
+
duration: typeof data.duration === 'number' ? this.formatDuration(data.duration) : (data.duration || '0:00'),
|
72
|
+
url: streamUrl
|
73
|
+
};
|
74
|
+
}
|
75
|
+
// Get stream only
|
76
|
+
getStream(query) {
|
77
|
+
const streamUrl = `${baseUrl}/stream?q=${encodeURIComponent(query)}`;
|
78
|
+
return got.stream(streamUrl);
|
79
|
+
}
|
80
|
+
setProgressCallback(callback) {
|
81
|
+
this.onProgressUpdate = callback;
|
82
|
+
}
|
83
|
+
parseDuration(duration) {
|
84
|
+
if (typeof duration === 'number')
|
85
|
+
return duration;
|
86
|
+
const parts = duration.split(':');
|
87
|
+
if (parts.length === 2) {
|
88
|
+
return parseInt(parts[0] || '0') * 60 + parseInt(parts[1] || '0');
|
89
|
+
}
|
90
|
+
else if (parts.length === 3) {
|
91
|
+
return parseInt(parts[0] || '0') * 3600 + parseInt(parts[1] || '0') * 60 + parseInt(parts[2] || '0');
|
92
|
+
}
|
93
|
+
return 0;
|
94
|
+
}
|
95
|
+
formatDuration(seconds) {
|
96
|
+
const m = Math.floor(seconds / 60);
|
97
|
+
const s = Math.floor(seconds % 60);
|
98
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
99
|
+
}
|
100
|
+
// Play using a provided stream and songInfo
|
101
|
+
async playStream(songInfo, stream) {
|
102
|
+
return new Promise((resolve, reject) => {
|
103
|
+
this.startTime = Date.now();
|
104
|
+
this.duration = this.parseDuration(songInfo.duration);
|
105
|
+
this.paused = false;
|
106
|
+
this.currentStream = stream;
|
107
|
+
this.pausedElapsed = 0;
|
108
|
+
// @ts-ignore
|
109
|
+
this.speaker = new portAudio.AudioIO({
|
110
|
+
outOptions: {
|
111
|
+
channelCount: 2,
|
112
|
+
sampleFormat: portAudio.SampleFormat16Bit,
|
113
|
+
sampleRate: 44100,
|
114
|
+
deviceId: -1, // default output device
|
115
|
+
closeOnError: true
|
116
|
+
}
|
117
|
+
});
|
118
|
+
stream.on('error', (err) => {
|
119
|
+
if (this.progressInterval)
|
120
|
+
clearInterval(this.progressInterval);
|
121
|
+
reject(err);
|
122
|
+
});
|
123
|
+
this.speaker.on('close', () => {
|
124
|
+
if (this.progressInterval)
|
125
|
+
clearInterval(this.progressInterval);
|
126
|
+
resolve();
|
127
|
+
});
|
128
|
+
stream.pipe(this.speaker);
|
129
|
+
this.speaker.start();
|
130
|
+
if (this.onProgressUpdate) {
|
131
|
+
this.progressInterval = setInterval(() => {
|
132
|
+
const elapsed = (Date.now() - this.startTime) / 1000;
|
133
|
+
if (this.onProgressUpdate) {
|
134
|
+
this.onProgressUpdate(elapsed);
|
135
|
+
}
|
136
|
+
}, 1000);
|
137
|
+
}
|
138
|
+
});
|
139
|
+
}
|
140
|
+
pause() {
|
141
|
+
if (this.currentStream && !this.paused) {
|
142
|
+
this.currentStream.unpipe();
|
143
|
+
this.paused = true;
|
144
|
+
// Store elapsed time at pause
|
145
|
+
this.pausedElapsed = (Date.now() - this.startTime) / 1000;
|
146
|
+
if (this.progressInterval) {
|
147
|
+
clearInterval(this.progressInterval);
|
148
|
+
this.progressInterval = null;
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
resume() {
|
153
|
+
if (this.currentStream && this.paused && this.speaker) {
|
154
|
+
this.currentStream.pipe(this.speaker);
|
155
|
+
this.paused = false;
|
156
|
+
// Adjust startTime so elapsed calculation resumes correctly
|
157
|
+
this.startTime = Date.now() - this.pausedElapsed * 1000;
|
158
|
+
if (this.onProgressUpdate) {
|
159
|
+
this.progressInterval = setInterval(() => {
|
160
|
+
const elapsed = (Date.now() - this.startTime) / 1000;
|
161
|
+
if (this.onProgressUpdate) {
|
162
|
+
this.onProgressUpdate(elapsed);
|
163
|
+
}
|
164
|
+
}, 1000);
|
165
|
+
}
|
166
|
+
}
|
167
|
+
}
|
168
|
+
isPaused() {
|
169
|
+
return this.paused;
|
170
|
+
}
|
171
|
+
// Keep for backward compatibility
|
172
|
+
async playSong(songInfo) {
|
173
|
+
return new Promise((resolve, reject) => {
|
174
|
+
this.startTime = Date.now();
|
175
|
+
this.duration = this.parseDuration(songInfo.duration);
|
176
|
+
// @ts-ignore
|
177
|
+
this.speaker = new portAudio.AudioIO({
|
178
|
+
outOptions: {
|
179
|
+
channelCount: 2,
|
180
|
+
sampleFormat: portAudio.SampleFormat16Bit,
|
181
|
+
sampleRate: 44100,
|
182
|
+
deviceId: -1, // default output device
|
183
|
+
closeOnError: false
|
184
|
+
}
|
185
|
+
});
|
186
|
+
const stream = got.stream(songInfo.url);
|
187
|
+
stream.on('error', (err) => {
|
188
|
+
if (this.progressInterval)
|
189
|
+
clearInterval(this.progressInterval);
|
190
|
+
reject(err);
|
191
|
+
});
|
192
|
+
this.speaker.on('close', () => {
|
193
|
+
if (this.progressInterval)
|
194
|
+
clearInterval(this.progressInterval);
|
195
|
+
resolve();
|
196
|
+
});
|
197
|
+
stream.pipe(this.speaker);
|
198
|
+
this.speaker.start();
|
199
|
+
if (this.onProgressUpdate) {
|
200
|
+
this.progressInterval = setInterval(() => {
|
201
|
+
const elapsed = (Date.now() - this.startTime) / 1000;
|
202
|
+
if (this.onProgressUpdate) {
|
203
|
+
this.onProgressUpdate(elapsed);
|
204
|
+
}
|
205
|
+
}, 1000);
|
206
|
+
}
|
207
|
+
});
|
208
|
+
}
|
209
|
+
cleanup() {
|
210
|
+
if (this.speaker) {
|
211
|
+
this.speaker.quit();
|
212
|
+
}
|
213
|
+
if (this.progressInterval) {
|
214
|
+
clearInterval(this.progressInterval);
|
215
|
+
}
|
216
|
+
}
|
217
|
+
}
|
package/dist/types.d.ts
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
export interface SongInfo {
|
2
|
+
title: string;
|
3
|
+
duration: string;
|
4
|
+
url: string;
|
5
|
+
}
|
6
|
+
export interface MusicPlayerState {
|
7
|
+
isPlaying: boolean;
|
8
|
+
isPaused: boolean;
|
9
|
+
currentSong: SongInfo | null;
|
10
|
+
error: string | null;
|
11
|
+
isSearching: boolean;
|
12
|
+
progress: {
|
13
|
+
elapsed: number;
|
14
|
+
total: number;
|
15
|
+
};
|
16
|
+
}
|
17
|
+
export interface ProgressBarProps {
|
18
|
+
elapsed: number;
|
19
|
+
total: number;
|
20
|
+
width: number;
|
21
|
+
}
|
package/dist/types.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
package/package.json
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
{
|
2
|
+
"name": "goofyy-don",
|
3
|
+
"version": "0.1.3",
|
4
|
+
"description": "Spotify – But for the terminal",
|
5
|
+
"author": "Himanshu https://x.com/Himanshu_Saiini",
|
6
|
+
"license": "MIT",
|
7
|
+
"bin": "dist/cli.js",
|
8
|
+
"type": "module",
|
9
|
+
"engines": {
|
10
|
+
"node": ">=16"
|
11
|
+
},
|
12
|
+
"scripts": {
|
13
|
+
"build": "tsc",
|
14
|
+
"dev": "tsc --watch",
|
15
|
+
"test": "prettier --check . && xo && ava"
|
16
|
+
},
|
17
|
+
"files": [
|
18
|
+
"dist"
|
19
|
+
],
|
20
|
+
"repository": {
|
21
|
+
"type": "git",
|
22
|
+
"url": "git+https://github.com/Misterr-H/goofyy.git"
|
23
|
+
},
|
24
|
+
"bugs": {
|
25
|
+
"url": "https://github.com/Misterr-H/goofyy/issues"
|
26
|
+
},
|
27
|
+
"homepage": "https://github.com/Misterr-H/goofyy#readme",
|
28
|
+
"dependencies": {
|
29
|
+
"got": "^14.4.7",
|
30
|
+
"ink": "^4.1.0",
|
31
|
+
"meow": "^11.0.0",
|
32
|
+
"react": "^18.2.0",
|
33
|
+
"naudiodon2": "github:Misterr-H/naudiodon2#node-pre-gyp"
|
34
|
+
},
|
35
|
+
"devDependencies": {
|
36
|
+
"@sindresorhus/tsconfig": "^3.0.1",
|
37
|
+
"@types/react": "^18.0.32",
|
38
|
+
"@vdemedes/prettier-config": "^2.0.1",
|
39
|
+
"ava": "^5.2.0",
|
40
|
+
"chalk": "^5.2.0",
|
41
|
+
"eslint-config-xo-react": "^0.27.0",
|
42
|
+
"eslint-plugin-react": "^7.32.2",
|
43
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
44
|
+
"ink-testing-library": "^3.0.0",
|
45
|
+
"prettier": "^2.8.7",
|
46
|
+
"ts-node": "^10.9.1",
|
47
|
+
"typescript": "^5.0.3",
|
48
|
+
"xo": "^0.53.1"
|
49
|
+
},
|
50
|
+
"ava": {
|
51
|
+
"extensions": {
|
52
|
+
"ts": "module",
|
53
|
+
"tsx": "module"
|
54
|
+
},
|
55
|
+
"nodeArguments": [
|
56
|
+
"--loader=ts-node/esm"
|
57
|
+
]
|
58
|
+
},
|
59
|
+
"xo": {
|
60
|
+
"extends": "xo-react",
|
61
|
+
"prettier": true,
|
62
|
+
"rules": {
|
63
|
+
"react/prop-types": "off"
|
64
|
+
}
|
65
|
+
},
|
66
|
+
"prettier": "@vdemedes/prettier-config"
|
67
|
+
}
|
package/readme.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# 🎵 GOOFYY – Terminal Music Player
|
2
|
+
|
3
|
+
> A sleek command-line music player that streams your favorite songs directly in the terminal! 🎸
|
4
|
+
|
5
|
+
## ✨ Features
|
6
|
+
|
7
|
+
- 🎧 Stream music directly in your terminal
|
8
|
+
- 🔍 Search and play songs instantly
|
9
|
+
- 🎨 Clean and minimal terminal UI
|
10
|
+
- ⚡️ Fast streaming with yt-dlp
|
11
|
+
- 🎮 Simple keyboard controls
|
12
|
+
- 📊 Real-time progress bar
|
13
|
+
|
14
|
+
## 🚀 Prerequisites
|
15
|
+
|
16
|
+
Before installing Goofyy, make sure you have:
|
17
|
+
- [Node.js](https://nodejs.org/) installed
|
18
|
+
- [ffmpeg](https://ffmpeg.org/) installed
|
19
|
+
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) installed
|
20
|
+
|
21
|
+
## 📦 Installation
|
22
|
+
|
23
|
+
```bash
|
24
|
+
npx goofyy
|
25
|
+
# OR
|
26
|
+
npm i -g goofyy
|
27
|
+
```
|
28
|
+
|
29
|
+
## 🎮 Usage
|
30
|
+
|
31
|
+
```bash
|
32
|
+
# Start Goofyy with a song
|
33
|
+
goofyy "shape of you"
|
34
|
+
|
35
|
+
# Search and play any song
|
36
|
+
goofyy "ed sheeran perfect"
|
37
|
+
|
38
|
+
# Play with artist and song name
|
39
|
+
goofyy "bohemian rhapsody queen"
|
40
|
+
```
|
41
|
+
|
42
|
+
### Controls
|
43
|
+
- Press `Enter` to start playing
|
44
|
+
- Press `ESC` to exit
|
45
|
+
- Type to search for songs
|
46
|
+
|
47
|
+
## 🛠️ Technical Details
|
48
|
+
|
49
|
+
Goofyy uses:
|
50
|
+
- `yt-dlp` for fetching and streaming music
|
51
|
+
- `ffmpeg` for audio processing
|
52
|
+
- React Ink for the beautiful terminal UI
|
53
|
+
|
54
|
+
## 🤝 Contributing
|
55
|
+
|
56
|
+
Contributions are welcome! Feel free to:
|
57
|
+
- 🐛 Report bugs
|
58
|
+
- 💡 Suggest new features
|
59
|
+
- 🔧 Submit pull requests
|
60
|
+
|
61
|
+
## 📝 License
|
62
|
+
|
63
|
+
MIT License - feel free to use this project however you want!
|
64
|
+
|
65
|
+
## 🎵 Made with ❤️ by Himanshu
|
66
|
+
|
67
|
+
---
|
68
|
+
|
69
|
+
*"Because sometimes the best music player is the one that streams directly in your terminal"* 🎹
|