terminal-music-player
Version:
A terminal-based music player that streams from YouTube
399 lines (336 loc) • 10.1 kB
JavaScript
import YTMusic from "ytmusic-api";
import { spawn } from "child_process";
import inquirer from "inquirer";
import chalk from "chalk";
import which from "which";
import MPVInstaller from "../scripts/install-mpv.js";
const ytm = new YTMusic();
let mpvProcess = null;
let currentPlaylist = [];
let currentTrackIndex = 0;
let isPlaying = false;
let autoPlay = true;
let isTransitioning = false;
async function checkMPVAvailability() {
try {
await which('mpv');
return true;
} catch (error) {
return false;
}
}
async function handleMPVInstallation() {
const isMPVAvailable = await checkMPVAvailability();
if (!isMPVAvailable) {
console.log(chalk.red.bold('⚠️ MPV Media Player not found!'));
console.log(chalk.yellow('MPV is required for audio playback in the terminal music player.\n'));
const { shouldInstall } = await inquirer.prompt({
type: 'confirm',
name: 'shouldInstall',
message: 'Would you like to install MPV automatically now?',
default: true
});
if (shouldInstall) {
console.log(''); // Empty line for spacing
const installer = new MPVInstaller();
const success = await installer.install();
if (!success) {
console.log(chalk.red('\nMPV installation failed. Please install MPV manually and restart the application.'));
process.exit(1);
}
console.log(''); // Empty line for spacing
} else {
console.log(chalk.yellow('\nPlease install MPV manually and restart the application.'));
console.log(chalk.gray('Visit https://mpv.io/installation/ for installation instructions.'));
process.exit(1);
}
}
}
async function initialize() {
try {
await ytm.initialize();
console.log(chalk.green("YTMusic API initialized successfully!"));
} catch (error) {
console.error(chalk.red("Error initializing YTMusic API:"), error);
process.exit(1);
}
}
async function searchTracks(query) {
try {
const results = await ytm.search(query);
return results
.filter((result) => result.type === "VIDEO" && result.videoId)
.map((result) => ({
id: result.videoId,
title: result.name,
artist: result.artist?.name || "Unknown Artist",
duration: formatDuration(result.duration) || "0:00",
thumbnail: result.thumbnails?.[0]?.url,
}));
} catch (error) {
console.error(chalk.red("Error searching tracks:"), error);
return [];
}
}
function formatDuration(seconds) {
if (!seconds) return "0:00";
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, "0")}`;
}
function cleanup() {
console.log(chalk.yellow("\nCleaning up..."));
if (mpvProcess && !mpvProcess.killed) {
try {
mpvProcess.kill("SIGTERM");
setTimeout(() => {
if (mpvProcess && !mpvProcess.killed) {
console.log(chalk.red("Force killing MPV process..."));
mpvProcess.kill("SIGKILL");
}
}, 2000);
} catch (error) {
console.error(chalk.red("Error stopping MPV:"), error.message);
try {
mpvProcess.kill("SIGKILL");
} catch (killError) {
console.error(chalk.red("Error force killing MPV:"), killError.message);
}
}
mpvProcess = null;
}
isPlaying = false;
isTransitioning = false;
}
function stopTrack() {
if (mpvProcess) {
autoPlay = false;
cleanup();
}
}
function playTrack(trackId) {
if (isTransitioning) {
console.log(chalk.yellow("Track transition in progress, please wait..."));
return;
}
isTransitioning = true;
if (mpvProcess) {
mpvProcess.kill("SIGTERM");
mpvProcess = null;
}
const url = `https://www.youtube.com/watch?v=${trackId}`;
const currentTrack = currentPlaylist[currentTrackIndex];
console.log(
chalk.yellow.bold("\nNow Playing:"),
chalk.white(`${currentTrack.title} - ${currentTrack.artist}`)
);
console.log(chalk.gray(`Duration: ${currentTrack.duration}`));
mpvProcess = spawn("mpv", [
"--no-video",
"--quiet",
"--no-terminal",
"--audio-display=no",
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
url,
]);
isPlaying = true;
autoPlay = true;
mpvProcess.on("error", (error) => {
console.error(chalk.red("MPV Error:"), error.message);
if (error.code === 'ENOENT') {
console.error(chalk.red("MPV not found! It may have been uninstalled or removed from PATH."));
console.log(chalk.yellow("Please restart the application to reinstall MPV, or install it manually."));
}
isPlaying = false;
isTransitioning = false;
if (autoPlay && currentPlaylist.length > 1) {
setTimeout(() => {
playNext();
}, 1000);
}
});
mpvProcess.on("close", (code) => {
isPlaying = false;
isTransitioning = false;
if (code === 0) {
console.log(chalk.gray("\nTrack finished playing"));
} else if (code !== null) {
console.log(chalk.red(`\nTrack stopped with code: ${code}`));
}
if (autoPlay && code === 0 && currentPlaylist.length > 1) {
setTimeout(() => {
playNext();
}, 500);
}
});
setTimeout(() => {
isTransitioning = false;
}, 1000);
}
function playNext() {
if (currentPlaylist.length === 0) {
console.log(chalk.yellow("Playlist is empty"));
return;
}
if (currentPlaylist.length === 1) {
console.log(chalk.yellow("Only one track in playlist"));
return;
}
const oldIndex = currentTrackIndex;
currentTrackIndex++;
if (currentTrackIndex >= currentPlaylist.length) {
currentTrackIndex = 0;
}
if (currentTrackIndex === oldIndex) {
console.log(chalk.yellow("Reached end of playlist"));
return;
}
playTrack(currentPlaylist[currentTrackIndex].id);
}
function playPrevious() {
if (currentPlaylist.length === 0) {
console.log(chalk.yellow("Playlist is empty"));
return;
}
currentTrackIndex--;
if (currentTrackIndex < 0) {
currentTrackIndex = currentPlaylist.length - 1;
}
playTrack(currentPlaylist[currentTrackIndex].id);
}
function displayPlaylist() {
if (currentPlaylist.length === 0) {
console.log(chalk.yellow("\nPlaylist is empty"));
return;
}
console.log(chalk.blue.bold("\nCurrent Playlist:"));
currentPlaylist.forEach((track, index) => {
const prefix =
index === currentTrackIndex ? chalk.green.bold("▶ ") : chalk.gray(" ");
console.log(
`${prefix}${track.title} - ${track.artist} ${chalk.gray(
`(${track.duration})`
)}`
);
});
}
function nowPlaying() {
if (!isPlaying || currentPlaylist.length === 0) {
console.log(chalk.yellow("\nNo track is currently playing"));
return;
}
const track = currentPlaylist[currentTrackIndex];
console.log(chalk.green.bold("\nNow Playing:"));
console.log(chalk.white(`Title: ${track.title}`));
console.log(chalk.white(`Artist: ${track.artist}`));
console.log(chalk.white(`Duration: ${track.duration}`));
}
async function mainMenu() {
while (true) {
const { choice } = await inquirer.prompt({
type: "list",
name: "choice",
message: "Music Player Menu",
choices: [
{ name: "Search and play music", value: "search" },
{ name: "Show current playlist", value: "show" },
{ name: "Now playing", value: "now" },
{ name: "Play next track", value: "next" },
{ name: "Play previous track", value: "prev" },
{ name: "Stop playback", value: "stop" },
{ name: "Exit", value: "exit" },
],
});
switch (choice) {
case "search":
await searchAndPlay();
break;
case "show":
displayPlaylist();
break;
case "now":
nowPlaying();
break;
case "next":
playNext();
break;
case "prev":
playPrevious();
break;
case "stop":
stopTrack();
console.log(chalk.yellow("Playback stopped"));
break;
case "exit":
cleanup();
console.log(chalk.green("Goodbye!"));
process.exit(0);
}
}
}
async function searchAndPlay() {
const { query } = await inquirer.prompt({
type: "input",
name: "query",
message: "Enter search query:",
});
const results = await searchTracks(query);
if (results.length === 0) {
console.log(chalk.red("\nNo results found"));
return;
}
const { selectedIndex } = await inquirer.prompt({
type: "list",
name: "selectedIndex",
message: "Select a track to play:",
choices: [
...results.map((track, index) => ({
name: `${track.title} - ${track.artist} (${track.duration})`,
value: index,
})),
{ name: "Cancel", value: -1 },
],
});
if (selectedIndex === -1) return;
currentPlaylist = results;
currentTrackIndex = selectedIndex;
playTrack(results[selectedIndex].id);
}
function gracefulShutdown(signal) {
console.log(chalk.yellow(`\nReceived ${signal}, cleaning up...`));
cleanup();
setTimeout(() => {
console.log(chalk.green("Cleanup complete. Goodbye!"));
process.exit(0);
}, 1000);
}
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGHUP", () => gracefulShutdown("SIGHUP"));
process.on("SIGQUIT", () => gracefulShutdown("SIGQUIT"));
process.on("uncaughtException", (error) => {
console.error(chalk.red("Uncaught Exception:"), error);
cleanup();
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
console.error(
chalk.red("Unhandled Rejection at:"),
promise,
"reason:",
reason
);
cleanup();
process.exit(1);
});
process.on("exit", (code) => {
console.log(chalk.gray(`Process exiting with code: ${code}`));
cleanup();
});
(async () => {
await handleMPVInstallation();
await initialize();
console.log(chalk.blue.bold("\nCLI Music Player\n"));
await mainMenu();
})();