This commit is contained in:
srgooglo 2022-10-08 23:09:12 +02:00
commit 0f56c052ae
6 changed files with 4789 additions and 0 deletions

3
.env-example Normal file
View File

@ -0,0 +1,3 @@
DISCORD_BOT_TOKEN=""
DISCORD_CLIENT_ID=""
DISCORD_GUILD_ID=""

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
/**/**/cache
/**/**/.env

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "sus-bot",
"version": "0.0.1",
"scripts": {
"dev": "corenode-node ./src/index.js"
},
"dependencies": {
"audic": "^3.0.1",
"axios": "^1.1.2",
"discord.js": "^14.5.0",
"dotenv": "16.0.3",
"play-sound": "^1.1.5",
"youtube-dl-exec": "^2.1.6",
"ytdl-core": "^4.11.2"
},
"devDependencies": {
"corenode": "^0.28.26"
}
}

26
src/deploy-commands.js Normal file
View File

@ -0,0 +1,26 @@
require("dotenv").config()
const { SlashCommandBuilder, Routes } = require("discord.js")
const { REST } = require("@discordjs/rest")
const PlayCommand = new SlashCommandBuilder()
.setName("playcam")
.setDescription("Play a song on the worstest camera ever!")
.addStringOption(option => option.setName("url").setDescription("URL of the song to play").setRequired(true))
.addStringOption(option => option.setName("start").setDescription("Set start time").setRequired(false))
.addStringOption(option => option.setName("volume").setDescription("Volume of the song to play").setRequired(false))
const StopCommand = new SlashCommandBuilder()
.setName("playcamstop")
.setDescription("Stop playing a song on the worstest camera ever!")
const commands = [
PlayCommand,
StopCommand
].map(command => command.toJSON())
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_BOT_TOKEN)
rest.put(Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, process.env.DISCORD_GUILD_ID), { body: commands })
.then(() => console.log("Successfully registered application commands."))
.catch(console.error)

247
src/index.js Normal file
View File

@ -0,0 +1,247 @@
require("dotenv").config()
import fs from "fs"
import path from "path"
import discordjs from "discord.js"
import child_process from "child_process"
import axios from "axios"
import playSound from "play-sound"
const cachePath = path.join(process.cwd(), "cache")
const youtubeIDRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?([a-zA-Z0-9_-]{11})/
const maxSoundTime = 10000
const player = playSound({
player: "mpg321",
})
let players = []
function applyStartTimeToEndTime(start, maxTime) {
// convert hh:mm string to milliseconds
const startMilliseconds = start.split(":").reverse().reduce((prev, curr, i) => prev + curr * Math.pow(60, i), 0) * 1000
const fixedTime = startMilliseconds + maxTime
// convert milliseconds to hh:mm string
const end = new Date(fixedTime).toISOString().substr(11, 8)
return end
}
async function processURLToAudio({ url, startTime }) {
if (!url) {
throw new Error("No URL provided")
}
let inputFileOutputPath = path.join(cachePath, `${Date.now()}-unprocessed`)
// check if url is youtube
const youtubeMatch = url.match(youtubeIDRegex)
if (youtubeMatch) {
console.log("Youtube Matched, starting download...")
// update status
const youtubeID = youtubeMatch[1]
inputFileOutputPath = path.resolve(cachePath, `${youtubeID}.webm`)
// check if video is already downloaded
if (!fs.existsSync(inputFileOutputPath)) {
const youtubeDownloadProcess = child_process.spawn("yt-dlp", [
youtubeID,
"-o",
inputFileOutputPath,
], {
stdio: "inherit",
})
await new Promise((resolve, reject) => {
youtubeDownloadProcess.on("close", (code) => {
if (code === 0) {
resolve()
} else {
reject()
}
})
})
}
} else {
// download file
const file = await axios.get(url, {
responseType: "stream",
})
file.data.pipe(fs.createWriteStream(inputFileOutputPath))
await new Promise((resolve, reject) => {
file.data.on("end", resolve)
file.data.on("error", reject)
})
}
const audioOutputPath = path.resolve(cachePath, `${inputFileOutputPath}.mp3`)
// check if audio is already processed
if (fs.existsSync(audioOutputPath)) {
fs.unlinkSync(audioOutputPath)
}
const endTime = applyStartTimeToEndTime(startTime, maxSoundTime)
console.log("Starting audio processing...")
console.log("Input file:", inputFileOutputPath)
console.log("Output file:", audioOutputPath)
console.log(`Starting Time: ${startTime} / To: ${endTime}`)
// process audio (select from start time)
const ffmpegProcess = child_process.spawn("ffmpeg", [
"-i",
inputFileOutputPath,
"-ss",
startTime,
"-to",
endTime,
"-acodec",
"libmp3lame",
"-ac",
"2",
"-ab",
"128k",
"-f",
"mp3",
audioOutputPath,
], {
stdio: "inherit",
})
await new Promise((resolve, reject) => {
ffmpegProcess.on("close", (code) => {
if (code === 0) {
resolve()
} else {
reject()
}
})
})
return audioOutputPath
}
function killFromPlayers(id) {
const player = players.find(player => player.id === id)
if (player) {
player.instance.kill()
}
}
const botCommands = {
"playcamstop": async (interaction) => {
players.forEach(player => player.instance.kill())
return interaction.reply("All players stopped")
},
"playcam": async (interaction) => {
// check if is currently playing
const userId = interaction.user.id
const url = interaction.options.getString("url")
const startTime = interaction.options.getString("start") ?? "00:00"
const volume = interaction.options.getString("volume") ?? "30"
await interaction.reply({ content: "Processing...", ephemeral: true })
const resultPath = await processURLToAudio({
url,
startTime,
}).catch((err) => {
console.error(err)
return false
})
if (!resultPath) {
return interaction.editReply("Failed to process audio")
}
await interaction.editReply({
content: `🔊 Playing audio [${url}] at volume ${volume}%`,
ephemeral: true
})
console.log(`🔊 Playing [${resultPath}] at volume ${volume}%`)
const newPlayerId = ` ${userId}-${Date.now()}`
const newPlayer = {
id: newPlayerId,
instance: player.play(resultPath, {
afplay: ['-v', volume],
// fix start time and volume
mpg123: [
"-k",
startTime,
"-v",
volume,
],
}, (err) => {
killFromPlayers(newPlayerId)
if (err && !err.killed) {
console.error(err)
}
console.log("Finished playing audio")
return interaction.editReply({
content: "🔊 Done playing audio",
ephemeral: true,
})
})
}
players.push(newPlayer)
setTimeout(() => {
killFromPlayers(newPlayerId)
}, maxSoundTime)
}
}
async function initializeDiscordBot() {
const client = new discordjs.Client({
intents: [discordjs.GatewayIntentBits.Guilds],
partials: ["MESSAGE", "CHANNEL", "REACTION"]
})
client.on("ready", () => {
console.log("Discord bot ready")
})
client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand()) return
const { commandName } = interaction
if (commandName in botCommands) {
await botCommands[commandName](interaction)
}
})
await client.login(process.env.DISCORD_BOT_TOKEN)
}
async function main() {
if (!fs.existsSync(cachePath)) {
fs.mkdirSync(cachePath)
}
await initializeDiscordBot()
}
main().catch((err) => {
console.error(`[FATAL ERROR] >`, err)
process.exit(1)
})

4491
yarn.lock Normal file

File diff suppressed because it is too large Load Diff