diff options
author | 2021-10-17 19:47:26 +0200 | |
---|---|---|
committer | 2022-02-28 20:36:12 +0000 | |
commit | cd726524db4385fb33cf3520d0e6ef6b0ac78af3 (patch) | |
tree | 2b53eb1986b34402c2a36d38398fed64a16d65d8 | |
download | yt_audio_player_scrap-master.tar.gz yt_audio_player_scrap-master.tar.bz2 yt_audio_player_scrap-master.zip |
-rw-r--r-- | README.md | 22 | ||||
-rwxr-xr-x | apps/yt | 203 | ||||
-rw-r--r-- | yt.jpg | bin | 0 -> 139769 bytes |
3 files changed, 225 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..229ed81 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Youtube Audio Player + +Youtube audio player, great to play music in the background. +You can also check other repo [YT_Audio_Player_API](https://git.pruss.it/yt_audio_player_api/) using official Youtube API. +This one is scraping the website, so don't need an API key, but it could potentially break in future in case of changes on the website. + + + +## Usage + +1. Type song name and press [ENTER] to search it in YT +2. Type result number and press [ENTER] to play the song +3. Type a range of songs (ex. [2-5]) to play them in a row +4. Type [r(nr)] (ex. [r2]) to find songs related to chosen result +5. Type [h] and press [ENTER] to see this help page +6. Type [q] and press [ENTER] to quit YT audio player + +## Dependencies + +The only dependency is a MPV player ([sources](https://mpv.io/installation/)). +It should be available in your favourite package manager. + @@ -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<Object.keys(n).length; i++){ + if(typeof n[Object.keys(n)[i]] == "object"){ + f(n[Object.keys(n)[i]], r) + } + } + return r + } + return f(o, r) +} + +const showResults = () => { + 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 (a<b){ + const range = [...Array(b-a+1).keys()].map(n=>n+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() Binary files differ |