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 ADDED
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ type Props = {
3
+ initialQuery?: string;
4
+ };
5
+ export default function App({ initialQuery }: Props): React.JSX.Element;
6
+ export {};
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";
@@ -0,0 +1,2 @@
1
+ // export const baseUrl = 'http://localhost:3000';
2
+ export const baseUrl = 'https://goofyy.himanshu-saini.com';
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,2 @@
1
+ import React from 'react';
2
+ export declare const BlinkingCursor: React.FC;
@@ -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,7 @@
1
+ import React from 'react';
2
+ interface InstallInstructionsProps {
3
+ missing: string[];
4
+ query?: string;
5
+ }
6
+ export declare const InstallInstructions: React.FC<InstallInstructionsProps>;
7
+ export {};
@@ -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,7 @@
1
+ import React from 'react';
2
+ interface PlayPauseButtonProps {
3
+ isPlaying: boolean;
4
+ isPaused: boolean;
5
+ }
6
+ export declare const PlayPauseButton: React.FC<PlayPauseButtonProps>;
7
+ export {};
@@ -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,3 @@
1
+ import React from 'react';
2
+ import { ProgressBarProps } from '../types.js';
3
+ export declare const ProgressBar: React.FC<ProgressBarProps>;
@@ -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,3 @@
1
+ import React from 'react';
2
+ declare function About(): React.JSX.Element;
3
+ export default About;
@@ -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,3 @@
1
+ import React from "react";
2
+ declare function Discord(): React.JSX.Element;
3
+ export default Discord;
@@ -0,0 +1,7 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ function Discord() {
4
+ return (React.createElement(Box, null,
5
+ React.createElement(Text, null, "Join us on discord: https://discord.gg/HNJgYuSUQ3")));
6
+ }
7
+ export default Discord;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { MusicPlayerState } from '../types.js';
3
+ interface MusicPlayerScreenProps {
4
+ state: MusicPlayerState;
5
+ input: string;
6
+ }
7
+ export declare const MusicPlayer: React.FC<MusicPlayerScreenProps>;
8
+ export {};
@@ -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,3 @@
1
+ import React from 'react';
2
+ declare function Playlists(): React.JSX.Element;
3
+ export default Playlists;
@@ -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,3 @@
1
+ import React from 'react';
2
+ declare function StarGithub(): React.JSX.Element;
3
+ export default StarGithub;
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ function StarGithub() {
4
+ return (React.createElement(Box, null,
5
+ React.createElement(Text, null, "Star us on Github: https://github.com/Misterr-H/goofyy")));
6
+ }
7
+ export default StarGithub;
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ declare function TrendingSongs(): React.JSX.Element;
3
+ export default TrendingSongs;
@@ -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
+ }
@@ -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"* 🎹