From 121e829e59cf6af9743d65e92595469178f6f0b2 Mon Sep 17 00:00:00 2001 From: Struck713 Date: Sun, 17 Sep 2023 20:26:56 -0400 Subject: [PATCH] Added progress bar --- src/app.ts | 5 ++- src/commands/voice/play.ts | 2 +- src/commands/voice/queue.ts | 10 +++++- src/commands/voice/skip.ts | 2 +- src/lib/utils/html.ts | 33 ------------------- src/lib/utils/misc.ts | 64 +++++++++++++++++++++++++++++++++++++ src/lib/utils/youtube.ts | 40 ++++++++++++----------- src/lib/voice.ts | 9 ++---- 8 files changed, 102 insertions(+), 63 deletions(-) delete mode 100644 src/lib/utils/html.ts create mode 100644 src/lib/utils/misc.ts diff --git a/src/app.ts b/src/app.ts index a3fe353..3f395cb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,10 +1,13 @@ import { Client, Events, GatewayIntentBits } from "discord.js"; -import { TOKEN, BRUH } from '../config.json'; +import { YOUTUBE, TOKEN, BRUH } from '../config.json'; import { Commands } from "./lib/command"; import { VoiceManager } from "./lib/voice"; import { StateManager } from "./lib/state"; import { BruhListener } from "./lib/listeners/bruh"; +import YTDlpWrap from "yt-dlp-wrap"; +export const ytdl = new YTDlpWrap(YOUTUBE.BINARY); + export const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent ] }); export const voiceManager = new VoiceManager(); export const stateManager = new StateManager(); diff --git a/src/commands/voice/play.ts b/src/commands/voice/play.ts index e81d9f0..573aaf3 100644 --- a/src/commands/voice/play.ts +++ b/src/commands/voice/play.ts @@ -38,7 +38,7 @@ export const Play: Command = { connection = voiceManager.join(user.voice.channel); connection.play(metadata); - await Embeds.send(interaction, embed => embed.setAuthor({ name: "Now playing "}) + await Embeds.send(interaction, embed => embed.setAuthor({ name: "Now Playing"}) .setTitle(metadata.getTitle()) .setURL(metadata.getUrl()) .setDescription(`by ${metadata.getAuthor()}`) diff --git a/src/commands/voice/queue.ts b/src/commands/voice/queue.ts index f15ace3..291512a 100644 --- a/src/commands/voice/queue.ts +++ b/src/commands/voice/queue.ts @@ -2,6 +2,7 @@ import { Colors, EmbedBuilder, SlashCommandBuilder } from "discord.js"; import { voiceManager } from "../../app"; import { Command } from "../../lib/command"; import { Embeds } from "../../lib/utils/embeds"; +import { Time } from "../../lib/utils/misc"; export const Queue: Command = { data: new SlashCommandBuilder() @@ -28,15 +29,22 @@ export const Queue: Command = { let { playing, queue } = connection; if (playing) { + + let bar = Time.progress(15, connection.resource?.playbackDuration, playing.getDuration() * 1000); let embed = Embeds.create() .setAuthor({ name: 'Now Playing' }) .setTitle(playing.getTitle()) .setURL(playing.getUrl()) - .setDescription(`by ${playing.getAuthor()}`) + .setDescription(` + by ${playing.getAuthor()} \n + ${bar} ${Time.format(connection.resource?.playbackDuration)}/${Time.format(playing.getDuration(), "seconds")} + `) .setThumbnail(playing.getThumbnailUrl()) .addFields({ name: '\u200B', value: 'Next in the queue:' }) + if (queue.length > 0) embed.addFields(queue.slice(0, Math.min(8, queue.length)).map((metadata, index) => ({ name: `${index + 2}. ${metadata.getTitle()}`, value: `by ${metadata.getAuthor()}` }))); else embed.addFields({ name: 'There is nothing next in the queue.', value: '\u200B' }) + await Embeds.send(interaction, () => embed); return; } diff --git a/src/commands/voice/skip.ts b/src/commands/voice/skip.ts index d797829..67199d5 100644 --- a/src/commands/voice/skip.ts +++ b/src/commands/voice/skip.ts @@ -26,7 +26,7 @@ export const Skip: Command = { } let metadata = connection.skip(); - await Embeds.send(interaction, embed => embed.setAuthor({ name: "Skipped - Now playing"}) + await Embeds.send(interaction, embed => embed.setAuthor({ name: "Skipped - Now Playing"}) .setTitle(metadata.getTitle()) .setURL(metadata.getUrl()) .setDescription(`by ${metadata.getAuthor()}`) diff --git a/src/lib/utils/html.ts b/src/lib/utils/html.ts deleted file mode 100644 index ecdfc11..0000000 --- a/src/lib/utils/html.ts +++ /dev/null @@ -1,33 +0,0 @@ -export namespace Entities { - - export enum HTML_ENTITIES { - nbsp = ' ', - cent = '¢', - pound = '£', - yen = '¥', - euro = '€', - copy = '©', - reg = '®', - lt = '<', - gt = '>', - quot = '"', - amp = '&', - apos = '\'' - }; - - export const decodeEntities = (str: string) => { - return str.replace(/\&([^;]+);/g, (entity, entityCode) => { - var match; - if (entityCode in HTML_ENTITIES) { - return HTML_ENTITIES[entityCode as keyof typeof HTML_ENTITIES]; - } else if (match = entityCode.match(/^#x([\da-fA-F]+)$/)) { - return String.fromCharCode(parseInt(match[1], 16)); - } else if (match = entityCode.match(/^#(\d+)$/)) { - return String.fromCharCode(~~match[1]); - } else { - return entity; - } - }); - }; - -} \ No newline at end of file diff --git a/src/lib/utils/misc.ts b/src/lib/utils/misc.ts new file mode 100644 index 0000000..3d5d2cf --- /dev/null +++ b/src/lib/utils/misc.ts @@ -0,0 +1,64 @@ +import { join } from "path"; + +export namespace Entities { + + export enum HTML_ENTITIES { + nbsp = ' ', + cent = '¢', + pound = '£', + yen = '¥', + euro = '€', + copy = '©', + reg = '®', + lt = '<', + gt = '>', + quot = '"', + amp = '&', + apos = '\'' + }; + + export const decodeEntities = (str: string) => { + return str.replace(/\&([^;]+);/g, (entity, entityCode) => { + var match; + if (entityCode in HTML_ENTITIES) { + return HTML_ENTITIES[entityCode as keyof typeof HTML_ENTITIES]; + } else if (match = entityCode.match(/^#x([\da-fA-F]+)$/)) { + return String.fromCharCode(parseInt(match[1], 16)); + } else if (match = entityCode.match(/^#(\d+)$/)) { + return String.fromCharCode(~~match[1]); + } else { + return entity; + } + }); + }; + +} + +export namespace Time { + + export const PROGRESS_UNFILLED = ":white_large_square:"; + export const PROGRESS_FILLED = ":yellow_square:"; + + export const decode = (str: string): number => { + let [minutes, seconds] = str.split(":"); + return (parseInt(minutes) * 60000) + (parseInt(seconds) * 1000); + } + + export const format = (ms?: number, type: "seconds" | "milliseconds" = "milliseconds"): string => { + if (!ms) return "0:00"; + if (type === "milliseconds") ms = Math.floor(ms / 1000); + + let minutes = Math.floor(ms / 60); + let seconds = (ms % 60).toFixed(0); + return `${minutes}:${seconds.padStart(2, '0')}`; + } + + export const progress = (segments: number, min?: number, max?: number) => { + if (!min || !max) return Array(segments).fill(PROGRESS_UNFILLED),join(""); + + let percent = Math.floor(((min / max) * segments)); + let overall = segments - percent; + return Array(percent).fill(PROGRESS_FILLED).concat(Array(overall).fill(PROGRESS_UNFILLED)).join(""); + } + +} \ No newline at end of file diff --git a/src/lib/utils/youtube.ts b/src/lib/utils/youtube.ts index 2b2237d..b8bc54d 100644 --- a/src/lib/utils/youtube.ts +++ b/src/lib/utils/youtube.ts @@ -1,6 +1,7 @@ import Axios from 'axios'; import { YOUTUBE } from "../../../config.json"; -import { Entities } from './html'; +import { Entities, Time } from './misc'; +import { ytdl } from '../../app'; const YOUTUBE_SHORTENED_URL = "https://youtu.be/"; const YOUTUBE_URL_REGEX = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]+)(\S+)?$/; @@ -10,44 +11,41 @@ const search = async (query: string): Promise => { const res = await Axios.get(url).then(res => res.data).catch(_ => undefined); if (!res) return undefined; - let videoData = res.items[0]; - return new YoutubeMetadata( - videoData.snippet.title, - videoData.snippet.channelTitle, - `${YOUTUBE_SHORTENED_URL}${videoData.id.videoId}`, - `https://i3.ytimg.com/vi/${videoData.id.videoId}/maxresdefault.jpg` - ); - } + let { id: { videoId } } = res.items[0]; + return getMetadata(createShareUrl(videoId)); + } const getVideoIdFromUrl = (url: string) => { let capture = YOUTUBE_URL_REGEX.exec(url); - return capture ? capture[6] : undefined; + return capture ? capture[6] : ""; } const getMetadata = async (url: string): Promise => { - let shortenedUrl = createShareUrl(url); - const urlMetadata: string = `https://www.youtube.com/oembed?url=${shortenedUrl}&format=json`; - const urlThumbnail: string = `https://i3.ytimg.com/vi/${getVideoIdFromUrl(url)}/maxresdefault.jpg`; - const res = await Axios.get(urlMetadata).then(res => res.data).catch(_ => undefined); - if (res) return new YoutubeMetadata(res.title, res.author_name, shortenedUrl, urlThumbnail); + let res = await ytdl.getVideoInfo(url).catch(_ => undefined); + if (res) return new YoutubeMetadata(res.fulltitle, res.channel, createShareUrl(res.display_id), res.thumbnail, res.duration); else return undefined; } -const createShareUrl = (url: string): string => { - return `${YOUTUBE_SHORTENED_URL}${ getVideoIdFromUrl(url) }`; -} +const getThumbnailUrl = (id: string): string => `https://i3.ytimg.com/vi/${id}/maxresdefault.jpg`; +const createShareUrl = (id: string): string => `${YOUTUBE_SHORTENED_URL}${id}`; export class YoutubeMetadata { private title: string; private author: string; private url: string; + private duration: number; private thumbnailUrl: string; - constructor(title: string, author: string, url: string, thumbnailUrl: string) { + constructor(title: string, + author: string, + url: string, + thumbnailUrl: string, + duration: number) { this.title = Entities.decodeEntities(title); this.author = Entities.decodeEntities(author); this.url = url; + this.duration = duration; this.thumbnailUrl = thumbnailUrl; } @@ -66,6 +64,10 @@ export class YoutubeMetadata { getThumbnailUrl(): string { return this.thumbnailUrl; } + + getDuration(): number { + return this.duration; + } } diff --git a/src/lib/voice.ts b/src/lib/voice.ts index ed31f2f..df2b1ff 100644 --- a/src/lib/voice.ts +++ b/src/lib/voice.ts @@ -1,13 +1,8 @@ -import { YOUTUBE } from "../../config.json"; - import { AudioPlayer, AudioPlayerStatus, AudioResource, createAudioPlayer, createAudioResource, getVoiceConnection, joinVoiceChannel } from "@discordjs/voice"; import { VoiceBasedChannel } from "discord.js"; -import { voiceManager } from "../app"; +import { voiceManager, ytdl } from "../app"; import { YoutubeMetadata } from "./utils/youtube"; -import YTDlpWrap from "yt-dlp-wrap"; -const ytdl = new YTDlpWrap(YOUTUBE.BINARY); - export class VoiceManager { private connections: VoiceConnection[]; @@ -52,10 +47,10 @@ export class VoiceConnection { channelId: string; playing?: YoutubeMetadata; + resource?: AudioResource; queue: YoutubeMetadata[]; private timeout?: NodeJS.Timeout; - private resource?: AudioResource; constructor(guildId: string, channelId: string) { this.guildId = guildId;