From cd726524db4385fb33cf3520d0e6ef6b0ac78af3 Mon Sep 17 00:00:00 2001 From: piotrruss Date: Sun, 17 Oct 2021 19:47:26 +0200 Subject: yt app --- apps/yt | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100755 apps/yt (limited to 'apps') diff --git a/apps/yt b/apps/yt new file mode 100755 index 0000000..3cfcc5b --- /dev/null +++ b/apps/yt @@ -0,0 +1,203 @@ +#!/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() -- cgit v1.2.3