diff --git a/src/commands/forceplay.ts b/src/commands/forceplay.ts new file mode 100644 index 0000000..4a099f0 --- /dev/null +++ b/src/commands/forceplay.ts @@ -0,0 +1,36 @@ +import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { connectToChannelByInteraction } from '../util/helpers'; +import { forceRequestSong } from '../playback'; +import { getPlayMsg } from '../messages'; + +const name = "forceplay" + +function register() { + return new SlashCommandBuilder() + .setName(name) + .setDescription('YT') + .addStringOption((option) => option.setName("url") + .setDescription("YT url") + .setRequired(true)) +} + +async function execute(interaction: ChatInputCommandInteraction) { + try { + connectToChannelByInteraction(interaction) + + const url = interaction.options.getString("url")!; + await forceRequestSong(interaction, url) + + const msg = getPlayMsg(url) + await interaction.reply(msg); + } catch (error) { + console.error(error); + await interaction.reply('Coś poszło nie tak :/'); + } +} + +export default { + name, + register, + execute +} diff --git a/src/commands/index.ts b/src/commands/index.ts index 77662c9..9ef53bd 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,8 +1,20 @@ +import forceplay from "./forceplay" +import help from "./help" +import pause from "./pause" import ping from "./ping" import play from "./play" +import queue from "./queue" +import resume from "./resume" +import stop from "./stop" -export const commands: {[key: string]: Command} = { +export const commands: { [key: string]: Command } = { ping, play, + forceplay, + queue, + help, + resume, + stop, + pause, } diff --git a/src/commands/join.ts b/src/commands/join.ts deleted file mode 100644 index b9e74c4..0000000 --- a/src/commands/join.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; -import { connectToChannel } from '../util/helpers'; -import { joinVoiceChannel } from '@discordjs/voice'; - -const name = "ping" - -function register() { - return new SlashCommandBuilder() - .setName(name) - .setDescription('Replies with Pong!') -} - -async function execute(message: ChatInputCommandInteraction) { - const channelId = "515300847790325790" -} - -export default { - name, - register, - execute -} diff --git a/src/commands/pause.ts b/src/commands/pause.ts new file mode 100644 index 0000000..f11e710 --- /dev/null +++ b/src/commands/pause.ts @@ -0,0 +1,27 @@ +import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { pause_playback } from '../playback'; +import { player } from '../main'; + + +const name = "pause" + +function register() { + return new SlashCommandBuilder() + .setName(name) + .setDescription('pause playback') +} + +async function execute(interaction: ChatInputCommandInteraction) { + try { + pause_playback(player) + } catch (error) { + console.error(error); + await interaction.reply('Coś poszło nie tak :/'); + } +} + +export default { + name, + register, + execute +} diff --git a/src/commands/play.ts b/src/commands/play.ts index abb4e3a..5320072 100644 --- a/src/commands/play.ts +++ b/src/commands/play.ts @@ -1,7 +1,7 @@ import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; -import { connectToChannel, playSong } from '../util/helpers'; -import { player } from '../main'; -import { get_audio } from '../util/downloader'; +import { connectToChannelByInteraction } from '../util/helpers'; +import { requestSong } from '../playback'; +import { getPlayMsg } from '../messages'; const name = "play" @@ -16,27 +16,14 @@ function register() { async function execute(interaction: ChatInputCommandInteraction) { try { - // await interaction.reply(`${interaction}`); - const member = await interaction.guild?.members.fetch(interaction.user.id); - if (!member) throw new Error("ale chuj") - const voiceChannel = member.voice.channel; - if (!voiceChannel) throw new Error("ale chuj 2") + connectToChannelByInteraction(interaction) - const connection = await connectToChannel(voiceChannel); - connection.subscribe(player); - - await interaction.reply('https://gitea.papryk.com/Papryk/dj-spangebob pull requesty milewidziane XD'); const url = interaction.options.getString("url")!; - const path = await get_audio(url) - // const path = "/home/patryk/Papryk/dbot/data/Kino - Summer is ending ⧸ Кино - Кончится лето [6VqiMQoMXmw].opus" - console.log(path) - - await playSong(player, path); + await requestSong(interaction, url) + const msg = getPlayMsg(url) + await interaction.reply(msg); } catch (error) { - /** - * The song isn't ready to play for some reason :( - */ console.error(error); await interaction.reply('Coś poszło nie tak :/'); } diff --git a/src/commands/queue.ts b/src/commands/queue.ts new file mode 100644 index 0000000..fe206d6 --- /dev/null +++ b/src/commands/queue.ts @@ -0,0 +1,24 @@ +import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { getQueue } from '../playback'; + +const name = "queue" + +function register() { + return new SlashCommandBuilder() + .setName(name) + .setDescription('Shows queue') +} + +async function execute(interaction: ChatInputCommandInteraction) { + console.log(interaction.member) + const queue = getQueue() + await interaction.reply(`Current: ${queue.current} +Queue: + ${queue.songList.join('\n ')}`); +} + +export default { + name, + register, + execute +} diff --git a/src/commands/resume.ts b/src/commands/resume.ts new file mode 100644 index 0000000..577006b --- /dev/null +++ b/src/commands/resume.ts @@ -0,0 +1,27 @@ +import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { resume_playback } from '../playback'; +import { player } from '../main'; + + +const name = "resume" + +function register() { + return new SlashCommandBuilder() + .setName(name) + .setDescription('resume playback') +} + +async function execute(interaction: ChatInputCommandInteraction) { + try { + resume_playback(player) + } catch (error) { + console.error(error); + await interaction.reply('Coś poszło nie tak :/'); + } +} + +export default { + name, + register, + execute +} diff --git a/src/commands/stop.ts b/src/commands/stop.ts new file mode 100644 index 0000000..8ce374e --- /dev/null +++ b/src/commands/stop.ts @@ -0,0 +1,27 @@ +import { CacheType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { stop_playback } from '../playback'; +import { player } from '../main'; + + +const name = "stop" + +function register() { + return new SlashCommandBuilder() + .setName(name) + .setDescription('stop playback') +} + +async function execute(interaction: ChatInputCommandInteraction) { + try { + stop_playback(player) + } catch (error) { + console.error(error); + await interaction.reply('Coś poszło nie tak :/'); + } +} + +export default { + name, + register, + execute +} diff --git a/src/global.d.ts b/src/global.d.ts index 1d9036f..e4ee251 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -3,3 +3,8 @@ type Command = { register: () => any, execute: (interaction: ChatInputCommandInteraction) => Promise, } + +type Queue = { + songList: string[], + current: string | null, +} diff --git a/src/main.ts b/src/main.ts index c2d8527..077df2f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,12 +2,19 @@ import { Client, Events, GatewayIntentBits, MessageFlags, REST, Routes } from 'discord.js'; import dotenv from 'dotenv'; import { commands } from "./commands"; -import { createAudioPlayer } from '@discordjs/voice'; +import { AudioPlayerState, createAudioPlayer } from '@discordjs/voice'; import { connectToChannel, playSong } from './util/helpers'; +import { updatePlayer } from './playback'; dotenv.config() +export const DATA_DIR = "./data"; + // AUDIO export const player = createAudioPlayer(); +player.on('stateChange', (oldState: AudioPlayerState, newState: AudioPlayerState) => { + updatePlayer() +}); + // Create a new client instance const client = new Client({ @@ -29,11 +36,17 @@ async function registerCommands() { const rest = new REST().setToken(process.env.DISCORD_TOKEN!); const registry = Object.values(commands).map((cmd) => cmd.register().toJSON()) console.log(registry) - const data = await rest.put(Routes.applicationCommands( + await rest.put(Routes.applicationGuildCommands( process.env.DISCORD_APP_ID!, + process.env.GUILD_ID! ), { body: registry }); + // await rest.put(Routes.applicationCommands( + // process.env.DISCORD_APP_ID!, + // ), { + // body: registry + // }); } diff --git a/src/messages.ts b/src/messages.ts new file mode 100644 index 0000000..8c9bc5b --- /dev/null +++ b/src/messages.ts @@ -0,0 +1,17 @@ +import { getQueue } from "./playback"; + +export function getPlayMsg(url: string): string { + return ` + Request: ${url} + https://gitea.papryk.com/Papryk/dj-spangebob pull requesty milewidziane XD\n` + getQueueMsg() +} + +export function getCurrentSongMsg(): string { + return 'TODO-1' +} +export function getQueueMsg(): string { + const queue = getQueue() + return `Current: ${queue.current} +Queue: + ${queue.songList.join('\n ')}` +} diff --git a/src/playback.ts b/src/playback.ts new file mode 100644 index 0000000..8f06d03 --- /dev/null +++ b/src/playback.ts @@ -0,0 +1,57 @@ +import { AudioPlayer, AudioPlayerStatus } from "@discordjs/voice"; +import { player } from "./main"; +import { CacheType, ChatInputCommandInteraction } from "discord.js"; +import { getAudioFile } from "./util/downloader"; +import { playSong } from "./util/helpers"; + +const queue: Queue = { + songList: [], + current: null +} + +export async function requestSong( + interaction: ChatInputCommandInteraction, + url: string) { + + const path = await getAudioFile(url) + queue.songList.push(path) + updatePlayer() + +} + +export async function forceRequestSong( + interaction: ChatInputCommandInteraction, + url: string) { + + const path = await getAudioFile(url) + queue.songList.push(path) + playSong(player, path); +} + + +export async function updatePlayer() { + if (player.state.status === AudioPlayerStatus.Idle) { + const nextSong = queue.songList.shift() + if (!nextSong) { + queue.current = null + return + }; + playSong(player, nextSong); + } +} + +export function pause_playback(player: AudioPlayer) { + player.pause() +} + +export function stop_playback(player: AudioPlayer) { + player.stop() +} + +export function resume_playback(player: AudioPlayer) { + player.unpause() +} + +export function getQueue(): Queue { + return queue +} diff --git a/src/util/downloader.ts b/src/util/downloader.ts index a0d340e..06e01af 100644 --- a/src/util/downloader.ts +++ b/src/util/downloader.ts @@ -1,51 +1,65 @@ import { spawn } from "node:child_process"; +import { DATA_DIR } from "../main"; -export async function get_audio(url: string): Promise { - const ytDlpBin = process.env.YT_DLP_BIN_PATH! ?? "yt-dlp"; - const dataDir = "./data"; +export async function getAudioFile(url: string): Promise { + const id = extractId(url) - const idMatch = /[?&]v=([a-zA-Z0-9_-]{11})/.exec(url); - if (!idMatch) throw new Error("Cannot extract video ID"); - - const id = idMatch[1]; - console.log(`ID: ${id}`); - - // ---- run yt-dlp ---- - await new Promise((resolve, reject) => { - const p = spawn(ytDlpBin, [ - "-x", - "--audio-format", "opus", - "--audio-quality", "0", - "--no-playlist", - "--paths", dataDir, - url, - ]); - - p.stderr.on("data", d => process.stderr.write(d)); - p.on("close", code => code === 0 ? resolve() : reject(new Error("yt-dlp failed"))); - }); - - // ---- find the file ---- - return await new Promise((resolve, reject) => { - const find = spawn("find", [ - dataDir, - "-type", "f", - "-iname", `*${id}*.opus`, - "-exec", "readlink", "-f", "{}", ";", - ]); - - let out = ""; - - find.stdout.on("data", d => out += d.toString()); - find.stderr.on("data", d => process.stderr.write(d)); - - find.on("close", code => { - if (code !== 0 || !out.trim()) { - reject(new Error("Audio file not found")); - } else { - resolve(out.trim()); - } - }); - }); + let path: string; + try { + path = await findFileById(id) + } catch (e) { + await downloadYTVideo(url) + path = await findFileById(id) + } + return path +} + +export function extractId(url: string): string { + const idMatch = /[?&]v=([a-zA-Z0-9_-]{11})/.exec(url); + if (!idMatch) throw new Error("Cannot extract video ID"); + const id = idMatch[1]; + console.log(`ID: ${id}`); + return id +} + +export async function downloadYTVideo(url: string): Promise { + const ytDlpBin = process.env.YT_DLP_BIN_PATH! ?? "yt-dlp"; + await new Promise((resolve, reject) => { + const p = spawn(ytDlpBin, [ + "-x", + "--audio-format", "opus", + "--audio-quality", "0", + "--no-playlist", + "--paths", DATA_DIR, + url, + ]); + + p.stderr.on("data", d => process.stderr.write(d)); + p.on("close", code => code === 0 ? resolve() : reject(new Error("yt-dlp failed"))); + }); +} + +export async function findFileById(id: string): Promise { + return await new Promise((resolve, reject) => { + const find = spawn("find", [ + DATA_DIR, + "-type", "f", + "-iname", `*${id}*.opus`, + "-exec", "readlink", "-f", "{}", ";", + ]); + + let out = ""; + + find.stdout.on("data", d => out += d.toString()); + find.stderr.on("data", d => process.stderr.write(d)); + + find.on("close", code => { + if (code !== 0 || !out.trim()) { + reject(new Error("Audio file not found")); + } else { + resolve(out.trim()); + } + }); + }); } diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 97ff2cb..cf2ca40 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -7,8 +7,18 @@ import { joinVoiceChannel, type AudioPlayer, } from '@discordjs/voice'; -import type { VoiceBasedChannel } from 'discord.js'; +import type { CacheType, ChatInputCommandInteraction, VoiceBasedChannel } from 'discord.js'; import { createDiscordJSAdapter } from './adapter.js'; +import { player } from '../main.js'; + +export async function connectToChannelByInteraction(interaction: ChatInputCommandInteraction): Promise { + const member = await interaction.guild?.members.fetch(interaction.user.id); + if (!member) throw new Error("ale chuj") + const voiceChannel = member.voice.channel; + if (!voiceChannel) throw new Error("ale chuj 2") + const connection = await connectToChannel(voiceChannel); + connection.subscribe(player); +} export async function connectToChannel(channel: VoiceBasedChannel) { /**