lyric-karaoke-cli
Version:
A CLI application for displaying song lyrics in a karaoke-style format
419 lines (361 loc) ⢠12.2 kB
JavaScript
/**
* CLI interface module for user interaction
*/
const inquirer = require('inquirer');
const chalk = require('chalk');
const api = require('../api');
const cache = require('../cache');
const { logger, formatLyrics } = require('../utils');
/**
* Start the CLI interface
*/
async function start() {
console.log(chalk.bold.green('\nšµ LYRIC KARAOKE CLI šµ\n'));
console.log(chalk.cyan('Search for songs and display lyrics in karaoke style!\n'));
await mainMenu();
}
/**
* Display the main menu
*/
async function mainMenu() {
try {
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: 'Search for a song', value: 'search' },
{ name: 'View recent songs', value: 'recent' },
{ name: 'Clear cache', value: 'clearCache' },
{ name: 'Exit', value: 'exit' }
]
}
]);
switch (action) {
case 'search':
await searchSong();
break;
case 'recent':
await viewRecentSongs();
break;
case 'clearCache':
await cache.clear();
console.log(chalk.green('Cache cleared successfully!'));
await mainMenu();
break;
case 'exit':
console.log(chalk.blue('Thanks for using Lyric Karaoke CLI! Goodbye! š'));
process.exit(0);
break;
}
} catch (error) {
logger.error('Error in main menu:', error.message);
console.log(chalk.red('An error occurred. Please try again.'));
await mainMenu();
}
}
/**
* Search for a song
*/
async function searchSong() {
try {
const { query } = await inquirer.prompt([
{
type: 'input',
name: 'query',
message: 'Enter song title, artist, or lyrics:',
validate: input => input.length > 0 ? true : 'Please enter a search term'
}
]);
console.log(chalk.yellow('\nSearching for songs...'));
// Check cache first
const cachedResults = await cache.get(`search:${query}`);
let searchResults;
if (cachedResults) {
searchResults = cachedResults;
console.log(chalk.gray('(Results loaded from cache)'));
} else {
searchResults = await api.searchSongs(query);
// Cache the results
await cache.set(`search:${query}`, searchResults);
}
if (searchResults.length === 0) {
console.log(chalk.red('\nNo songs found. Please try a different search term.'));
return await mainMenu();
}
await displaySearchResults(searchResults);
} catch (error) {
logger.error('Error searching for song:', error.message);
console.log(chalk.red('Error searching for songs. Please try again.'));
await mainMenu();
}
}
/**
* Display search results and allow user to select a song
* @param {Array} results - Search results
*/
async function displaySearchResults(results) {
try {
const choices = results.map((song, index) => ({
name: `${index + 1}. ${song.title} - ${song.artist}`,
value: song.id
}));
choices.push({ name: 'Return to main menu', value: 'back' });
const { songId } = await inquirer.prompt([
{
type: 'list',
name: 'songId',
message: 'Select a song:',
choices
}
]);
if (songId === 'back') {
return await mainMenu();
}
await displaySongLyrics(songId);
} catch (error) {
logger.error('Error displaying search results:', error.message);
console.log(chalk.red('Error displaying results. Please try again.'));
await mainMenu();
}
}
/**
* Display song lyrics in karaoke style
* @param {string} songId - ID of the selected song
*/
async function displaySongLyrics(songId) {
try {
console.log(chalk.yellow('\nFetching lyrics...'));
// Check cache first
const cachedSong = await cache.get(`song:${songId}`);
let songDetails;
if (cachedSong) {
songDetails = cachedSong;
console.log(chalk.gray('(Lyrics loaded from cache)'));
} else {
songDetails = await api.getSongDetails(songId);
// Cache the song details
await cache.set(`song:${songId}`, songDetails);
}
console.clear();
console.log(chalk.bold.cyan(`\nšµ ${songDetails.title} - ${songDetails.artist} šµ\n`));
// Display options for viewing lyrics
const { displayMode } = await inquirer.prompt([
{
type: 'list',
name: 'displayMode',
message: 'How would you like to view the lyrics?',
choices: [
{ name: 'Karaoke Mode (animated with controls)', value: 'karaoke' },
{ name: 'Simple Mode (static display)', value: 'simple' }
]
}
]);
if (displayMode === 'karaoke') {
// Use the new karaoke-style display
await displayLyrics(songDetails.lyrics, {
autoScroll: true,
speed: 2000
});
} else {
// Simple static display
console.clear();
console.log(chalk.bold.cyan(`\nšµ ${songDetails.title} - ${songDetails.artist} šµ\n`));
console.log(formatLyrics(songDetails.lyrics));
}
await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Options:',
choices: [
{ name: 'Return to main menu', value: 'main' }
]
}
]);
await mainMenu();
} catch (error) {
logger.error('Error displaying lyrics:', error.message);
console.log(chalk.red('Error displaying lyrics. Please try again.'));
await mainMenu();
}
}
/**
* View recently viewed songs from cache
*/
async function viewRecentSongs() {
try {
const recentSongs = await cache.getRecentSongs();
if (recentSongs.length === 0) {
console.log(chalk.yellow('\nNo recent songs found.'));
return await mainMenu();
}
const choices = recentSongs.map((song, index) => ({
name: `${index + 1}. ${song.title} - ${song.artist}`,
value: song.id
}));
choices.push({ name: 'Return to main menu', value: 'back' });
const { songId } = await inquirer.prompt([
{
type: 'list',
name: 'songId',
message: 'Select a recent song:',
choices
}
]);
if (songId === 'back') {
return await mainMenu();
}
await displaySongLyrics(songId);
} catch (error) {
logger.error('Error displaying recent songs:', error.message);
console.log(chalk.red('Error accessing recent songs. Please try again.'));
await mainMenu();
}
}
/**
* Display lyrics in a karaoke-style format
* @param {string} lyrics - The lyrics text to display
* @param {Object} options - Display options (speed, highlight color, etc.)
* @returns {Promise<void>}
*/
async function displayLyrics(lyrics, options = {}) {
try {
// Default options
const defaultOptions = {
speed: 2000, // milliseconds per line
highlightColor: 'green',
pauseBetweenLines: 500, // milliseconds
autoScroll: true
};
// Merge default options with user-provided options
const displayOptions = { ...defaultOptions, ...options };
// Format and split the lyrics into lines
const formattedLyrics = formatLyrics(lyrics);
const lines = formattedLyrics.split('\n').filter(line => line.trim() !== '');
// Clear the console and prepare the display
console.clear();
console.log(chalk.bold.cyan('\nšµ KARAOKE MODE šµ\n'));
let isPaused = false;
let currentLineIndex = 0;
// Setup user controls
console.log(chalk.gray('Controls: [Space] Pause/Resume | [ā/ā] Navigate | [Esc] Exit\n'));
// If we're not in auto-scroll mode, display all lyrics at once
if (!displayOptions.autoScroll) {
console.log(formattedLyrics);
// Wait for user to press a key to return
await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Options:',
choices: [
{ name: 'Return to main menu', value: 'main' }
]
}
]);
return;
}
// Function to display the current line and surrounding context
const displayCurrentLine = () => {
console.clear();
console.log(chalk.bold.cyan('\nšµ KARAOKE MODE šµ\n'));
console.log(chalk.gray('Controls: [Space] Pause/Resume | [ā/ā] Navigate | [Esc] Exit\n'));
// Show a few lines before the current line for context
const contextBefore = 2;
const startLine = Math.max(0, currentLineIndex - contextBefore);
// Display previous lines in gray
for (let i = startLine; i < currentLineIndex; i++) {
console.log(chalk.gray(lines[i]));
}
// Display current line highlighted
console.log(chalk[displayOptions.highlightColor].bold('> ' + lines[currentLineIndex]));
// Show a few lines after the current line
const contextAfter = 3;
const endLine = Math.min(lines.length, currentLineIndex + contextAfter + 1);
// Display next lines in normal text
for (let i = currentLineIndex + 1; i < endLine; i++) {
console.log(lines[i]);
}
// Show status (paused or playing)
console.log('\n' + chalk.gray(isPaused ? '[PAUSED]' : '[PLAYING]'));
};
// Initial display
displayCurrentLine();
// Setup readline for keyboard input
const readline = require('readline');
readline.emitKeypressEvents(process.stdin);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
return new Promise((resolve) => {
// Timer for auto-scrolling
let timer = null;
const startTimer = () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
if (!isPaused && currentLineIndex < lines.length - 1) {
currentLineIndex++;
displayCurrentLine();
startTimer();
} else if (currentLineIndex >= lines.length - 1) {
// End of lyrics
console.log(chalk.yellow('\nEnd of lyrics. Press any key to return...'));
// Keep listening for keypress
}
}, displayOptions.speed);
};
if (displayOptions.autoScroll && !isPaused) {
startTimer();
}
// Handle keyboard input
process.stdin.on('keypress', (str, key) => {
if (key.ctrl && key.name === 'c') {
// Ctrl+C exits
if (timer) clearTimeout(timer);
process.stdin.setRawMode(false);
process.stdin.removeAllListeners('keypress');
resolve();
return;
}
switch (key.name) {
case 'space':
// Toggle pause/play
isPaused = !isPaused;
if (!isPaused) startTimer();
displayCurrentLine();
break;
case 'up':
// Go to previous line
if (currentLineIndex > 0) {
currentLineIndex--;
displayCurrentLine();
}
break;
case 'down':
// Go to next line
if (currentLineIndex < lines.length - 1) {
currentLineIndex++;
displayCurrentLine();
}
break;
case 'escape':
// Exit karaoke mode
if (timer) clearTimeout(timer);
process.stdin.setRawMode(false);
process.stdin.removeAllListeners('keypress');
resolve();
break;
}
});
});
} catch (error) {
logger.error('Error in karaoke display:', error.message);
console.log(chalk.red('Error displaying lyrics. Please try again.'));
}
}
module.exports = {
start,
displayLyrics
};