UNPKG

lyric-karaoke-cli

Version:

A CLI application for displaying song lyrics in a karaoke-style format

419 lines (361 loc) • 12.2 kB
/** * 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 };