#!/usr/bin/env node const spawn = require('child_process').spawn; const colors = require('colors'); const prompt = require('prompt'); const fetch = require('node-fetch'); const htmlChars = require('./htmlChars'); 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 [r(nr)] (ex. [r2]) to find songs related to chosen result\n5. Type [h] and press [ENTER] to see this help page\n6. Type [q] and press [ENTER] to quit YT audio player\n')); const apiUrl = 'https://www.googleapis.com/youtube/v3'; const searchUrl = q => `${apiUrl}/search?part=snippet&maxResults=20&q=${q}&type=video&key=${YTKEY}`; const detailsUrl = a => `${apiUrl}/videos?part=contentDetails&id=${a.join(',')}&key=${YTKEY}`; const relatedUrl = i => `${apiUrl}/search?part=snippet&maxResults=20&relatedToVideoId=${i}&type=video&key=${YTKEY}`; const isSongRange = q => q.match(/^[0-9]+-[0-9]+$/); const isSongName = q => q.match(/.{3,}/); const isRelated = q => q.match(/^r[0-9]+$/) && parseInt(q.replace('r','')) <= data.ids.length; 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 => getSongs(q); const handleRelated = q => { return getRelated(parseInt(q.replace('r','')) - 1); } 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 formatTime = i => i.contentDetails.duration.replace(/(PT|S)/g,'').split(/[A-Z]/g).map(d => d.padStart(2, '0')).join(':'); const getJson = (url) => fetch(url).then(res => res.json()).catch(e => console.log(e)); const getSongs = (q) => getJson(searchUrl(q)).then(json => presentResults(json)); const getRelated = (n) => getJson(relatedUrl(data.ids[n])).then(json => presentResults(json)); const getDurations = (ids) => getJson(detailsUrl(ids)).then(json => json.items.map(formatTime)); const presentResults = (results) => { const items = results.items.filter(i => i.snippet); data.titles = items.map(i => decode(i.snippet.title)); data.descriptions = items.map(i => decode(i.snippet.description)); data.channels = items.map(i => decode(i.snippet.channelTitle)); data.ids = items.map(i => i.id.videoId); getDurations(data.ids).then(durations => { data.durations = durations; clearOutput(); showOsd(); showPrompt(); }); }; const decode = t => t.replace(new RegExp(`/${Object.keys(htmlChars).join('|')}/gi`), (m) => htmlChars[m]); const handlePrompt = (err, { query }) => { if (err) { console.log(err); return 1; } if (isSongRange(query)) { handleSongRange(query); } else if (isRelated(query)) { handleRelated(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();