diff --git a/.gitignore b/.gitignore index b8a8673..7f5e5ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea node_modules -config.js \ No newline at end of file +config.js +data/* \ No newline at end of file diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main.js b/main.js index eca5afd..ea8005a 100644 --- a/main.js +++ b/main.js @@ -1,9 +1,16 @@ import { exec } from 'node:child_process'; +import { + readFile, + writeFile +} from 'node:fs/promises'; import { overrideConsole } from 'nodejs-better-console'; import Eris from 'eris'; import { Command, Argument } from 'commander'; import KeyEvent from 'keyevent.json' assert { type: 'json' }; +import { M3uParser } from 'm3u-parser-generator'; +import { parse as parseXspf } from 'xspf-js'; +import outdent from 'outdent'; import AsciiTable from 'ascii-table'; import { parse } from 'shell-quote'; @@ -34,7 +41,20 @@ const resolve(stdout.replace(/\r/g, '').trim()); } )), - handleCommand = async command => { + getConfig = async id => { + try { + return JSON.parse(await readFile(`./data/${id}.json`, 'utf8')); + } + catch { + return {}; + } + }, + setConfig = async (id, config) => await writeFile(`./data/${id}.json`, JSON.stringify(config)), + handleCommand = async ({ + content, + authorID, + attachments + }) => { const program = new Command(''); let response; program.exitOverride(); @@ -107,6 +127,92 @@ const .description('Play media on MPV') .argument('', 'Media URL') .action(url => adb(`am start -a android.intent.action.VIEW -d ${url} -t video/any is.xyz.mpv`)); + program + .command('playlist') + .alias('pl') + .description('Manage playlists & play on MPV') + .argument('[name]', 'Playlist name') + .argument('[item]', 'Playlist item index/name/partial name') + .action(async (name, item) => { + const + config = await getConfig(authorID) || {}, + newPlaylistUrl = attachments[0]?.url; + let { playlists } = config; + if(!playlists) + config.playlists = playlists = []; + if(!name){ + if(!playlists.length) + return response = { embed: { title: `❌ No existing playlist` } }; + return response = { + embed: { + title: `ℹ️ Your playlists :`, + description: playlists.map(playlist => `- [${playlist.name}](${playlist.url})`).join('\n') + } + }; + } + let playlist = playlists.find(playlist => playlist.name === name); + if(newPlaylistUrl){ + if(!/\.(m3u8?|xspf)$/i.test(new URL(newPlaylistUrl).pathname)) + return response = { embed: { title: `❌ Invalid playlist` } }; + response = { embed: { title: `✅ Playlist updated` } }; + if(!playlist){ + playlists.push(playlist = { name }); + response = { embed: { title: `✅ Playlist created` } }; + } + playlist.url = newPlaylistUrl; + return setConfig(authorID, config); + } + if(!playlist) + return response = { embed: { title: `❌ Playlist not found` } }; + const { pathname: playlistPathname } = new URL(playlist.url); + let + rawPlaylistData = await (await fetch(playlist.url)).text(), + playlistItems; + if(/\.m3u8?$/i.test(playlistPathname)){ + if(!rawPlaylistData.startsWith('#EXTM3U')) + rawPlaylistData = `#EXTM3U\n${rawPlaylistData}`; + playlistItems = M3uParser.parse(rawPlaylistData).medias.map(item => ({ + url: item['location'], + title: item['name'] || new URL(item['location']).pathname.split('/').slice(-1)[0] + })); + } + if(/\.xspf?$/i.test(playlistPathname)){ + playlistItems = parseXspf(rawPlaylistData)['playlist']['tracks'].map(item => ({ + url: item['location'], + title: item['title'] || new URL(item['location']).pathname.split('/').slice(-1)[0] + })); + } + if(!item) + return response = { + output: outdent ` + 📀 ${playlist.name} + ${playlistItems.map((item, index) => `${index + 1}. \`${item.title}\``).join('\n')} + ` + }; + const playlistItem = /^\d+$/.test(item) + ? playlistItems[parseInt(item) - 1] + : playlistItems.find(({ title }) => title.toLowerCase().includes(item.toLowerCase())); + if(!playlistItem) + return response = { embed: { title: `❌ Item not found` } }; + await adb(`am start -a android.intent.action.VIEW -d ${playlistItem.url} -t video/any is.xyz.mpv`); + return response = { embed: { title: `▶️ \`${playlistItem.title}\`` } }; + }); + program + .command('playlist.rm') + .alias('pl.rm') + .description('Remove playlist') + .argument('', 'Playlist name') + .action(async name => { + const + config = await getConfig(authorID) || {}, + { playlists } = config, + playlistIndex = playlists?.findIndex(playlist => playlist.name === name); + if([undefined, -1].includes(playlistIndex)) + return response = { embed: { title: `❌ No existing playlist by that name` } }; + playlists.splice(playlistIndex, 1); + await setConfig(authorID, config); + response = { embed: { title: `✅ Playlist removed` } }; + }); program .command('kiwi') .description('Launch Kiwi browser') @@ -146,7 +252,7 @@ const }); try { await program.parseAsync( - parse(command), + parse(content), { from: 'user' } ); } @@ -171,7 +277,8 @@ eris.on( author: { id: authorID }, channel: { id: channelID }, id: messageID, - content + content, + attachments } = message, reply = async (content, isCodeBlock) => { const lines = content.split('\n'); @@ -206,10 +313,23 @@ eris.on( try { const { output, - isRaw - } = await handleCommand(content) || {}; + isRaw, + embed + } = await handleCommand({ + content, + authorID, + attachments + }) || {}; if(output) await reply(output, isRaw); + if(embed) + await eris.createMessage( + channelID, + { + embed, + messageReference: { messageID } + } + ); await message.addReaction('✅'); } catch(error){ diff --git a/package.json b/package.json index caaab3d..92e18a1 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,11 @@ "commander": "^11.1.0", "eris": "^0.17.1", "keyevent.json": "^0.1.0", + "m3u-parser-generator": "^1.6.0", "nodejs-better-console": "^1.0.2", - "shell-quote": "^1.8.1" + "outdent": "^0.8.0", + "shell-quote": "^1.8.1", + "xspf-js": "^0.1.1" }, "scripts": { "start": "node ./main.js" diff --git a/yarn.lock b/yarn.lock index 7b1633c..8090e68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,6 +27,11 @@ keyevent.json@^0.1.0: resolved "https://registry.yarnpkg.com/keyevent.json/-/keyevent.json-0.1.0.tgz#598ef46218f53350ac4c5ce845dcaadcf2b6f2bb" integrity sha512-1SVBT4dTNggvKXOk8etHiMngBTfo2RaRwcNSugx94IvYNr2Yhlypoq/eerQyPY79+Ew4PyxLnKw2duRHx9n1Kw== +m3u-parser-generator@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/m3u-parser-generator/-/m3u-parser-generator-1.6.0.tgz#d65f5a68dce75a0d022d1833be352d87b07a0638" + integrity sha512-PXKVY7TcAraOvlMVYgC2XCLkV6DFFvYZfnjPeON3tBEvOTUJkt/FoDwKcfNJu0SCHVFIUE5yobZ2LEueA+3YvA== + nodejs-better-console@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nodejs-better-console/-/nodejs-better-console-1.0.2.tgz#bf6da52a3bf6ddbc61a414b1d83739c86117f207" @@ -37,6 +42,16 @@ opusscript@^0.0.8: resolved "https://registry.yarnpkg.com/opusscript/-/opusscript-0.0.8.tgz#00b49e81281b4d99092d013b1812af8654bd0a87" integrity sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ== +outdent@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.8.0.tgz#2ebc3e77bf49912543f1008100ff8e7f44428eb0" + integrity sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A== + +sax@^1.2.4: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" + integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== + shell-quote@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" @@ -51,3 +66,17 @@ ws@^8.2.3: version "8.8.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.0.tgz#8e71c75e2f6348dbf8d78005107297056cb77769" integrity sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ== + +xml-js@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" + integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== + dependencies: + sax "^1.2.4" + +xspf-js@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/xspf-js/-/xspf-js-0.1.1.tgz#af54fc9514556a36d3a2e0186a3ae46f401853da" + integrity sha512-/2lUkOvMkjBLrHqk9zts0Hn589nfG07MakYIGhI4lseDsXi8YDvsFDCX8iAE2Dsm7lkZnyS7CkaYfRSlkS+oyw== + dependencies: + xml-js "^1.6.11"