#!/usr/bin/env node const spawn = require('child_process').spawn; const colors = require('colors'); const prompt = require('prompt'); const fetch = require('node-fetch'); const { YTKEY } = require('./config'); let data = {ids: [], titles: [], descriptions: [], channels: [], durations: []}; const clearOutput = () => console.clear(); const showError = (msg) => console.log(colors.red('Error'), ' - ', msg); const showMsg = (msg) => console.log(colors.green(msg)); const showOsd = () => { data.ids.forEach((id, i) => { console.log(`${colors.yellow(i+1)}. ${colors.cyan(data.titles[i])} ${colors.green('['+data.durations[i]+']')} ${colors.red(data.channels[i])}`); }); console.log(''); }; const showPlaying = (n) => console.log(`${colors.green('Currently playing:\n------------------')}\n${colors.yellow('Title:')} ${colors.cyan(data.titles[n])}\n${colors.yellow('Description:')} ${colors.cyan(data.descriptions[n])}\n${colors.yellow('Channel:')} ${colors.cyan(data.channels[n])}\n${colors.yellow('Length:')} ${colors.cyan(data.durations[n])}\n`); const showPlaylist = (r,n) => console.log(r.map(x=>x===n?(colors.yellow(x+'. '+data.titles[x-1])):(colors.cyan(x+'. '+data.titles[x-1]))).join('\n'),'\n'); const showHelp = () => console.log(colors.green('YT audio player\n---------------\n1. Type song name and press [ENTER] to search it in YT\n2. Type result number and press [ENTER] to play the song\n3. Type a range of songs (ex. [2-5]) to play them in a row\n4. Type "h" and press [ENTER] to see this help page\n5. Type "q" and press [ENTER] to quit YT audio player\n')); const searchUrl = (q) => `https://www.googleapis.com/youtube/v3/search?part=snippet&partcontentDetails&maxResults=20&q=${q}&type=video&key=${YTKEY}`; const detailsUrl = (a) => `https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id=${a.join(',')}&key=${YTKEY}`; const isSongRange = q => q.match(/^[0-9]+-[0-9]+$/); const isSongName = q => q.match(/.{3,}/); const isSongNumber = q => q.match(/^[0-9]+$/) && parseInt(q) <= data.ids.length; const playNr = (n) => spawn('mpv', ['--no-video', '--msg-level=all=no,statusline=status', `https://www.youtube.com/watch?v=${data.ids[parseInt(n)]}`], { stdio: 'inherit' }); const playNext = (r, n) => { clearOutput(); showMsg('Playlist:\n---------'); showPlaylist(r, n); showPlaying(n-1); const player = playNr(n-1); player.on('close', () => { if (n < r[r.length-1]){ playNext(r, n+1); } else { clearOutput(); showOsd(); showPrompt(); } }); }; const handleSongRange = (q) => { if (data.ids.length > 0) { const [a,b] = q.split('-').map(n=>parseInt(n)); if (an+a); playNext(range, range[0]); } else { clearOutput(); showOsd(); showError('invalid range, try again'); showPrompt(); } } else { clearOutput(); showError('first search for the videos'); showPrompt(); } }; const handleSongName = (q) => ( fetch(searchUrl(q)) .then(res => res.json()) .then(json => presentResults(json)) .catch(e => console.log(e)) ); const handleSongNumber = (q) => { const nr = parseInt(q) - 1; clearOutput(); showPlaying(nr); const player = playNr(nr); player.on('close', () => { clearOutput(); showOsd(); showPrompt(); }); }; const handleHelp = () => { clearOutput(); showHelp(); showPrompt(); }; const handleQuit = () => { clearOutput(); showMsg('Quiting YT audio player...'); return 0; }; const handleWrongInput = () => { clearOutput(); showOsd(); showError('wrong input'); showPrompt(); }; const getDurations = (ids) => ( fetch(detailsUrl(ids)) .then(res => res.json()) .then(json => json.items.map(i => i.contentDetails.duration.replace(/(PT|S)/g,'').split(/[A-Z]/g).map(d => d.padStart(2, '0')).join(':'))) .catch(e => console.log(e)) ); const presentResults = (results) => { data.titles = results.items.map(result => result.snippet.title); data.descriptions = results.items.map(result => result.snippet.description); data.channels = results.items.map(result => result.snippet.channelTitle); data.ids = results.items.map(result => result.id.videoId); getDurations(data.ids).then(durations => { data.durations = durations; clearOutput(); showOsd(); showPrompt(); }); }; const handlePrompt = (err, { query }) => { if (err) { console.log(err); return 1; } if (isSongRange(query)) { handleSongRange(query); } else if (isSongName(query)) { handleSongName(query); } else if (isSongNumber(query)) { handleSongNumber(query); } else if (query === 'h') { handleHelp(); } else if (query === 'q') { handleQuit(); } else { handleWrongInput(); } }; const showPrompt = () => { prompt.message = ""; prompt.delimiter = ""; prompt.start(); prompt.get({ name: 'query', description: 'YT Search ([h]elp, [q]uit):', required: true, }, handlePrompt); } clearOutput(); showPrompt();