discord-home-cinema/main.js

403 lines
16 KiB
JavaScript
Raw Permalink Normal View History

2024-01-27 03:54:37 +00:00
import { exec } from 'node:child_process';
2024-01-28 02:20:29 +00:00
import {
readFile,
writeFile
} from 'node:fs/promises';
2024-01-27 03:54:37 +00:00
2022-07-11 09:34:15 +00:00
import { overrideConsole } from 'nodejs-better-console';
import Eris from 'eris';
2024-01-27 19:38:33 +00:00
import { Command, Argument } from 'commander';
import KeyEvent from 'keyevent.json' assert { type: 'json' };
2024-01-28 02:20:29 +00:00
import { M3uParser } from 'm3u-parser-generator';
import { parse as parseXspf } from 'xspf-js';
import outdent from 'outdent';
2024-01-27 21:25:29 +00:00
import AsciiTable from 'ascii-table';
2024-01-29 01:19:43 +00:00
import dayjs from 'dayjs';
2024-01-27 03:54:37 +00:00
import { parse } from 'shell-quote';
2022-07-11 09:34:15 +00:00
import {
botToken,
2024-01-27 03:54:37 +00:00
botWhitelist,
adbIp,
2024-01-28 22:17:53 +00:00
adbPort,
localMediaRootPath
2022-07-11 09:34:15 +00:00
} from './config.js';
overrideConsole();
const
eris = new Eris(
botToken,
{ intents: ['directMessages'] }
2024-01-27 03:54:37 +00:00
),
adb = command => new Promise((resolve, reject) => exec(
2024-01-28 22:17:53 +00:00
`adb -s ${adbIp}:${adbPort} shell "${command.replaceAll('"', '\\"')}"`,
2024-01-27 03:54:37 +00:00
(
error,
stdout,
stderr
) => {
if(error || stderr)
reject(error || stderr);
else
resolve(stdout.replace(/\r/g, '').trim());
2024-01-27 03:54:37 +00:00
}
)),
2024-01-28 02:20:29 +00:00
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)),
parseMediaUrl = url => {
const
pathname = decodeURI(new URL(url).pathname),
title = pathname
.slice(pathname.lastIndexOf('/') + 1, pathname.lastIndexOf('.'))
.replace(/[\s\-._()&+\[\],]/g, ' ')
.replace(/\s+/g, ' ');
2024-01-28 22:17:53 +00:00
return {
url,
2024-01-28 22:17:53 +00:00
title,
keywords: title.toLowerCase().split(' ')
};
},
2024-01-28 02:20:29 +00:00
handleCommand = async ({
content,
authorID,
attachments
}) => {
content = `${content[0].toLowerCase()}${content.slice(1)}`;
2024-01-27 03:54:37 +00:00
const program = new Command('');
let response;
program.exitOverride();
program.configureOutput({
writeOut: output => response = { output, isRaw: true },
writeErr: output => response = { output, isRaw: true }
});
program
.command('shell')
.description('Execute arbitrary shell command')
.argument('<command>', 'Arbitrary shell command')
.action(async command => {
response = {
output: await adb(command),
isRaw: true
}
});
2024-01-27 19:38:33 +00:00
program
.command('home')
.alias('h')
.description('Go to home')
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_HOME}`));
program
.command('back')
.alias('b')
.description('Go back')
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_BACK}`));
2024-01-27 03:54:37 +00:00
program
.command('p')
.description('Play/pause media')
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE}`));
2024-01-27 03:54:37 +00:00
program
.command('play')
.description('Play media')
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_MEDIA_PLAY}`));
2024-01-27 03:54:37 +00:00
program
.command('pause')
.description('Pause media')
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_MEDIA_PAUSE}`));
2024-01-27 19:38:33 +00:00
program
.command('prev')
.description('Previous media')
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_MEDIA_PREVIOUS}`));
program
.command('next')
.alias('n')
2024-01-27 19:38:33 +00:00
.description('Next media')
.action(() => adb(`input keyevent ${KeyEvent.KEYCODE_MEDIA_NEXT}`));
program
.command('type')
.description('Type text')
.argument('<text>', 'Text to type')
.action(text => adb(`input text ${text}`));
program
.command('press')
.description('Press key')
.addArgument(
new Argument('<key>', 'Key to press')
2024-01-27 23:05:45 +00:00
.choices(['tab', 'enter'])
2024-01-27 19:38:33 +00:00
)
.action(key => adb(`input keyevent ${{
'tab': KeyEvent.KEYCODE_TAB,
'enter': KeyEvent.KEYCODE_ENTER
}[key]}`));
2024-01-27 03:54:37 +00:00
program
.command('spotify')
.description('Launch Spotify')
.action(() => adb('monkey -p com.spotify.music 1'));
program
.command('mpv')
.description('Play media on MPV')
2024-01-27 03:54:37 +00:00
.argument('<url>', 'Media URL')
.action(url => adb(`am start -a android.intent.action.VIEW -d ${url} -t video/any is.xyz.mpv`));
2024-01-28 02:20:29 +00:00
program
.command('playlist')
.alias('pl')
.description('Manage playlists & play on MPV')
.argument('[name]', 'Playlist name')
.argument('[media]', 'Playlist media index/title/partial title')
.action(async (name, media) => {
2024-01-28 02:20:29 +00:00
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 => parseMediaUrl(item['location']));
2024-01-28 02:20:29 +00:00
}
if(/\.xspf?$/i.test(playlistPathname))
playlistItems = parseXspf(rawPlaylistData)['playlist']['tracks'].map(item => parseMediaUrl(item['location']));
if(!media)
2024-01-28 02:20:29 +00:00
return response = {
output: outdent `
📀 ${playlist.name}
${playlistItems.map((item, index) => `${index + 1}. \`${item.title}\``).join('\n')}
`
};
const
isIndex = /^\d+$/.test(media),
keywords = isIndex ? undefined : media.toLowerCase().split(' '),
matchingPlaylistItems = isIndex
? [playlistItems[parseInt(media) - 1]]
2024-01-28 23:09:01 +00:00
: playlistItems.filter(item => keywords.every(keyword => item.title.toLowerCase().includes(keyword)));
if(matchingPlaylistItems.length === 0)
return response = { embed: { title: `❌ Media not found` } };
if(matchingPlaylistItems.length > 1)
return response = {
output: outdent `
🔎 Multiple media found :
${matchingPlaylistItems.map(item => `${playlistItems.indexOf(item) + 1}\\. \`${item.title}\``).join('\n')}
`
};
const [matchingPlaylistItem] = matchingPlaylistItems;
await adb(`am start -a android.intent.action.VIEW -d ${matchingPlaylistItem.url} -t video/any is.xyz.mpv`);
return response = { embed: { title: `▶️ \`${matchingPlaylistItem.title}\` (${playlistItems.indexOf(matchingPlaylistItem) + 1}/${playlistItems.length})` } };
2024-01-28 02:20:29 +00:00
});
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` } };
});
2024-01-28 22:17:53 +00:00
program
.command('local')
.alias('l')
.description('List & play local media')
.argument('[media]', 'Media title/partial title')
.action(async title => {
const mediaList = (await adb(`find ${localMediaRootPath} -iname "*.mkv" -o -iname "*.mp4"`)).split('\n').map(path => parseMediaUrl(`file://${path}`));
2024-01-28 22:17:53 +00:00
if(!title)
return response = {
output: outdent `
📂 Local media :
${mediaList.map(item => `- \`${item.title}\``).join('\n')}
`
};
const
keywords = title.toLowerCase().split(' '),
2024-01-28 23:09:01 +00:00
matchingMediaList = mediaList.filter(item => keywords.every(keyword => item.title.toLowerCase().includes(keyword)));
2024-01-28 22:17:53 +00:00
if(matchingMediaList.length === 0)
return response = { embed: { title: `❌ Media not found` } };
if(matchingMediaList.length > 1)
return response = {
output: outdent `
🔎 Multiple media found :
${matchingMediaList.map(item => `- \`${item.title}\``).join('\n')}
`
};
const [matchingMedia] = matchingMediaList;
await adb(`am start -a android.intent.action.VIEW -n com.android.gallery3d/.app.MovieActivity -d "${matchingMedia.url}" -e MediaPlayerType 2`);
2024-01-28 22:17:53 +00:00
return response = { embed: { title: `▶️ \`${matchingMedia.title}\`` } };
});
2024-01-27 19:38:33 +00:00
program
.command('kiwi')
.description('Launch Kiwi browser')
.argument('[url]', 'Website URL')
.action(url => adb(url
? `am start -a android.intent.action.VIEW -d ${url} -t text/plain com.kiwibrowser.browser`
: 'monkey -p com.kiwibrowser.browser 1'
));
2024-01-27 21:25:29 +00:00
program
.command('notifications')
.description('Show notification history')
.action(async () => {
const items = [];
let item;
for(let line of (await adb('dumpsys notification --noredact')).split('\n')){
line = line.trim();
if(line.startsWith('NotificationRecord('))
items.push(item = { app: line.slice(35).split(' ')[0] });
if(line.startsWith('android.title='))
item.title = line.slice(line.indexOf('(') + 1, -1);
if(line.startsWith('android.text='))
item.text = line.slice(line.indexOf('(') + 1, -1);
if(line.startsWith('mCreationTimeMs='))
item.date = parseInt(line.slice(16));
}
items.sort((a, b) => b.date - a.date);
response = {
output: new AsciiTable()
.setHeading('App', 'Time', 'Content')
.addRowMatrix(items
.sort((a, b) => b.date - a.date)
.map(item => [
item.app,
2024-01-29 01:19:43 +00:00
dayjs(item.date).format('DD/MM HH:mm'),
2024-01-27 21:25:29 +00:00
`${item.title}${item.title.length + item.text.length < 50 ? ` · ${item.text}` : ''}`
])
).toString(),
isRaw: true
};
});
2024-01-27 03:54:37 +00:00
try {
await program.parseAsync(
2024-01-28 02:20:29 +00:00
parse(content),
2024-01-27 03:54:37 +00:00
{ from: 'user' }
);
}
catch(error){
2024-01-28 02:08:10 +00:00
if(!error.code?.startsWith?.('commander.'))
2024-01-27 03:54:37 +00:00
throw error;
}
return response;
};
eris.on(
'error',
error => console.error(error)
);
2022-07-11 09:34:15 +00:00
eris.on(
'messageCreate',
async message => {
2024-01-27 03:54:37 +00:00
const
{
guildID,
author: { id: authorID },
channel: { id: channelID },
id: messageID,
2024-01-28 02:20:29 +00:00
content,
attachments
2024-01-27 03:54:37 +00:00
} = message,
reply = async (content, isCodeBlock) => {
const lines = content.split('\n');
let chunk = isCodeBlock ? '```\n' : '';
for(let lineIndex = 0; lineIndex < lines.length; lineIndex++){
const
line = lines[lineIndex] + '\n',
canAddLine = chunk.length + line.length + (isCodeBlock ? 3 : 0) <= 2000;
if(canAddLine)
chunk += line;
if(!canAddLine || lineIndex === lines.length - 1){
if(isCodeBlock)
chunk += '```';
await eris.createMessage(
channelID,
{
content: chunk,
messageReference: { messageID }
}
);
2024-01-28 02:08:10 +00:00
chunk = isCodeBlock ? `\`\`\`\n${line}` : line;
2024-01-27 03:54:37 +00:00
}
}
};
if(message.author.bot) return;
2022-07-11 09:34:15 +00:00
if(
guildID
||
!botWhitelist.includes(authorID)
2024-01-27 03:54:37 +00:00
) return message.addReaction('❌');
await message.addReaction('⏳');
try {
const {
output,
2024-01-28 02:20:29 +00:00
isRaw,
embed
} = await handleCommand({
content,
authorID,
attachments
}) || {};
2024-01-27 03:54:37 +00:00
if(output)
await reply(output, isRaw);
2024-01-28 02:20:29 +00:00
if(embed)
await eris.createMessage(
channelID,
{
embed,
messageReference: { messageID }
}
);
2024-01-27 03:54:37 +00:00
await message.addReaction('✅');
}
catch(error){
console.error(error);
await message.addReaction('❌');
2022-07-11 09:34:15 +00:00
}
}
);
(async () => {
await Promise.all([
eris.connect(),
2024-01-27 22:51:29 +00:00
new Promise(resolve => eris.once('ready', resolve)),
new Promise(resolve => exec(`adb connect ${adbIp}:${adbPort}`, resolve))
2022-07-11 09:34:15 +00:00
]);
console.log('Ready');
})().catch(error => console.error(error));