remove radio, display video titles
BiRabittoh andronacomarco@gmail.com
Tue, 09 Jan 2024 11:17:24 +0100
9 files changed,
100 insertions(+),
94 deletions(-)
A
LICENSE
@@ -0,0 +1,21 @@
+MIT License + +Copyright (c) 2024 Marco Andronaco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
M
README.md
→
README.md
@@ -1,7 +1,7 @@
# Simple Discord Music Bot ### Why? -I wanted to experiment with [discord.js](). +I wanted to experiment with [discord.js](https://discord.js.org/). ### Will you add \<feature>? Nah, this is just a proof of concept.@@ -22,6 +22,11 @@ ```
npm run deploy-commands ``` This will also give you a link to invite the bot to any server. + +Build the project: +``` +npm run build +``` Finally, type this to start the bot: ```
M
config.json.example
→
config.json.example
@@ -7,12 +7,5 @@ { "name": "TheFatRat - Xenogenesis", "value": "https://www.youtube.com/watch?v=6N8zvi1VNSc" },
{ "name": "OMFG - Hello", "value": "https://www.youtube.com/watch?v=5nYVNTX0Ib8" }, { "name": "Pegboard Nerds - Disconnected", "value": "https://www.youtube.com/watch?v=YdBtx8qG68w" }, { "name": "Gym Class Heroes - Stereo Hearts", "value": "https://www.youtube.com/watch?v=ThctmvQ3NGk" } - ], - "radios": - [ - { "name": "EusariRadio", "value": "https://onair7.xdevel.com/proxy/xautocloud_cnou_1049?mp=/;stream/" }, - { "name": "Radio 24", "value": "https://shoutcast3.radio24.ilsole24ore.com/stream.mp3" }, - { "name": "Radio Delfino", "value": "https://nr8.newradio.it/proxy/emaamo00?mp=/stream?ext=.mp3" }, - { "name": "Radio 105", "value": "https://icy.unitedradio.it/Radio105.mp3" } ] }
M
src/commands/play.ts
→
src/commands/play.ts
@@ -1,6 +1,27 @@
-import { BaseInteraction, ChatInputCommandInteraction, CommandInteraction, SlashCommandBuilder } from 'discord.js'; -import play from 'play-dl'; -import { playUrls, getChannel } from '../functions/music'; +import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import play, { YouTubeVideo } from 'play-dl'; +import { playUrls, getChannel, formatTitle } from '../functions/music'; + +async function handleUserInput(input: string): Promise<YouTubeVideo[]> { + switch (play.yt_validate(input)) { + case 'video': + const info = await play.video_basic_info(input); + return [info.video_details]; + case 'search': + const results = await play.search(input, { source: { youtube: 'video' }, limit: 1 }); + + if (results.length == 0) + return []; + + const firstResult = results[0]; + return [firstResult]; + case 'playlist': + const playlist = await play.playlist_info(input, { incomplete: true }); + return await playlist.all_videos(); + default: + return []; + } +} module.exports = { data: new SlashCommandBuilder()@@ -19,35 +40,18 @@ return await interaction.reply({ content: channel, ephemeral: true });
await interaction.deferReply(); const opt = interaction.options; - const url = opt.getString('query'); - - let video, yt_info; - switch (play.yt_validate(url)) { - case 'video': - playUrls([url], channel); - return await interaction.editReply(`Added ${url} to queue.`); - - case 'search': - yt_info = await play.search(url, { source: { youtube: 'video' }, limit: 1 }); + const input = opt.getString('query'); - if (yt_info.length === 0) - return await interaction.editReply('No results found.'); + const yt_videos = await handleUserInput(input); + const added = await playUrls(yt_videos, channel); - video = yt_info[0]; - playUrls([video.url], channel); - return await interaction.editReply(`Added ${video.url} to queue.`); - - case 'playlist': - const playlist = await play.playlist_info(url, { incomplete : true }); - const videos = await playlist.all_videos(); - const urls = videos.map((e) => e.url); - const result = await playUrls(urls, channel); - if (result) - return await interaction.editReply(`Added ${urls.length} videos from the following playlist: ${playlist.title}.`); - else - return await interaction.editReply(`Could not add playlist.`); + switch (added.length) { + case 0: + return await interaction.editReply('No videos were added to the queue.'); + case 1: + return await interaction.editReply(`Added ${formatTitle(added[0])} to queue.`); default: - return await interaction.editReply('Not supported.'); + return await interaction.editReply(`Added ${added.length} videos to queue.`); } }, };
M
src/commands/queue.ts
→
src/commands/queue.ts
@@ -1,21 +1,22 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; -import { getChannel, getQueue } from '../functions/music'; +import { formatTitle, getChannel, getQueue } from '../functions/music'; +import { YouTubeVideo } from 'play-dl'; const CHARACTER_LIMIT_API = 2000; -function getReply(result: string[]): string { - const nowPlaying = "Now playing: " + result.shift() +function getReply(result: YouTubeVideo[]): string { + const nowPlaying = "Now playing: " + formatTitle(result.shift()); if (!result.length) { return nowPlaying; } let reply = "Queue:" - let new_string = ""; const characterLimit = CHARACTER_LIMIT_API - nowPlaying.length - 6; // 4 chars for "\n...", 2 chars for "\n\n" for (let r in result) { - new_string = "\n" + (r + 1) + ". <" + result[r] + ">"; + const video = result[r]; + const new_string = `\n${r + 1}. ${formatTitle(video)}`; if (reply.length + new_string.length > characterLimit) { reply += "\n..."; break;
D
src/commands/radio.ts
@@ -1,29 +0,0 @@
-import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; -import { playStream, getChannel } from '../functions/music'; -import path from 'node:path'; -const { radios } = require(path.join(process.cwd(), 'config.json')); - -module.exports = { - data: new SlashCommandBuilder() - .setName('radio') - .setDescription('Play a custom-defined webradio URL.') - .addStringOption(option => - option.setName('which') - .setDescription('Select which radio to play') - .setRequired(false) - .addChoices(...radios)), - - async execute(interaction: ChatInputCommandInteraction) { - const channel = await getChannel(interaction); - if (typeof channel == 'string') - return await interaction.reply({ content: channel, ephemeral: true }); - - await interaction.deferReply(); - - const radio = interaction.options.getString('which'); - const streamUrl = radio ? radio : radios[0].value; - - playStream(streamUrl, channel); - return await interaction.editReply('Playing web radio.'); - }, -};
M
src/functions/music.ts
→
src/functions/music.ts
@@ -1,9 +1,14 @@
-import { createAudioResource, joinVoiceChannel, StreamType } from '@discordjs/voice'; -import { ChatInputCommandInteraction, GuildMember, VoiceBasedChannel } from 'discord.js' +import { joinVoiceChannel } from '@discordjs/voice'; +import { ChatInputCommandInteraction, VoiceBasedChannel } from 'discord.js' import MyQueue from './myqueue'; +import { YouTubeVideo } from 'play-dl'; const q = new MyQueue(); +export function formatTitle(video: YouTubeVideo): string { + return `**${video.title}** (\`${video.durationRaw}\`)` +} + export function getChannelConnection(channel: VoiceBasedChannel) { const guild = channel.guild; return joinVoiceChannel({@@ -13,16 +18,16 @@ adapterCreator: guild.voiceAdapterCreator
}); } -export async function playUrls(urls: string[], channel: VoiceBasedChannel): Promise<boolean> { +export async function playUrls(videos: YouTubeVideo[], channel: VoiceBasedChannel): Promise<YouTubeVideo[]> { if (!channel) { console.log('Channel error:', channel); return; } q.connection = getChannelConnection(channel); - return await q.addArray(urls); + return await q.addArray(videos); } - +/* Only useful for radio export async function playStream(url: string, channel: VoiceBasedChannel) { if (!channel) { console.log('Channel error:', channel);@@ -31,6 +36,7 @@ }
q.connection = getChannelConnection(channel); q.add(createAudioResource(url, { inputType: StreamType.Opus })); } +*/ export async function playOutro(url: string, channel: VoiceBasedChannel) { if (!channel) {
M
src/functions/myqueue.ts
→
src/functions/myqueue.ts
@@ -1,14 +1,14 @@
import { createAudioResource, createAudioPlayer, NoSubscriberBehavior, AudioPlayerStatus, VoiceConnection, AudioPlayer } from '@discordjs/voice'; -import play from 'play-dl'; +import play, { YouTubeVideo } from 'play-dl'; -async function resourceFromUrl(url) { +async function resourceFromYTUrl(url: string) { const stream = await play.stream(url); return createAudioResource(stream.stream, { inputType: stream.type }) } export default class MyQueue { - #nowPlaying = null; - #queue = Array(); + #nowPlaying: YouTubeVideo; + #queue: Array<YouTubeVideo>; connection: VoiceConnection; player: AudioPlayer; static _instance: MyQueue;@@ -24,20 +24,19 @@ return MyQueue._instance
} MyQueue._instance = this; - this.#nowPlaying = ""; + this.#nowPlaying = null; this.connection = null; - this.#queue = Array(); + this.#queue = Array<YouTubeVideo>(); this.player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Stop } }); this.player.on(AudioPlayerStatus.Idle, () => { if (this.#queue.length > 0) return this.next(); - //@ts-expect-error - this.player.subscribers.forEach((e) => e.connection.disconnect()); + this.connection.disconnect(); }); } clear() { - this.#queue = Array(); + this.#queue = Array<YouTubeVideo>(); } stop() {@@ -51,23 +50,23 @@ async next() {
if (this.#queue.length == 0) return this.stop(); this.#nowPlaying = this.#queue.shift(); - const resource = await resourceFromUrl(this.#nowPlaying); + const resource = await resourceFromYTUrl(this.#nowPlaying.url); this.player.play(resource); this.connection.subscribe(this.player); } - add(url, position=this.#queue.length) { + add(video: YouTubeVideo, position=this.#queue.length) { const l = this.#queue.length; const normalizedPosition = position % (l + 1); - this.#queue.splice(normalizedPosition, 0, url); + this.#queue.splice(normalizedPosition, 0, video); if (l == 0) this.next(); } - async addArray(urls) { + async addArray(urls: YouTubeVideo[]) { const l = this.#queue.length; this.#queue.push(...urls); if (l == 0 && this.player.state.status == AudioPlayerStatus.Idle) this.next(); - return l != this.#queue.length; + return urls; } pause() {@@ -79,9 +78,9 @@ this.player.unpause();
this.connection.subscribe(this.player); } - async outro(url) { + async outro(url: string) { this.player.pause(); - const resource = await resourceFromUrl(url); + const resource = await resourceFromYTUrl(url); const p = createAudioPlayer(); p.on(AudioPlayerStatus.Idle, () => {@@ -89,8 +88,7 @@ if (this.player.state.status == AudioPlayerStatus.Paused) {
this.resume(); return } - //@ts-expect-error - p.subscribers.forEach((e) => e.connection.disconnect()); + this.connection.disconnect(); }); p.play(resource);
M
src/types.d.ts
→
src/types.d.ts
@@ -1,10 +1,17 @@
import { SlashCommandBuilder, Collection, AutocompleteInteraction, ChatInputCommandInteraction } from "discord.js" +import { YouTubeStream } from "play-dl" export interface SlashCommand { command: SlashCommandBuilder, execute: (interaction : ChatInputCommandInteraction) => void, autocomplete?: (interaction: AutocompleteInteraction) => void, cooldown?: number // in seconds +} + +export interface YTVideo { + name: string, + url: string, + stream?: YouTubeStream, } declare module "discord.js" {