#!/usr/bin/env node /* pruss.it */ const https = require('https') const readline = require("readline") const spawn = require('child_process').spawn let results = [] const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) const p = { log: m => console.log(m), cls: () => console.clear(), inf: m => console.log(`\x1b[32m${m}\x1b[0m`), err: e => console.error(`\x1b[31m${e}\x1b[0m`), prt: () => prompt(), res: () => showResults() } const joinRuns = t => t ? t.runs ? t.runs.map(r => r.text).join('').split(/\r\n|\r|\n/).slice(0,14).join('\n') : t.simpleText : '-' const convertViews = (v) => { const t = v ? v.split(' ')[0].split('.') : '-' switch(t.length) { case 2: return t[0] + 'K' case 3: return t[0] + 'M' case 4: return t[0] + 'B' default: return t } } const findNested = (obj, key) => { let r for (let k in obj) { if (obj.hasOwnProperty(k)) { if (k === key) return obj[key] else if (typeof obj[k] === "object") { r = findNested(obj[k], key) if (typeof r !== "undefined") return r } } } } const findAllNested = (o, p) => { var r = [] const f = (n, r) => { if(n.hasOwnProperty(p)) r.push(n[p]) for(var i=0; i { results.forEach((r, i) => { p.log(`\x1b[33m${i+1}. \x1b[36m${r.title} \x1b[32m[${r.length}] \x1b[35m${r.channel} \x1b[31m${r.views}\x1b[0m`) }) p.log(' ') } const parseVideos = o => o.map(v => ({ id: v.videoId, title: joinRuns(v.title), length: v?.lengthText?.simpleText, views: convertViews(v?.viewCountText?.simpleText), channel: joinRuns(v?.shortBylineText) })) const handleResObj = (x) => { const n = (findAllNested(JSON.parse(x[0])['contents'], 'videoRenderer')) results = parseVideos(n) p.cls(); p.res(); p.prt() } const objFromRes = (r, c) => { const x = r.match(/(?<=ytInitialData = )(.*?)(?=;<\/script>)/gm) if (x) c(x) } const getResponse = (u, c) => https.get(u, res => { var r = '' res.on('data', d => { r += d }) res.on('end', () => { objFromRes(r, c) }) res.on('error', e => { p.err(e) }) }) 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','')) <= results.length const isSongNumber = q => q.match(/^[0-9]+$/) && parseInt(q) <= results.length const showPlaying = n => { getResponse(('https://www.youtube.com/watch?v='+results[n].id), x => { p.log('\x1b[32mDescription:\n------------\x1b[36m') p.log(joinRuns(findNested(JSON.parse(x)['contents'], 'description')), '\x1b[0m') p.log(' ') }) p.log(`\n\x1b[32mCurrently playing:\n------------------\n\x1b[32mTitle: \x1b[36m${results[n].title}\n\x1b[32mChannel: \x1b[36m${results[n].channel}\n\x1b[32mLength: \x1b[36m${results[n].length}\n\x1b[32mViews: \x1b[36m${results[n].views}\x1b[0m\n`) } const showPlaylist = (r, n) => p.log(r.map(x=>`${x===n?'\x1b[33m':'\x1b[36m'}${x}. ${results[x-1].title}\x1b[0m`).join('\n'),'\n') const playNr = (n) => spawn('mpv', ['--no-video', '--msg-level=all=no,statusline=status', `https://www.youtube.com/watch?v=${results[parseInt(n)].id}`], { stdio: 'inherit' }) const playNext = (r, n) => { p.cls() p.inf('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 { p.cls(); p.res(); p.prt() } }) } const handleSongRange = q => { if (results.length > 0) { const [a,b] = q.split('-').map(n=>parseInt(n)) if (an+a) playNext(range, range[0]) } else { p.cls(); p.res(); p.err('invalid range, try again'); p.prt() } } else { p.cls(); p.err('first search for the videos'); p.prt() } }; const handleRelated = q => getResponse(('https://www.youtube.com/watch?v='+results[parseInt(q.substring(1)) - 1].id), x => { results = parseVideos(findAllNested(JSON.parse(x)['contents'], 'compactVideoRenderer')) p.cls(); p.res(); p.prt() }) const handleSongName = q => getResponse(('https://www.youtube.com/results?search_query='+q.replaceAll(' ', '+')), handleResObj) const handleSongNumber = q => { const nr = parseInt(q) - 1 p.cls(); showPlaying(nr) const player = playNr(nr) player.on('close', () => { p.cls(); p.res(); p.prt() }) } const handleHelp = () => { p.cls() p.log('\x1b[36mYT Audio Player (HELP)\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\x1b[0m\n') p.prt() } const handleQuit = () => { rl.close(); p.cls(); p.inf('See you!\n\n'); return 0 } const handleWrongInput = () => { p.cls(); p.res(); p.err('wrong input\n'); p.prt() } const handlePrompt = q => { rl.pause() if (isSongRange(q)) { handleSongRange(q) } else if (isRelated(q)) { handleRelated(q) } else if (isSongName(q)) { handleSongName(q) } else if (isSongNumber(q)) { handleSongNumber(q) } else if (q === 'h') { handleHelp() } else if (q === 'q') { handleQuit() } else { handleWrongInput() } } const prompt = () => { rl.resume() rl.question("\x1b[32mYT Audio Player ([h]elp, [q]uit): \x1b[0m", k => { handlePrompt(k) }) } p.cls(); p.prt()