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