💬 Add playlists feature
This commit is contained in:
parent
165d095185
commit
60ceb2b18a
5 changed files with 160 additions and 7 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
.idea
|
||||
node_modules
|
||||
config.js
|
||||
data/*
|
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
130
main.js
130
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('<url>', '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('<name>', '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){
|
||||
|
|
|
@ -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"
|
||||
|
|
29
yarn.lock
29
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"
|
||||
|
|
Loading…
Reference in a new issue