first
This commit is contained in:
commit
0f56c052ae
|
@ -0,0 +1,3 @@
|
||||||
|
DISCORD_BOT_TOKEN=""
|
||||||
|
DISCORD_CLIENT_ID=""
|
||||||
|
DISCORD_GUILD_ID=""
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
/**/**/cache
|
||||||
|
/**/**/.env
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
@ -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)
|
||||||
|
})
|
Loading…
Reference in New Issue